com.rapidclipse.framework.security.authorization.AuthorizationManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rap-security-auth Show documentation
Show all versions of rap-security-auth Show documentation
Rapid Application Platform / Security / Authentication and Authorization
/*
* 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);
});
}
}
}