You can also check out this example to see the actual code involved in creating a custom security provider
ModeShape can integrate with a custom authentication and authorization service, and it just takes a little bit of code.
The AuthenticationProvider interface
ModeShape defines a simple interface for authenticating users. Each repository can have multiple providers, and a client is authenticated as soon as one of the providers accepts the credentials. The interface is quite simple:
/**
* An interface used by a ModeShape Repository for authenticating users when they create new sessions
* using Repository.login(javax.jcr.Credentials, String)} and related methods.
*/
public interface AuthenticationProvider {
/**
* Authenticate the user that is using the supplied credentials. If the supplied credentials are authenticated, this
* method should construct an ExecutionContext that reflects the authenticated environment, including the context's
* valid SecurityContext security context that will be used for authorization throughout.
*
* Note that each provider is handed a map into which it can place name-value pairs that will be used in the
* Session attributes of the Session that results from this authentication attempt.
* ModeShape will ignore any attributes if this provider does not authenticate the credentials.
*
*
* @param credentials the user's JCR credentials, which may be an AnonymousCredentials if authenticating as an
* anonymous user
* @param repositoryName the name of the JCR repository; never null
* @param workspaceName the name of the JCR workspace; never null
* @param repositoryContext the execution context of the repository, which may be wrapped by this method
* @param sessionAttributes the map of name-value pairs that will be placed into the Session attributes; never null
* @return the execution context for the authenticated user, or null if this provider could not authenticate the user
*/
ExecutionContext authenticate( Credentials credentials,
String repositoryName,
String workspaceName,
ExecutionContext repositoryContext,
Map<String, Object> sessionAttributes );
}
All the parameters are supplied by ModeShape and contain everything necessary to authenticate a client attempting to create a new JCR Session.
Implementations are expected return a new ExecutionContext instance for the user, and this can be created from the repository's execution context by calling repositoryContext.with(securityContext), where securityContext is a custom implementation of the org.modeshape.jcr.security.SecurityContext interface that returns information about the authenticated user:
/**
* A security context provides a pluggable means to support disparate authentication and authorization mechanisms that specify the
* user name and roles.
*
* A security context should only be associated with the execution context after authentication has occurred.
*/
@NotThreadSafe
public interface SecurityContext {
/**
* Return whether this security context is an anonymous context.
* @return true if this context represents an anonymous user, or false otherwise
*/
boolean isAnonymous();
/**
* Returns the authenticated user's name
* @return the authenticated user's name
*/
String getUserName();
/**
* Returns whether the authenticated user has the given role.
* @param roleName the name of the role to check
* @return true if the user has the role and is logged in; false otherwise
*/
boolean hasRole( String roleName );
/**
* Logs the user out of the authentication mechanism.
* For some authentication mechanisms, this will be implemented as a no-op.
*/
void logout();
}
Note that if you want to provide authorization functionality, then your SecurityContext implementation must also implement AuthorizationProvider or AdvancedAuthorizationProvider.
The AuthorizationProvider interface
ModeShape uses its org.modeshape.jcr.security.AuthorizationProvider interface to determine whether a Session has the appropriate privileges to perform reads and writes.
/**
* An interface that can authorize access to specific resources within repositories.
*/
public interface AuthorizationProvider {
/**
* Determine if the supplied execution context has permission for all of the named actions in the named workspace.
* If not all actions are allowed, the method returns false.
*
* @param context the context in which the subject is performing the actions on the supplied workspace
* @param repositoryName the name of the repository containing the workspace content
* @param repositorySourceName <i>This is no longer used and will always be the same as the repositoryName</i>
* @param workspaceName the name of the workspace in which the path exists
* @param path the path on which the actions are occurring
* @param actions the list of {@link ModeShapePermissions actions} to check
* @return true if the subject has privilege to perform all of the named actions on the content at the supplied
* path in the given workspace within the repository, or false otherwise
*/
boolean hasPermission( ExecutionContext context,
String repositoryName,
String repositorySourceName,
String workspaceName,
Path path,
String... actions );
}
Simply have your SecurityContext implementation also implement this interface, and return true whenever the session is allows to perform the requested operations.
The AdvancedAuthorizationProvider interface
ModeShape uses its org.modeshape.jcr.security.AdvancedAuthorizationProvider interface to determine whether a Session has the appropriate privileges to perform reads and writes.
/**
* An interface that can authorize access to specific resources within repositories. Unlike the more basic and simpl
* AuthenticationProvider, this interface allows an implementation to get at additional information with each call to
* hasPermission(Context, Path, String...).
*
* In particular, the supplied Context instance contains the Session that is calling this provider, allowing the
* provider implementation to access authorization-specific content within the repository to determine permissions for other
* repository content.
*
* In these cases, calls to the session to access nodes will result in their own calls to hasPermission(Context, Path, String...).
* Therefore, such implementations need to handle these special authorization-specific content permissions in an explicit fashion.
* It is also adviced that such providers cache as much of the authorization-specifc content as possible, as the
* hasPermission(Context, Path, String...) method is called frequently.
*/
public interface AdvancedAuthorizationProvider {
/**
* Determine if the supplied execution context has permission for all of the named actions in the given context. If not all
* actions are allowed, the method returns false.
*
* @param context the context in which the subject is performing the actions on the supplied workspace
* @param absPath the absolute path on which the actions are occurring, or null if the permissions are at the workspace-level
* @param actions the list of {@link ModeShapePermissions actions} to check
* @return true if the subject has privilege to perform all of the named actions on the content at the supplied path in the
* given workspace within the repository, or false otherwise
*/
boolean hasPermission( Context context,
Path absPath,
String... actions );
}
where Context is a new nested interface nested in AdvancedAuthorizationProvider:
/**
* The context in which the calling session is operating, and which contains session-related information that a provider
* implementation may find useful.
*/
public static interface Context {
/**
* Get the execution context in which this session is running.
*
* @return the session's execution context; never null
*/
public ExecutionContext getExecutionContext();
/**
* Get the session that is requesting this authorization provider to
* {@link AdvancedAuthorizationProvider#hasPermission(Context, Path, String...) determine permissions}. Provider
* implementations are free to use the session to access nodes <i>other</i> than those for which permissions are being
* determined. For example, the implementation may access other <i>authorization-related content</i> inside the same
* repository. Just be aware that such accesses will generate additional calls to the
* {@link AdvancedAuthorizationProvider#hasPermission(Context, Path, String...)} method.
*
* @return the session; never null
*/
public Session getSession();
/**
* Get the name of the repository that is being accessed.
*
* @return the repository name; never null
*/
public String getRepositoryName();
/**
* Get the name of the repository workspace that is being accessed.
*
* @return the workspace name; never null
*/
public String getWorkspaceName();
}
Simply have your SecurityContext implementation also implement this interface, and return true whenever the session is allowed to perform the requested operations.
Putting it all together
To have full control over the authentication & authorization process, you need to implement all the above interfaces and then configure your repository to use the AuthenticationProvider implementation.
For example:
public class SimpleTestSecurityProvider implements AuthenticationProvider, AuthorizationProvider, SecurityContext {
@Override
public ExecutionContext authenticate( Credentials credentials, String repositoryName, String workspaceName,
ExecutionContext repositoryContext, Map<String, Object> sessionAttributes ) {
return repositoryContext.with(this);
}
@Override
public boolean hasPermission( ExecutionContext context, String repositoryName, String repositorySourceName,
String workspaceName, Path absPath, String... actions ) {
return true;
}
@Override
public boolean isAnonymous() {
return false;
}
@Override
public String getUserName() {
return "test user";
}
@Override
public boolean hasRole( String roleName ) {
return true;
}
@Override
public void logout() {
}
}
Note how the authenticate method in the above example places this as the security context. This ensures that the hasPermission method will be called each time a repository operation is performed.
If you wanted more control, you could choose to implement AdvancedAuthorizationProvider instead of AuthorizationProvider.