All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.rapidclipse.framework.security.authorization.AuthorizationManager Maven / Gradle / Ivy

There is a newer version: 14.0.0
Show newest version
/*
 * Copyright (C) 2013-2023 by XDEV Software, All Rights Reserved.
 *
 * This file is part of the RapidClipse Application Platform (RAP).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 *
 * Contributors:
 *     XDEV Software - initial API and implementation
 */
package com.rapidclipse.framework.security.authorization;

import static java.util.Objects.requireNonNull;

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;


/**
 * Composite type that combines {@link AuthorizationRegistry} with managing
 * aspects of {@link PermissionManager}, {@link RoleManager} and
 * {@link SubjectManager}.
 *
 * @author XDEV Software (TM)
 */
public interface AuthorizationManager
	extends AuthorizationRegistry, PermissionManager, RoleManager, SubjectManager
{
	/**
	 * @return the {@link AuthorizationRegistry} instance, possibly this
	 *         instance itself.
	 */
	public AuthorizationRegistry authorizationRegistry();

	/**
	 * Causes the authorization information to be reloaded and all depending
	 * data structures to be rebuilt accordingly.
	 */
	public void reloadAuthorizations();

	/**
	 * Returns the {@link Resource} instance identified by the passed name.
	 *
	 * @param name
	 *            the name identifying the desired resource.
	 * @return the {@link Resource} instance identified by the passed name.
	 */
	public Resource resource(String name);

	/**
	 * Creates a new {@link AuthorizationManager} instance configured by the
	 * passed part instances.
	 *
	 * @param configurationProvider
	 *            the source for the {@link AuthorizationConfiguration} to be
	 *            used.
	 * @param resourceProvider
	 *            the provider of {@link Resource} instances.
	 * @param resourceUpdater
	 *            the updating logic for the {@link Resource} instances provided
	 *            by the resource provider.
	 * @param permissionProvider
	 *            the provider of {@link Permission} instances.
	 * @param roleProvider
	 *            the provider of {@link Role} instances.
	 * @param roleUpdater
	 *            the updating logic for the {@link Role} instances provided by
	 *            the role provider.
	 * @param subjectUpdater
	 *            the providing/updating logic for {@link SubjectManager}
	 *            instances.
	 * @return a new {@link AuthorizationManager} instance.
	 */
	public static AuthorizationManager New(
		final AuthorizationConfigurationProvider configurationProvider,
		final ResourceProvider resourceProvider,
		final ResourceUpdater resourceUpdater,
		final PermissionProvider permissionProvider,
		final RoleProvider roleProvider,
		final RoleUpdater roleUpdater,
		final SubjectUpdater subjectUpdater)
	{
		return new Default(requireNonNull(configurationProvider),
			requireNonNull(resourceProvider), requireNonNull(resourceUpdater),
			requireNonNull(permissionProvider), requireNonNull(roleProvider),
			requireNonNull(roleUpdater), requireNonNull(subjectUpdater));
	}

	/**
	 * Creates a new {@link AuthorizationManager} instance using the passed
	 * {@link AuthorizationConfigurationProvider} and default implementations
	 * for authorization entity providers and updaters. Also see
	 * {@link #New(AuthorizationConfigurationProvider, ResourceProvider, ResourceUpdater, PermissionProvider, RoleProvider, RoleUpdater, SubjectUpdater)}.
	 *
	 * @param configurationProvider
	 *            the source for the {@link AuthorizationConfiguration} to be
	 *            used.
	 * @return a new {@link AuthorizationManager} instance.
	 * @see #New(AuthorizationConfigurationProvider, ResourceProvider,
	 *      ResourceUpdater, PermissionProvider, RoleProvider, RoleUpdater,
	 *      SubjectUpdater)
	 */
	public static AuthorizationManager New(
		final AuthorizationConfigurationProvider configurationProvider)
	{
		return New(configurationProvider, Resource::provide, Resource::update, Permission::New,
			Role::provide, Role::update, Subject::update);
	}

	/**
	 * Creates a new {@link AuthorizationManager} instance from a {@link File}
	 * assumed to contain an XML configuration suitable for processing by a
	 * {@link XmlAuthorizationConfigurationProvider}.
	 *
	 * @param xmlFile
	 *            the file containing the XML authorization configuration.
	 * @return a new {@link AuthorizationManager} instance.
	 */
	public static AuthorizationManager NewFromXmlFile(final File xmlFile)
	{
		return New(XmlAuthorizationConfigurationProvider.New(xmlFile));
	}

	/**
	 * @return a new {@link AuthorizationManager.Builder} instance.
	 */
	public static Builder Builder()
	{
		return new Builder.Default();
	}

	/**
	 * A builder pattern utility type that allows more a flexible and
	 * ergonomical assembly of {@link AuthorizationManager} instances than using
	 * a constructor method directly.
	 *
	 * @author XDEV Software (TM)
	 */
	public interface Builder
	{
		/**
		 * Sets the {@link AuthorizationConfigurationProvider} instance to be
		 * used in the assembly.
		 *
		 * @param configurationProvider
		 * @return this
		 */
		public Builder configurationProvider(
			AuthorizationConfigurationProvider configurationProvider);

		/**
		 * Sets the {@link ResourceProvider} instance to be used in the
		 * assembly.
		 *
		 * @param resourceProvider
		 *            the {@link ResourceProvider} instance to be set.
		 * @return this
		 */
		public Builder resourceProvider(ResourceProvider resourceProvider);

		/**
		 * Sets the {@link ResourceUpdater} instance to be used in the assembly.
		 *
		 * @param resourceUpdater
		 *            the {@link ResourceUpdater} instance to be set.
		 * @return this
		 */
		public Builder resourceUpdater(ResourceUpdater resourceUpdater);

		/**
		 * Sets the {@link PermissionProvider} instance to be used in the
		 * assembly.
		 *
		 * @param permissionProvider
		 *            the {@link PermissionProvider} instance to be set.
		 * @return this
		 */
		public Builder permissionProvider(PermissionProvider permissionProvider);

		/**
		 * Sets the {@link RoleProvider} instance to be used in the assembly.
		 *
		 * @param roleProvider
		 *            the {@link RoleProvider} instance to be set.
		 * @return this
		 */
		public Builder roleProvider(RoleProvider roleProvider);

		/**
		 * Sets the {@link RoleUpdater} instance to be used in the assembly.
		 *
		 * @param roleUpdater
		 *            the {@link RoleUpdater} instance to be set.
		 * @return this
		 */
		public Builder roleUpdater(RoleUpdater roleUpdater);

		/**
		 * Sets the {@link SubjectUpdater} instance to be used in the assembly.
		 *
		 * @param subjectUpdater
		 *            the {@link SubjectUpdater} instance to be set.
		 * @return this
		 */
		public Builder subjectUpdater(SubjectUpdater subjectUpdater);

		/**
		 * Creates a new {@link AuthorizationManager} instance from the
		 * previously set assembly part instances.
		 *
		 * @return a new {@link AuthorizationManager} instance
		 */
		public AuthorizationManager build();

		/**
		 * Simple {@link AuthorizationManager.Builder} implementation.
		 *
		 * @author XDEV Software (TM)
		 */
		public final class Default implements Builder
		{
			///////////////////////////////////////////////////////////////////////////
			// instance fields //
			////////////////////

			private AuthorizationConfigurationProvider configurationProvider = null;
			private ResourceProvider                   resourceProvider      = Resource::provide;
			private ResourceUpdater                    resourceUpdater       = Resource::update;
			private PermissionProvider                 permissionProvider    = Permission::New;
			private RoleProvider                       roleProvider          = Role::provide;
			private RoleUpdater                        roleUpdater           = Role::update;
			private SubjectUpdater                     subjectUpdater        = Subject::update;

			///////////////////////////////////////////////////////////////////////////
			// override methods //
			/////////////////////

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default configurationProvider(
				final AuthorizationConfigurationProvider configurationProvider)
			{
				this.configurationProvider = configurationProvider;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default resourceProvider(final ResourceProvider resourceProvider)
			{
				this.resourceProvider = resourceProvider;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default resourceUpdater(final ResourceUpdater resourceUpdater)
			{
				this.resourceUpdater = resourceUpdater;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default permissionProvider(
				final PermissionProvider permissionProvider)
			{
				this.permissionProvider = permissionProvider;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default roleProvider(final RoleProvider roleProvider)
			{
				this.roleProvider = roleProvider;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default roleUpdater(final RoleUpdater roleUpdater)
			{
				this.roleUpdater = roleUpdater;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public Builder.Default subjectUpdater(final SubjectUpdater subjectUpdater)
			{
				this.subjectUpdater = subjectUpdater;
				return this;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public AuthorizationManager build()
			{
				return AuthorizationManager.New(requireNonNull(this.configurationProvider),
					requireNonNull(this.resourceProvider), requireNonNull(this.resourceUpdater),
					requireNonNull(this.permissionProvider), requireNonNull(this.roleProvider),
					requireNonNull(this.roleUpdater), requireNonNull(this.subjectUpdater));
			}

		}

	}

	/**
	 * Simple {@link AuthorizationManager} default implementation that uses an
	 * internally created locking instance and connects delegate
	 * {@link PermissionManager}, {@link RoleManager} and {@link SubjectManager}
	 * instances to it.
	 *
	 * @author XDEV Software (TM)
	 */
	public final class Default implements AuthorizationManager
	{
		///////////////////////////////////////////////////////////////////////////
		// instance fields //
		////////////////////

		private final AuthorizationConfigurationProvider configurationProvider;
		private final ResourceProvider                   resourceProvider;
		private final ResourceUpdater                    resourceUpdater;
		private final PermissionProvider                 permissionProvider;
		private final RoleProvider                       roleProvider;
		private final RoleUpdater                        roleUpdater;
		private final SubjectUpdater                     subjectUpdater;

		private final HashMap                       resourceTable   = new HashMap<>();
		private final HashMap> permissionTable = new HashMap<>();
		private final HashMap                           roleTable       = new HashMap<>();
		private final HashMap                        subjectTable    = new HashMap<>();

		private final Object            sharedLock = new Object();
		private final PermissionManager permissionManager;
		private final RoleManager       roleManager;
		private final SubjectManager    subjectManager;

		private boolean initialized = false;

		///////////////////////////////////////////////////////////////////////////
		// constructors //
		/////////////////

		/**
		 * Implementation detail constructor that might change in the future.
		 */
		protected Default(
			final AuthorizationConfigurationProvider configurationProvider,
			final ResourceProvider resourceProvider,
			final ResourceUpdater resourceUpdater,
			final PermissionProvider permissionProvider,
			final RoleProvider roleProvider,
			final RoleUpdater roleUpdater,
			final SubjectUpdater subjectUpdater)
		{
			super();
			this.configurationProvider = configurationProvider;
			this.resourceProvider      = resourceProvider;
			this.resourceUpdater       = resourceUpdater;
			this.permissionProvider    = permissionProvider;
			this.roleProvider          = roleProvider;
			this.roleUpdater           = roleUpdater;
			this.subjectUpdater        = subjectUpdater;
			this.permissionManager     = PermissionManager.New(this.sharedLock, permissionProvider,
				this.permissionTable);
			this.roleManager           = RoleManager.New(this.sharedLock, this.roleTable);
			this.subjectManager        = SubjectManager.New(this.sharedLock, this.subjectTable);
		}

		///////////////////////////////////////////////////////////////////////////
		// override methods //
		/////////////////////

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Object lockPermissionRegistry()
		{
			this.ensureInitialized();
			return this.sharedLock;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Object lockRoleRegistry()
		{
			this.ensureInitialized();
			return this.sharedLock;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Object lockSubjectRegistry()
		{
			this.ensureInitialized();
			return this.sharedLock;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Resource resource(final String name)
		{
			this.ensureInitialized();

			// must lock locally as there is no registry instance to handle it
			// (26.06.2014 TM)TODO: resource Registry?
			synchronized(this.sharedLock)
			{
				return this.resourceTable.get(name);
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Permission permission(final Resource resource, final Integer factor)
		{
			this.ensureInitialized();
			return this.permissionManager.permission(resource, factor);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Role role(final String roleName)
		{
			this.ensureInitialized();
			return this.roleManager.role(roleName);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Subject subject(final String subjectName)
		{
			this.ensureInitialized();
			return this.subjectManager.subject(subjectName);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Permission providePermission(final Resource resource, final Integer factor)
		{
			this.ensureInitialized();
			return this.permissionManager.providePermission(resource, factor);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Map roles()
		{
			this.ensureInitialized();
			return this.roleManager.roles();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Map subjects()
		{
			this.ensureInitialized();
			return this.subjectManager.subjects();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final AuthorizationRegistry authorizationRegistry()
		{
			this.ensureInitialized();

			// through the registry encapsulation this manager implementation
			// itself acts as a registry.
			return this;
		}

		private void ensureInitialized()
		{
			synchronized(this.sharedLock)
			{
				if(this.initialized)
				{
					return;
				}
				this.build();
				this.initialized = true;
			}
		}

		private void build()
		{
			final HashMap                       resources   = new HashMap<>();
			final HashMap> permissions = new HashMap<>();
			final HashMap                           roles       = new HashMap<>();
			final HashMap                        subjects    = new HashMap<>();

			// prepare updaters (normaly no-op, but important hooking
			// opportunity for custom logic)
			this.resourceUpdater.prepareResourceUpdate(this.resourceTable.values());
			this.roleUpdater.prepareRoleUpdate(this.roleTable.values());
			this.subjectUpdater.prepareSubjectUpdate(this.subjectTable.values());

			try
			{
				/*
				 * note that building mutates already existing instances. This
				 * can't be prevented if instance identity (outside references)
				 * is to be preserved.
				 */
				final AuthorizationConfiguration configuration = this.configurationProvider
					.provideConfiguration();

				// update existing or get new instances via the provided
				// configuration
				this.buildResourceTable(resources, configuration);
				this.buildRoleTable(roles, configuration, resources, permissions);
				this.buildSubjectTable(subjects, configuration, roles);

				// signal updaters that the process has been completed
				// successfully
				// (26.06.2014 TM)TODO: separate into dedicated trys with
				// exception collecting?
				this.resourceUpdater.commitResourceUpdate(resources.values());
				this.roleUpdater.commitRoleUpdate(roles.values());
				this.subjectUpdater.commitSubjectUpdate(subjects.values());
			}
			catch(final Exception e)
			{
				// rollback the updating process on any exception
				this.resourceUpdater.rollbackResourceUpdate(resources.values(), e);
				this.roleUpdater.rollbackRoleUpdate(roles.values(), e);
				this.subjectUpdater.rollbackSubjectUpdate(subjects.values(), e);

				// pass exception along
				throw e;
			}
			finally
			{
				// clean up after logic is completed, one way or another
				this.resourceUpdater.cleanupResourceUpdate();
				this.roleUpdater.cleanupRoleUpdate();
				this.subjectUpdater.cleanupSubjectUpdate();
			}

			/*
			 * clear and update registry tables. Must be the same instances as
			 * they are referenced by / linked into registry instances. Note
			 * that these methods "should" be exception-free.
			 */
			this.resourceTable.clear();
			this.permissionTable.clear();
			this.roleTable.clear();
			this.subjectTable.clear();

			this.resourceTable.putAll(resources);
			this.permissionTable.putAll(permissions);
			this.roleTable.putAll(roles);
			this.subjectTable.putAll(subjects);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void reloadAuthorizations()
		{
			synchronized(this.sharedLock)
			{
				this.initialized = false;
				this.ensureInitialized();
			}
		}

		private static final  Set resolve(
			final Set names,
			final HashMap mapping)
		{
			if(names == null)
			{
				return null;
			}

			final HashSet resolved = new HashSet<>(names.size());

			for(final String name : names)
			{
				final E element = mapping.get(name);
				if(element == null)
				{
					throw new RuntimeException("Missing element: " + name);
				}
				resolved.add(element);
			}

			return resolved;
		}

		private static final Permission lookupPermission(
			final HashMap> table,
			final Resource resource,
			final Integer factor)
		{
			final HashMap subTable = table.get(resource);
			return subTable == null ? null : subTable.get(factor);
		}

		private static final void putPermission(
			final HashMap> table,
			final Resource resource,
			final Integer factor,
			final Permission permission)
		{
			HashMap subTable = table.get(resource);
			if(subTable == null)
			{
				table.put(resource, subTable = new HashMap<>());
			}
			subTable.put(factor, permission);
		}

		private static final Set resolvePermissions(
			final HashMap> oldPermissionTable,
			final HashMap> newPermissionTable,
			final Map definitions,
			final Map resources,
			final PermissionProvider permissionProvider)
		{
			if(definitions == null)
			{
				return null;
			}

			final HashSet permissions = new HashSet<>();

			definitions.forEach((k, v) -> {
				final Resource resource = resources.get(k);
				if(resource == null)
				{
					throw new RuntimeException("Resource not found: " + k); // (18.06.2014
																			// TM)TODO:
																			// proper
																			// exception
				}

				Permission permission = lookupPermission(newPermissionTable, resource, v);
				if(permission == null)
				{
					permission = permissionProvider.providePermission(resource, v);
				}
				if(permission == null)
				{
					throw new RuntimeException(
						"Permission provider failure for " + k + " (" + v + ")"); // (18.06.2014
																					// TM)TODO:
																					// proper
																					// exception
				}
				putPermission(newPermissionTable, resource, v, permission);
				permissions.add(permission);
			});

			return permissions;
		}

		private void buildResourceTable(
			final HashMap newResources,
			final AuthorizationConfiguration configuration)
		{
			final ResourceProvider          resourceProvider = this.resourceProvider;
			final ResourceUpdater           resourceUpdater  = this.resourceUpdater;
			final HashMap oldResources     = this.resourceTable;

			final Map> resourcesDefinitions = configuration
				.resourceResources();

			/*
			 * resolve instance names to actual instances by looking up already
			 * registered existing ones or getting new ones from the provider.
			 * String references are passed as a means for early validation.
			 */
			resourcesDefinitions.forEach((resourceName, value) -> {
				final Resource resource = resourceProvider
					.provideResource(oldResources.get(resourceName), resourceName, value);

				// always put returned instance in case the provider discarded
				// the old instance and created a new one.
				newResources.put(resourceName, resource);
			});

			/*
			 * Update the instances in a second pass after all instances have
			 * been resolved. Children collections have to be resolved locally
			 * for passing the actual collections to the updater.
			 */
			newResources.forEach((name, value) -> {
				resourceUpdater.updateResource(value, name,
					resolve(resourcesDefinitions.get(name), newResources));
			});
		}

		private void buildRoleTable(
			final HashMap newRoles,
			final AuthorizationConfiguration config,
			final Map resources,
			final HashMap> newPermissionTable)
		{
			final RoleProvider                                    roleProvider       = this.roleProvider;
			final PermissionProvider                              permissionProvider = this.permissionProvider;
			final RoleUpdater                                     roleUpdater        = this.roleUpdater;
			final HashMap                           oldRoles           = this.roleTable;
			final HashMap> oldPermissionTable = this.permissionTable;

			final Map>          roleRoles       = config.roleRoles();
			final Map> rolePermissions = config
				.rolePermissions();

			/*
			 * resolve instance names to actual instances by looking up already
			 * registered existing ones or getting new ones from the provider.
			 * String references are passed as a means for early validation.
			 */
			roleRoles.forEach((roleName, value) -> {
				final Map permissions  = rolePermissions.get(roleName);
				final Role                 existingRole = oldRoles.get(roleName);
				final Role                 role         = roleProvider.provideRole(existingRole, roleName, value,
					permissions != null ? permissions.keySet() : Collections.emptySet());

				// always put returned instance in case the provider discarded
				// the old instance and created a new one.
				newRoles.put(roleName, role);
			});
			// complementary pass to cover definitions that only have
			// permissions, no parent roles
			rolePermissions.forEach((roleName, value) -> {
				if(newRoles.get(roleName) != null)
				{
					// role already covered, continue loop
					return;
				}

				final Role role = roleProvider.provideRole(oldRoles.get(roleName), roleName,
					Collections.emptySet(), // obviously empty, otherwise
											// first loop would have covered
											// it already
					value.keySet());

				// always put returned instance in case the provider discarded
				// the old instance and created a new one.
				newRoles.put(roleName, role);
			});

			/*
			 * Update the instances in a second pass after all instances have
			 * been resolved. Children collections have to be resolved locally
			 * for passing the actual collections to the updater.
			 */
			newRoles.forEach((name, value) -> {
				roleUpdater.updateRole(value, name, resolve(roleRoles.get(name), newRoles),
					resolvePermissions(oldPermissionTable, newPermissionTable,
						rolePermissions.get(name), resources, permissionProvider));
			});
		}

		private void buildSubjectTable(
			final HashMap newSubjects,
			final AuthorizationConfiguration configuration,
			final HashMap roles)
		{
			final SubjectUpdater           subjectUpdater = this.subjectUpdater;
			final HashMap oldSubjects    = this.subjectTable;

			final Map> subjectDefinitions = configuration
				.subjectRoles();

			/*
			 * subject resolving and updating can be done in one pass as it has
			 * no type-recursion (subject knows other subjects) like resource or
			 * role.
			 */
			// for(final KeyValue> subjDef :
			// subjectDefinitions.)
			subjectDefinitions.forEach((subjectName, value) -> {
				final Subject subject = subjectUpdater.updateSubject(oldSubjects.get(subjectName),
					subjectName, resolve(value, roles));
				if(subject == null)
				{
					throw new RuntimeException("Subject providing failure for " + subjectName); // (18.06.2014
																								// TM)TODO:
																								// proper
																								// exception
				}

				// always put returned instance in case the provider discarded
				// the old instance and created a new one.
				newSubjects.put(subjectName, subject);
			});
		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy