AuthConfig.java

/*
 * Copyright © 2025 Indiana University
 * All rights reserved.
 *
 * BSD 3-Clause License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * - Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package iu.auth.config;

import java.lang.reflect.Type;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

import edu.iu.IuException;
import edu.iu.IuObject;
import edu.iu.auth.config.IuAuthenticationRealm;
import edu.iu.auth.config.IuAuthorizationClient.AuthMethod;
import edu.iu.auth.config.IuAuthorizationClient.GrantType;
import edu.iu.client.IuJson;
import edu.iu.client.IuJsonAdapter;
import edu.iu.client.IuJsonPropertyNameFormat;
import edu.iu.client.IuVault;
import edu.iu.crypt.WebCryptoHeader;
import edu.iu.crypt.WebEncryption;
import edu.iu.crypt.WebEncryption.Encryption;
import edu.iu.crypt.WebKey;
import edu.iu.crypt.WebKey.Algorithm;
import edu.iu.crypt.WebSignedPayload;
import iu.crypt.CryptJsonAdapters;
import iu.crypt.Jwe;
import iu.crypt.JwsBuilder;
import jakarta.json.JsonString;

/**
 * Authentication and authorization root configuration utility.
 */
public class AuthConfig {
	static {
		IuObject.assertNotOpen(AuthConfig.class);
	}

	private static class StorageConfig<T> {
		private final String prefix;
		private final Consumer<? super T> verifier;
		private final IuJsonAdapter<T> adapter;
		private final IuVault[] vault;

		private StorageConfig(String prefix, Consumer<? super T> verifier, IuJsonAdapter<T> adapter, IuVault... vault) {
			this.prefix = prefix;
			this.verifier = verifier;
			this.adapter = adapter;
			this.vault = vault;
		}
	}

	private static final Map<String, IuAuthConfig> CONFIG = new HashMap<>();
	private static final Map<Class<?>, StorageConfig<?>> STORAGE = new HashMap<>();
	private static boolean sealed;

	static {
		registerDefaults();
	}

	private static void registerDefaults() {
		registerAdapter(AuthMethod.class, AuthMethod.JSON);
		registerAdapter(GrantType.class, GrantType.JSON);
		registerAdapter(Algorithm.class, CryptJsonAdapters.ALG);
		registerAdapter(Encryption.class, CryptJsonAdapters.ENC);
		registerAdapter(WebKey.class, CryptJsonAdapters.WEBKEY);
		registerAdapter(WebCryptoHeader.class, CryptJsonAdapters.JOSE);
		registerAdapter(WebEncryption.class, Jwe.JSON);
		registerAdapter(WebSignedPayload.class, JwsBuilder.JSON);
		registerAdapter(X509Certificate.class, CryptJsonAdapters.CERT);
		registerAdapter(X509CRL.class, CryptJsonAdapters.CRL);
		registerAdapter(IuAuthenticationRealm.Type.class, IuAuthenticationRealm.Type.JSON);
	}

	/**
	 * Registers a configuration descriptor for an authentication realm.
	 * 
	 * <p>
	 * Only one verifier may be registered per realm
	 * </p>
	 * 
	 * @param config principal identity verifier
	 */
	public static synchronized void register(IuAuthConfig config) {
		if (sealed)
			throw new IllegalStateException("sealed");

		IuObject.assertNotOpen(config.getClass());
		IuObject.requireFinalImpl(config.getClass());

		final var realm = config.getRealm();
		if (CONFIG.containsKey(realm))
			throw new IllegalArgumentException("already configured");

		CONFIG.put(realm, config);
	}

	/**
	 * Registers a JSON type adapter for a non-interface configuration class, for
	 * example a custom enum.
	 * 
	 * @param <T>     type
	 * @param type    class
	 * @param adapter {@link IuJsonAdapter}
	 */
	public static synchronized <T> void registerAdapter(Class<T> type, IuJsonAdapter<T> adapter) {
		if (sealed)
			throw new IllegalStateException("sealed");

		if (STORAGE.containsKey(type))
			throw new IllegalArgumentException("already configured");

		STORAGE.put(type, new StorageConfig<>(null, null, adapter));
	}

	/**
	 * Registers an authorization configuration interface that doesn't tie to a
	 * vault configuration name.
	 * 
	 * @param <T>             configuration type
	 * @param configInterface configuration interface
	 */
	public static synchronized <T> void registerInterface(Class<T> configInterface) {
		registerAdapter(configInterface, IuJsonAdapter.from(configInterface,
				IuJsonPropertyNameFormat.LOWER_CASE_WITH_UNDERSCORES, AuthConfig::adaptJson));
	}

	/**
	 * Registers a vault for loading authorization configuration.
	 * 
	 * @param <T>             configuration type
	 * @param prefix          prefix to append to vault key to classify the resource
	 *                        names used by {@link #load(Class, String)}
	 * @param configInterface configuration interface
	 * @param vault           vault to use for loading configuration
	 */
	@SuppressWarnings("unchecked")
	public static synchronized <T> void registerInterface(String prefix, Class<T> configInterface, IuVault... vault) {
		final Consumer<? super T> verifier;
		if (IuAuthenticationRealm.class.isAssignableFrom(configInterface))
			verifier = ((Consumer<? super T>) (Consumer<IuAuthenticationRealm>) IuAuthenticationRealm::verify);
		else
			verifier = null;

		registerInterface(prefix, configInterface, verifier, vault);
	}

