/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedExceptionAction; import java.util.HashMap; import java.util.Map; import javax.jcr.Credentials; import javax.jcr.LoginException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.SimpleCredentials; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import org.junit.After; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.modeshape.common.FixFor; import org.modeshape.common.junit.SkipLongRunning; import org.modeshape.common.junit.SkipTestRule; import org.modeshape.jcr.RepositoryConfiguration.FieldName; import org.modeshape.jcr.security.AdvancedAuthorizationProvider; import org.modeshape.jcr.security.AuthenticationProvider; import org.modeshape.jcr.security.AuthorizationProvider; import org.modeshape.jcr.security.JaasSecurityContext.UserPasswordCallbackHandler; import org.modeshape.jcr.security.SecurityContext; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.StringFactory; import org.modeshape.schematic.Schematic; import org.modeshape.schematic.document.Document; import org.modeshape.schematic.document.EditableArray; import org.modeshape.schematic.document.EditableDocument; public class AuthenticationAndAuthorizationTest { @Rule public TestRule skipTestRule = new SkipTestRule(); private static final String REPO_NAME = "testRepo"; @BeforeClass public static void beforeAll() { // Initialize PicketBox ... JaasTestUtil.initJaas("security/jaas.conf.xml"); } protected JcrRepository repository; protected JcrSession session; @After public void afterEach() throws Exception { if (repository != null) { try { TestingUtil.killRepositories(repository); } finally { repository = null; session = null; } } } /** * Start the repository using the supplied repository configuration. Note that this does <i>not</i> create a session. * * @param doc the document containing the configuration; may not be null * @param repoName the name of the repository; may not be null * @throws Exception if there is a problem starting the repository */ protected void startRepositoryWith( Document doc, String repoName ) throws Exception { RepositoryConfiguration config = new RepositoryConfiguration(doc, repoName, new TestingEnvironment()); repository = new JcrRepository(config); repository.start(); } /** * Creates a repository configuration that uses the specified JAAS provider and optionally enables anonymous logins. * <p> * The configuration for "repositoryName" as the repository name, anonymous logins disabled, and "modeshape-jcr" as the JAAS * policy looks as follows: * * <pre> * { * "name" : "repositoryNameParameter"; * "security" : { * "anonymous" : { * "roles" : [] * }, * "providers" : [ * { * "classname" : "JAAS", * "policyName" : "modeshape-jcr" * } * ] * } * } * </pre> * * If anonymous logins <i>are</i> enabled, then they are also enabled on failed logins: * * <pre> * { * "name" : "repositoryNameParameter"; * "security" : { * "anonymous" : { * "useOnFailedLogin" : true * }, * "providers" : [ * { * "classname" : "JAAS", * "policyName" : "modeshape-jcr" * } * ] * } * } * </pre> * * </p> * * @param repositoryName the name of the repository; may not be null * @param jaasPolicyName the name of the jaas policy; may be null if JAAS should be not be enabled * @param anonymousRoleNames the anonymous role names, or empty if anonymous logins should be disabled * @return the configuration document; never null */ protected Document createRepositoryConfiguration( String repositoryName, String jaasPolicyName, String... anonymousRoleNames ) { EditableDocument doc = Schematic.newDocument("name", repositoryName); EditableDocument security = doc.getOrCreateDocument("security"); if (anonymousRoleNames == null || anonymousRoleNames.length == 0) { // Disable anonymous logins ... EditableDocument anonymous = security.getOrCreateDocument("anonymous"); anonymous.setArray("roles"); } else { // Set the roles and use on failed logins ... EditableDocument anonymous = security.getOrCreateDocument("anonymous"); anonymous.setArray("roles", (Object[])anonymousRoleNames); anonymous.setBoolean("useOnFailedLogin", true); } if (jaasPolicyName != null) { // Add the JAAS provider ... EditableArray providers = security.getOrCreateArray("providers"); EditableDocument jaas = Schematic.newDocument(FieldName.CLASSNAME, "JAAS", "policyName", "modeshape-jcr"); providers.addDocument(jaas); } return doc; } @Test public void shouldLogInAsAnonymousUsingNoCredentials() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr-non-existant"; String[] anonRoleNames = {ModeShapeRoles.READWRITE}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); session = repository.login(); session.getRootNode().getPath(); session.getRootNode().addNode("someNewNode"); } @Test public void shouldLogInAsAnonymousWithReadOnlyPrivilegesUsingNoCredentials() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr-non-existant"; String[] anonRoleNames = {ModeShapeRoles.READONLY}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); session = repository.login(); session.getRootNode().getPath(); try { session.getRootNode().addNode("someNewNode"); fail("Should not have been able to update content with a read-only user"); } catch (javax.jcr.AccessDeniedException e) { // expected } } @Test public void shouldLogInAsUserWithReadOnlyRole() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); session = repository.login(new SimpleCredentials(ModeShapeRoles.READONLY, ModeShapeRoles.READONLY.toCharArray())); session.getRootNode().getPath(); session.getRootNode().getDefinition(); try { session.getRootNode().addNode("someNewNode"); fail("Should not have been able to update content with a read-only user"); } catch (javax.jcr.AccessDeniedException e) { // expected } } @Test public void shouldLogInAsUserWithReadWriteRole() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); session = repository.login(new SimpleCredentials("readwrite", "readwrite".toCharArray())); session.getRootNode().getPath(); session.getRootNode().getDefinition(); session.getRootNode().addNode("someNewNode"); } @Test public void shouldNotAllowAnonymousLoginsWhenUsingOnlyJaas() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); try { session = repository.login(); fail("Should not have been able to login anonymously if anonymous logins are disabled"); } catch (LoginException e) { // expected } } @Test public void shouldLogInAsAnonymousUserIfNoProviderAuthenticatesCredentials() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {ModeShapeRoles.READONLY}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); session = repository.login(new SimpleCredentials("readwrite", "wrongpassword".toCharArray())); assertThat(session.isAnonymous(), is(true)); session.getRootNode().getPath(); session.getRootNode().getDefinition(); try { session.getRootNode().addNode("someNewNode"); fail("Should not have been able to update content with a read-only user"); } catch (javax.jcr.AccessDeniedException e) { // expected } } @Test public void shouldLogInAsWritableAnonymousUserIfNoProviderAuthenticatesCredentials() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {ModeShapeRoles.READWRITE}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); session = repository.login(new SimpleCredentials("readwrite", "wrongpassword".toCharArray())); assertThat(session.isAnonymous(), is(true)); session.getRootNode().getPath(); session.getRootNode().getDefinition(); session.getRootNode().addNode("someNewNode"); } @SuppressWarnings( "cast" ) @Test public void shouldAllowLoginWithNoCredentialsInPrivilegedBlock() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {ModeShapeRoles.READWRITE}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); // Verify the JAAS was configured correctly ... session = repository.login(new SimpleCredentials("readwrite", "readwrite".toCharArray())); LoginContext login = new LoginContext("modeshape-jcr", new UserPasswordCallbackHandler("superuser", "superuser".toCharArray())); login.login(); Subject subject = login.getSubject(); Session session = (Session)Subject.doAsPrivileged(subject, new PrivilegedExceptionAction<Session>() { @Override public Session run() throws Exception { return repository.login(); } }, AccessController.getContext()); assertThat(session, is(notNullValue())); assertThat(session.getUserID(), is("superuser")); login.logout(); } @Test( expected = javax.jcr.LoginException.class ) public void shouldNotAllowLoginIfCredentialsDoNotProvideJaasMethod() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); repository.login(new Credentials() { private static final long serialVersionUID = 1L; }); } @Test( expected = javax.jcr.LoginException.class ) public void shouldNotAllowLoginIfCredentialsReturnNullAccessControlContext() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); repository.login(new Credentials() { private static final long serialVersionUID = 1L; @SuppressWarnings( "unused" ) public AccessControlContext getAccessControlContext() { return null; } }); } @Test( expected = javax.jcr.LoginException.class ) public void shouldNotAllowLoginIfCredentialsReturnNullLoginContext() throws Exception { String repoName = REPO_NAME; String jaasPolicyName = "modeshape-jcr"; String[] anonRoleNames = {}; Document config = createRepositoryConfiguration(repoName, jaasPolicyName, anonRoleNames); startRepositoryWith(config, repoName); repository.login(new Credentials() { private static final long serialVersionUID = 1L; @SuppressWarnings( "unused" ) public LoginContext getLoginContext() { return null; } }); } @Test @FixFor( "MODE-1938" ) @SkipLongRunning public void shouldNotLeakUninitializedWorkspaceCaches() throws Exception { int runCount = 200; for (int i = 0; i < runCount; i++) { shouldLogInAsAnonymousUserIfNoProviderAuthenticatesCredentials(); afterEach(); } } @Test @FixFor( "MODE-2225" ) public void shouldInvokeAuthorizationProviderWhenIteratingNodes() throws Exception { //this will import an initial structure of nodes (see xmlImport/docWithChildren.xml) repository = TestingUtil.startRepositoryWithConfig("config/custom-authentication-provider-config-1.json"); assertPermissionsCheckedWhenIteratingChildren(); } @Test @FixFor( "MODE-2225" ) public void shouldInvokeAdvancedAuthorizationProviderWhenIteratingNodes() throws Exception { //this will import an initial structure of nodes (see xmlImport/docWithChildren.xml) repository = TestingUtil.startRepositoryWithConfig("config/custom-authentication-provider-config-2.json"); assertPermissionsCheckedWhenIteratingChildren(); } private void assertPermissionsCheckedWhenIteratingChildren() throws RepositoryException { session = repository.login(); Node testRoot = session.getNode("/testRoot"); NodeIterator children = testRoot.getNodes(); while (children.hasNext()) { children.nextNode(); } TestSecurityProvider provider = (TestSecurityProvider) session.context().getSecurityContext(); Map<String, String> actionsByNodePath = provider.getActionsByNodePath(); assertTrue("READ permission not checked for node", actionsByNodePath.containsKey("/testRoot")); assertTrue("READ permission not checked for node", actionsByNodePath.containsKey("/testRoot/node3")); assertTrue("READ permission not checked for node", actionsByNodePath.containsKey("/testRoot/node2")); assertTrue("READ permission not checked for node", actionsByNodePath.containsKey("/testRoot/node1")); } public static abstract class TestSecurityProvider implements AuthenticationProvider, SecurityContext { protected final Map<String, String> actionsByNodePath = new HashMap<>(); protected StringFactory stringFactory; @Override public ExecutionContext authenticate( Credentials credentials, String repositoryName, String workspaceName, ExecutionContext repositoryContext, Map<String, Object> sessionAttributes ) { stringFactory = repositoryContext.getValueFactories().getStringFactory(); return repositoryContext.with(this); } @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() { } public Map<String, String> getActionsByNodePath() { return actionsByNodePath; } } public static class SimpleTestSecurityProvider extends TestSecurityProvider implements AuthorizationProvider { @Override public boolean hasPermission( ExecutionContext context, String repositoryName, String repositorySourceName, String workspaceName, Path absPath, String... actions ) { actionsByNodePath.put(stringFactory.create(absPath), actions[0]); return true; } } public static class AdvancedTestSecurityProvider extends TestSecurityProvider implements AdvancedAuthorizationProvider { @Override public boolean hasPermission( Context context, Path absPath, String... actions ) { actionsByNodePath.put(stringFactory.create(absPath), actions[0]); return true; } } }