	/**
	 * Registers a vault for loading authorization configuration.
	 * 
	 * @param <T>             configuration type
	 * @param prefix          prefix to append to vault key to classify the resource
	 *                        names used by {@link #load(Class, String)}
	 * @param configInterface configuration interface
	 * @param verifier        provides additional verification logic to be apply
	 *                        before returning each loaded instance
	 * @param vault           vault to use for loading configuration
	 */
	public static synchronized <T> void registerInterface(String prefix, Class<T> configInterface,
			Consumer<? super T> verifier, IuVault... vault) {
		if (sealed)
			throw new IllegalStateException("sealed");

		IuObject.require(configInterface, Class::isInterface, configInterface + " is not an interface");
		IuObject.require(Objects.requireNonNull(prefix, "Missing prefix"), a -> a.matches("\\p{Lower}+"),
				"invalid prefix " + prefix);

		if (STORAGE.containsKey(configInterface))
			throw new IllegalArgumentException("already configured");

		final var propertyAdapter = IuJsonAdapter.from(configInterface,
				IuJsonPropertyNameFormat.LOWER_CASE_WITH_UNDERSCORES, AuthConfig::adaptJson);

		final var adapter = IuJsonAdapter.from(v -> {
			if (v instanceof JsonString)
				return load(configInterface, ((JsonString) v).getString());
			else
				return IuObject.convert(v, propertyAdapter::fromJson);
		}, //
				propertyAdapter::toJson);

		STORAGE.put(configInterface,
				Objects.requireNonNull(new StorageConfig<>(prefix + '/', verifier, adapter, vault)));
	}

	/**
	 * Loads a configuration object from vault.
	 * 
	 * @param <T>             configuration type
	 * @param configInterface configuration interface
	 * @param key             vault key
	 * @return loaded configuration
	 */
	public static <T> T load(Class<T> configInterface, String key) {
		final var vaultConfig = Objects.requireNonNull(STORAGE.get(configInterface), "not configured");
		return (new Object() {
			T value;
			Throwable error;

			@SuppressWarnings({ "unchecked", "rawtypes" })
			void check(IuVault vault) {
				final var keyedValue = vault.get(vaultConfig.prefix + key).getValue();
				final var config = IuJson.parse(keyedValue).asJsonObject();

				final var value = IuJson.wrap(config, configInterface, AuthConfig::adaptJson);
				if (vaultConfig.verifier != null)
					((Consumer) vaultConfig.verifier).accept(value);

				this.value = value;
			}

			T load() {
				for (final var v : vaultConfig.vault) {
					error = IuException.suppress(error, () -> check(v));
					if (value != null)
						return value;
				}
				throw IuException.unchecked(error);
			}
		}).load();
	}

	/**
	 * Gets the configuration registered for a realm.
	 * 
	 * @param <T>   configuration type
	 * @param realm authentication realm
	 * @return {@link IuAuthConfig} by realm
	 */
	@SuppressWarnings("unchecked")
	public static <T extends IuAuthConfig> T get(String realm) {
		if (sealed)
			return (T) Objects.requireNonNull(CONFIG.get(realm), "invalid realm");
		else
			throw new IllegalStateException("not sealed");
	}

	/**
	 * Finds configuration registered by interface.
	 * 
	 * @param <T>  configuration type
	 * @param type type
	 * @return {@link IuAuthConfig} by type
	 */
	public static <T extends IuAuthConfig> Iterable<T> get(Class<T> type) {
		if (sealed)
			return () -> CONFIG.values().stream().filter(a -> type.isInstance(a)).map(a -> type.cast(a)).iterator();
		else
			throw new IllegalStateException("not sealed");
	}

	/**
	 * Seals the authentication and authorization configuration.
	 * 
	 * <p>
	 * Until sealed, no per-realm configurations can be used. Once sealed, no new
	 * configurations can be registered. Configuration state is controlled by the
	 * auth module.
	 * </p>
	 */
	public static synchronized void seal() {
		sealed = true;
	}

	/**
	 * Provides additional JSON adapters for configuring the authorization module.
	 * 
	 * @param <T>  target type
	 * @param type type
	 * @return {@link IuJsonAdapter}
	 */
	@SuppressWarnings("unchecked")
	public static <T> IuJsonAdapter<T> adaptJson(Class<T> type) {
		return (IuJsonAdapter<T>) adaptJson((Type) type);
	}

	/**
	 * Provides JSON adapters for components that used
	 * {@link #registerInterface(String, Class, Consumer, IuVault...)} to register
	 * configuration interfaces for authentication and authorization.
	 * 
	 * @param type type
	 * @return {@link IuJsonAdapter}
	 */
	public static IuJsonAdapter<?> adaptJson(Type type) {
		if (STORAGE.containsKey(type))
			return STORAGE.get(type).adapter;
		else
			return IuJsonAdapter.of(type, AuthConfig::adaptJson);
	}

	private AuthConfig() {
	}
}