diff --git a/data-model/src/main/java/org/alfresco/service/cmr/security/PermissionService.java b/data-model/src/main/java/org/alfresco/service/cmr/security/PermissionService.java index 7e74dcd67d..40592d7d23 100644 --- a/data-model/src/main/java/org/alfresco/service/cmr/security/PermissionService.java +++ b/data-model/src/main/java/org/alfresco/service/cmr/security/PermissionService.java @@ -2,7 +2,7 @@ * #%L * Alfresco Data model classes * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2024 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -78,6 +78,28 @@ public interface PermissionService */ public static final String GUEST_AUTHORITY = "ROLE_GUEST"; + /** + * The dynamic authority for the Admin service account. + */ + String ADMIN_SVC_AUTHORITY = "ROLE_ADMIN_SERVICE_ACCOUNT"; + + /** + * The dynamic authority for the Collaborator service account. + */ + String COLLABORATOR_SVC_AUTHORITY = "ROLE_COLLABORATOR_SERVICE_ACCOUNT"; + + /** + * The dynamic authority for the Editor service account. + */ + String EDITOR_SVC_AUTHORITY = "ROLE_EDITOR_SERVICE_ACCOUNT"; + + /** + * A convenient set of service account authorities to simplify checks + * for whether a given authority is a service account authority or not. + */ + Set SVC_AUTHORITIES_SET = Set.of(ADMIN_SVC_AUTHORITY, COLLABORATOR_SVC_AUTHORITY, + EDITOR_SVC_AUTHORITY); + /** * The permission for all - not defined in the model. Repsected in the code. */ diff --git a/repository/src/main/java/org/alfresco/repo/security/permissions/dynamic/ServiceAccountAuthority.java b/repository/src/main/java/org/alfresco/repo/security/permissions/dynamic/ServiceAccountAuthority.java new file mode 100644 index 0000000000..245667da5e --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/security/permissions/dynamic/ServiceAccountAuthority.java @@ -0,0 +1,83 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.permissions.dynamic; + +import java.util.Optional; +import java.util.Set; + +import org.alfresco.repo.serviceaccount.ServiceAccountRegistry; +import org.alfresco.repo.security.permissions.DynamicAuthority; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * This class represents a dynamic authority for service accounts in the system. + * + * @author Jamal Kaabi-Mofrad + */ +public class ServiceAccountAuthority implements DynamicAuthority, InitializingBean +{ + private String authority; + private ServiceAccountRegistry serviceAccountRegistry; + + public void setAuthority(String authority) + { + this.authority = authority; + } + + public void setServiceAccountRegistry(ServiceAccountRegistry serviceAccountRegistry) + { + this.serviceAccountRegistry = serviceAccountRegistry; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "authority", authority); + PropertyCheck.mandatory(this, "serviceAccountRegistry", serviceAccountRegistry); + } + + @Override + public boolean hasAuthority(NodeRef nodeRef, String userName) + { + Optional role = serviceAccountRegistry.getServiceAccountRole(userName); + return role.isPresent() && role.get().equals(this.getAuthority()); + } + + @Override + public String getAuthority() + { + return this.authority; + } + + @Override + public Set requiredFor() + { + return null; + } +} diff --git a/repository/src/main/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistry.java b/repository/src/main/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistry.java new file mode 100644 index 0000000000..29af0027b0 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistry.java @@ -0,0 +1,61 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.serviceaccount; + +import java.util.Optional; +import java.util.Set; + +/** + * A service account registry that allows service accounts to be registered + * with their corresponding roles. + * + * @author Jamal Kaabi-Mofrad + */ +public interface ServiceAccountRegistry +{ + /** + * Registers a service account with its corresponding role. + * + * @param serviceAccountName The name of the service account to be registered. + * @param serviceAccountRole The role of the service account to be registered. + */ + void register(String serviceAccountName, String serviceAccountRole); + + /** + * Retrieves the role of a specific service account. + * + * @param serviceAccountName The name of the service account. + * @return An Optional containing the role of the service account if it exists, otherwise an empty Optional. + */ + Optional getServiceAccountRole(String serviceAccountName); + + /** + * Retrieves the names of all service accounts. + * + * @return A set of service account names. If no service accounts are present, an empty set is returned. + */ + Set getServiceAccountNames(); +} diff --git a/repository/src/main/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistryImpl.java b/repository/src/main/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistryImpl.java new file mode 100644 index 0000000000..b8fe8377b7 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistryImpl.java @@ -0,0 +1,157 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.serviceaccount; + +import java.util.Locale; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.PropertyCheck; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * Processes the alfresco-global properties file and applies a naming convention to distinguish the service + * account's name and role. + *

+ * The naming convention adheres to the following format: + *

+ *

+ *   {@code
+ *     serviceaccount.role.=
+ *   }
+ * 
+ *

+ * Please note, any property with an invalid role value will be disregarded and the corresponding service account + * will not be registered. + *

+ * For instance, to register a service account named 'custom-app-sa' with the 'Editor' role (which allows it to + * update node properties), the following should be defined in the alfresco-global properties file: + *

    + *
  • serviceaccount.role.custom-app-sa=EDITOR_SERVICE_ACCOUNT
  • + *
  • or
  • + *
  • serviceaccount.role.custom-app-sa=ROLE_EDITOR_SERVICE_ACCOUNT
  • + *
+ * + * @author Jamal Kaabi-Mofrad + */ +public class ServiceAccountRegistryImpl implements ServiceAccountRegistry, InitializingBean +{ + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceAccountRegistryImpl.class); + + public static final String KEY_PREFIX = "serviceaccount.role."; + + private Properties globalProperties; + private final ConcurrentMap saRoleMap = new ConcurrentHashMap<>(); + + public void setGlobalProperties(Properties globalProperties) + { + this.globalProperties = globalProperties; + } + + @Override + public void register(String serviceAccountName, String serviceAccountRole) + { + saRoleMap.put(serviceAccountName, serviceAccountRole); + LOGGER.info("Service account '{}' is registered with the role '{}'.", serviceAccountName, serviceAccountRole); + } + + @Override + public Optional getServiceAccountRole(String serviceAccountName) + { + return Optional.ofNullable(saRoleMap.get(serviceAccountName)); + } + + @Override + public Set getServiceAccountNames() + { + return Set.copyOf(saRoleMap.keySet()); + } + + private void init() + { + globalProperties.stringPropertyNames() + .stream() + .filter(key -> key.startsWith(KEY_PREFIX)) + .forEach(key -> { + String name = key.substring(KEY_PREFIX.length()); + if (isNotValidProperty(key, name, "name")) + { + return; + } + String role = globalProperties.getProperty(key); + if (isNotValidProperty(key, role, "role")) + { + return; + } + // Ensure the role is in uppercase and has the prefix + role = role.toUpperCase(Locale.ENGLISH); + role = getRoleWithPrefix(role); + if (!PermissionService.SVC_AUTHORITIES_SET.contains(role)) + { + LOGGER.warn("Invalid service account role '{}'. The role is not recognized.", role); + return; + } + + // Register the service account name with the corresponding role. + register(name, role); + }); + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "globalProperties", globalProperties); + init(); + } + + private String getRoleWithPrefix(String saRole) + { + if (!saRole.startsWith(PermissionService.ROLE_PREFIX)) + { + saRole = PermissionService.ROLE_PREFIX + saRole; + } + return saRole; + } + + private boolean isNotValidProperty(String key, String value, String valueType) + { + if (StringUtils.isBlank(value)) + { + LOGGER.warn("Invalid service account {} defined in the property '{}'. The {} cannot be an empty string.", + valueType, key, valueType); + return true; + } + return false; + } +} diff --git a/repository/src/main/resources/alfresco/authority-services-context.xml b/repository/src/main/resources/alfresco/authority-services-context.xml index 68a3bf5bda..97b5eb16b4 100644 --- a/repository/src/main/resources/alfresco/authority-services-context.xml +++ b/repository/src/main/resources/alfresco/authority-services-context.xml @@ -143,4 +143,8 @@ + + + + diff --git a/repository/src/main/resources/alfresco/model/permissionDefinitions.xml b/repository/src/main/resources/alfresco/model/permissionDefinitions.xml index df0f9c0bc1..1678cc8cf0 100644 --- a/repository/src/main/resources/alfresco/model/permissionDefinitions.xml +++ b/repository/src/main/resources/alfresco/model/permissionDefinitions.xml @@ -13,462 +13,487 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/repository/src/main/resources/alfresco/public-services-security-context.xml b/repository/src/main/resources/alfresco/public-services-security-context.xml index 51e2bab8df..8f8769493e 100644 --- a/repository/src/main/resources/alfresco/public-services-security-context.xml +++ b/repository/src/main/resources/alfresco/public-services-security-context.xml @@ -102,6 +102,9 @@ + + + @@ -147,6 +150,23 @@ + + + + + + + + + + + + + + diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index 61dfdd4a01..8f5dc5b937 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -261,7 +261,8 @@ import org.junit.runners.Suite; org.alfresco.repo.event2.RepoEvent2UnitSuite.class, org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class, - org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class + org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class, + org.alfresco.repo.serviceaccount.ServiceAccountRegistryImplTest.class }) public class AllUnitTestsSuite { diff --git a/repository/src/test/java/org/alfresco/AppContext05TestSuite.java b/repository/src/test/java/org/alfresco/AppContext05TestSuite.java index 6e92701079..18fafbbe50 100644 --- a/repository/src/test/java/org/alfresco/AppContext05TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext05TestSuite.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2023 Alfresco Software Limited + * Copyright (C) 2005 - 2024 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -85,6 +85,7 @@ import org.junit.runners.Suite; org.alfresco.repo.model.ModelTestSuite.class, org.alfresco.repo.tenant.MultiTNodeServiceInterceptorTest.class, org.alfresco.repo.transfer.RepoTransferReceiverImplTest.class, + org.alfresco.repo.security.permissions.dynamic.ServiceAccountRoleTest.class }) public class AppContext05TestSuite { diff --git a/repository/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java b/repository/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java index 4382fa91cc..8454d1cb83 100644 --- a/repository/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java +++ b/repository/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java @@ -45,6 +45,7 @@ import org.alfresco.repo.security.authority.AuthorityServiceTest; import org.alfresco.repo.security.authority.DuplicateAuthorityTest; import org.alfresco.repo.security.authority.ExtendedPermissionServiceTest; import org.alfresco.repo.security.permissions.dynamic.LockOwnerDynamicAuthorityTest; +import org.alfresco.repo.security.permissions.dynamic.ServiceAccountRoleTest; import org.alfresco.repo.security.permissions.impl.AclDaoComponentTest; import org.alfresco.repo.security.permissions.impl.PermissionServiceTest; import org.alfresco.repo.security.permissions.impl.ReadPermissionTest; @@ -108,6 +109,7 @@ public class SecurityTestSuite extends TestSuite suite.addTest(new JUnit4TestAdapter(LocalAuthenticationServiceTest.class)); suite.addTest(new JUnit4TestAdapter(ResetPasswordServiceImplTest.class)); + suite.addTest(new JUnit4TestAdapter(ServiceAccountRoleTest.class)); return suite; } } diff --git a/repository/src/test/java/org/alfresco/repo/security/permissions/dynamic/ServiceAccountRoleTest.java b/repository/src/test/java/org/alfresco/repo/security/permissions/dynamic/ServiceAccountRoleTest.java new file mode 100644 index 0000000000..66a26b7ae2 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/security/permissions/dynamic/ServiceAccountRoleTest.java @@ -0,0 +1,355 @@ +/* + * #%L + * Alfresco Data model classes + * %% + * Copyright (C) 2005 - 2024 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.permissions.dynamic; + +import static java.lang.System.currentTimeMillis; +import static org.junit.Assert.assertEquals; + +import java.io.Serializable; +import java.util.Map; +import java.util.Properties; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.test.junitrules.AlfrescoPerson; +import org.alfresco.util.test.junitrules.ApplicationContextInit; +import org.alfresco.util.test.junitrules.TemporaryNodes; +import org.alfresco.util.test.junitrules.TemporarySites; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.springframework.context.ApplicationContext; + +/** + * This test class demonstrates the permissions of the service account authorities. + * The service account authorities are used to grant permissions to service accounts. + *

+ * The service account authorities are defined in the alfresco-global.properties file. + * Using the following naming convention: + *

+ *   {@code
+ *     serviceaccount.role.=
+ *   }
+ * 
+ * The service account roles that currently supported are: + *
    + *
  • {@link PermissionService#EDITOR_SVC_AUTHORITY}
  • + *
  • {@link PermissionService#COLLABORATOR_SVC_AUTHORITY}
  • + *
  • {@link PermissionService#ADMIN_SVC_AUTHORITY}
  • + *
+ * The test class relies on the following service accounts defined in the alfresco-global.properties file: + *
    + *
  • serviceaccount.role.test-editor-sa=ROLE_EDITOR_SERVICE_ACCOUNT
  • + *
  • serviceaccount.role.test-collaborator-sa=ROLE_COLLABORATOR_SERVICE_ACCOUNT
  • + *
  • serviceaccount.role.test-admin-sa=ROLE_ADMIN_SERVICE_ACCOUNT
  • + *
+ *

+ * Note: There is no need to use public services (i.e., beans that start with a capital letter, such as NodeService) + * to validate roles permissions. This is because the security enforcement of public services (i.e., ACL checks) ultimately relies on + * the {@code permissionService.hasPermission()} method. Therefore, we can directly use the {@code permissionService.hasPermission()} method. + * + * @author Jamal Kaabi-Mofrad + */ +// Ignore the PMD warning about having too many test methods in this class; it makes the tests easier to read and maintain. +@SuppressWarnings("PMD.TooManyMethods") +public class ServiceAccountRoleTest +{ + // Rule to initialise the default Alfresco spring configuration + private static final ApplicationContextInit APP_CONTEXT = new ApplicationContextInit(); + + // A rule to manage a test site + private static final TemporarySites TEST_SITES = new TemporarySites(APP_CONTEXT); + + // A rule to manage test nodes reused across all the test methods + private static final TemporaryNodes TEST_NODES = new TemporaryNodes(APP_CONTEXT); + + // Rules to create the users for the tests + private static final AlfrescoPerson NORMAL_USER = getAlfrescoPerson("john.doe" + currentTimeMillis()); + private static final AlfrescoPerson EDITOR_SA = getAlfrescoPerson("test-editor-sa"); + private static final AlfrescoPerson COLLABORATOR_SA = getAlfrescoPerson("test-collaborator-sa"); + private static final AlfrescoPerson ADMIN_SA = getAlfrescoPerson("test-admin-sa"); + + private static final String TEST_TEXT_FILE_NAME = "testTextFile.txt"; + + // Tie them together in a static Rule Chain + @ClassRule + public static final RuleChain STATIC_RULE_CHAIN = RuleChain.outerRule(APP_CONTEXT) + .around(TEST_SITES) + .around(TEST_NODES) + .around(NORMAL_USER) + .around(EDITOR_SA) + .around(COLLABORATOR_SA) + .around(ADMIN_SA); + + private static NodeService nodeService; + private static SiteService siteService; + private static PermissionService permissionService; + private static Properties globalProperties; + private static NodeRef testTextFile; + + @BeforeClass + public static void initStaticData() throws Exception + { + ApplicationContext context = APP_CONTEXT.getApplicationContext(); + nodeService = context.getBean("NodeService", NodeService.class); + siteService = context.getBean("SiteService", SiteService.class); + permissionService = context.getBean("permissionService", PermissionService.class); + globalProperties = context.getBean("global-properties", Properties.class); + + // Check that the service account roles are defined in the global properties before starting the tests. + serviceAccountsShouldExistInGlobalProperties(); + + // Create a test site + SiteInfo testSite = createTestSite(); + // Create a test text file in the test site + createTestFile(testSite); + + // Clear the current security context to avoid any issues with the test setup + AuthenticationUtil.clearCurrentSecurityContext(); + } + + private static AlfrescoPerson getAlfrescoPerson(String username) + { + return new AlfrescoPerson(APP_CONTEXT, username); + } + + private static SiteInfo createTestSite() + { + // Create a private test site to make sure no other non-members or + // non-site-admins can access the test site. + return TEST_SITES.createSite("sitePreset", "saTestSite" + currentTimeMillis(), "SA Test Site", + "sa test site desc", SiteVisibility.PRIVATE, + AuthenticationUtil.getAdminUserName()); + } + + private static void createTestFile(SiteInfo testSite) + { + // Create a test text file in the test site as the admin user + AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser(); + final NodeRef docLib = siteService.getContainer(testSite.getShortName(), SiteService.DOCUMENT_LIBRARY); + + final NodeRef testFolder = TEST_NODES.createFolder(docLib, "testFolder", AuthenticationUtil.getAdminUserName()); + testTextFile = TEST_NODES.createNodeWithTextContent(testFolder, TEST_TEXT_FILE_NAME, ContentModel.TYPE_CONTENT, + AuthenticationUtil.getAdminUserName(), + "The quick brown fox jumps over the lazy dog."); + + Map props = Map.of(ContentModel.PROP_NAME, TEST_TEXT_FILE_NAME, + ContentModel.PROP_DESCRIPTION, "Desc added by Admin."); + nodeService.setProperties(testTextFile, props); + } + + private static void serviceAccountsShouldExistInGlobalProperties() + { + assertServiceAccountIsDefined(PermissionService.EDITOR_SVC_AUTHORITY, EDITOR_SA.getUsername()); + assertServiceAccountIsDefined(PermissionService.COLLABORATOR_SVC_AUTHORITY, COLLABORATOR_SA.getUsername()); + assertServiceAccountIsDefined(PermissionService.ADMIN_SVC_AUTHORITY, ADMIN_SA.getUsername()); + } + + private static void assertServiceAccountIsDefined(String expectedRole, String username) + { + assertEquals(expectedRole, globalProperties.getProperty("serviceaccount.role." + username)); + } + + @After + public void tearDown() throws Exception + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + + @Test + public void normalUserReadAccessShouldBeDenied() + { + assertAccessDenied(NORMAL_USER, PermissionService.READ); + } + + @Test + public void editorSaReadAccessShouldBeAllowed() + { + assertAccessAllowed(EDITOR_SA, PermissionService.READ); + } + + @Test + public void collaboratorSaReadAccessShouldBeAllowed() + { + assertAccessAllowed(COLLABORATOR_SA, PermissionService.READ); + } + + @Test + public void adminSaReadAccessShouldBeAllowed() + { + assertAccessAllowed(ADMIN_SA, PermissionService.READ); + } + + @Test + public void normalUserWriteAccessShouldBeDenied() + { + assertAccessDenied(NORMAL_USER, PermissionService.WRITE); + } + + @Test + public void editorSaWriteAccessShouldBeAllowed() + { + assertAccessAllowed(EDITOR_SA, PermissionService.WRITE); + } + + @Test + public void collaboratorSaWriteAccessShouldBeAllowed() + { + assertAccessAllowed(COLLABORATOR_SA, PermissionService.WRITE); + } + + @Test + public void adminSaWriteAccessShouldBeAllowed() + { + assertAccessAllowed(ADMIN_SA, PermissionService.WRITE); + } + + @Test + public void normalUserAddChildrenAccessShouldBeDenied() + { + assertAccessDenied(NORMAL_USER, PermissionService.ADD_CHILDREN); + } + + @Test + public void editorSaAddChildrenAccessShouldBeDenied() + { + assertAccessDenied(EDITOR_SA, PermissionService.ADD_CHILDREN); + } + + @Test + public void collaboratorSaAddChildrenAccessShouldBeAllowed() + { + assertAccessAllowed(COLLABORATOR_SA, PermissionService.ADD_CHILDREN); + } + + @Test + public void adminSaAddChildrenAccessShouldBeAllowed() + { + assertAccessAllowed(ADMIN_SA, PermissionService.ADD_CHILDREN); + } + + @Test + public void normalUserDeleteAccessShouldBeDenied() + { + assertAccessDenied(NORMAL_USER, PermissionService.DELETE); + } + + @Test + public void editorSaDeleteAccessShouldBeDenied() + { + assertAccessDenied(EDITOR_SA, PermissionService.DELETE); + } + + @Test + public void collaboratorSaDeleteAccessShouldBeDenied() + { + assertAccessDenied(COLLABORATOR_SA, PermissionService.DELETE); + } + + @Test + public void adminSaDeleteAccessShouldBeAllowed() + { + assertAccessAllowed(ADMIN_SA, PermissionService.DELETE); + } + + @Test + public void normalUserAssociationAccessShouldBeDenied() + { + assertAccessDenied(NORMAL_USER, PermissionService.READ_ASSOCIATIONS); + assertAccessDenied(NORMAL_USER, PermissionService.CREATE_ASSOCIATIONS); + assertAccessDenied(NORMAL_USER, PermissionService.DELETE_ASSOCIATIONS); + } + + @Test + public void editorSaAssociationAccessShouldBeDenied() + { + assertAccessDenied(EDITOR_SA, PermissionService.READ_ASSOCIATIONS); + assertAccessDenied(EDITOR_SA, PermissionService.CREATE_ASSOCIATIONS); + assertAccessDenied(EDITOR_SA, PermissionService.DELETE_ASSOCIATIONS); + } + + @Test + public void collaboratorSaAssociationAccessShouldBeDenied() + { + assertAccessDenied(COLLABORATOR_SA, PermissionService.READ_ASSOCIATIONS); + assertAccessDenied(COLLABORATOR_SA, PermissionService.CREATE_ASSOCIATIONS); + assertAccessDenied(COLLABORATOR_SA, PermissionService.DELETE_ASSOCIATIONS); + } + + @Test + public void adminSaAssociationAccessShouldBeAllowed() + { + assertAccessAllowed(ADMIN_SA, PermissionService.READ_ASSOCIATIONS); + assertAccessAllowed(ADMIN_SA, PermissionService.CREATE_ASSOCIATIONS); + assertAccessAllowed(ADMIN_SA, PermissionService.DELETE_ASSOCIATIONS); + } + + @Test + public void normalUserReadPermissionsAccessShouldBeDenied() + { + assertAccessDenied(NORMAL_USER, PermissionService.READ_PERMISSIONS); + } + + @Test + public void editorSaReadPermissionsAccessShouldBeDenied() + { + assertAccessDenied(EDITOR_SA, PermissionService.READ_PERMISSIONS); + } + + @Test + public void collaboratorSaReadPermissionsAccessShouldBeDenied() + { + assertAccessDenied(COLLABORATOR_SA, PermissionService.READ_PERMISSIONS); + } + + @Test + public void adminSaReadPermissionsAccessShouldBeAllowed() + { + assertAccessAllowed(ADMIN_SA, PermissionService.READ_PERMISSIONS); + } + + private static void assertAccessDenied(AlfrescoPerson user, String permission) + { + AuthenticationUtil.setFullyAuthenticatedUser(user.getUsername()); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(testTextFile, permission)); + } + + private static void assertAccessAllowed(AlfrescoPerson user, String permission) + { + AuthenticationUtil.setFullyAuthenticatedUser(user.getUsername()); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(testTextFile, permission)); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java b/repository/src/test/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java index 0bcbe57dab..b3bd0d94c2 100644 --- a/repository/src/test/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/permissions/impl/AbstractPermissionTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2023 Alfresco Software Limited + * Copyright (C) 2005 - 2024 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -66,6 +66,8 @@ import org.springframework.context.ApplicationContext; public abstract class AbstractPermissionTest extends TestCase { + public static final int NUMBER_OF_GLOBAL_PERMISSIONS = 8; + protected static final String USER2_LEMUR = "lemur"; protected static final String USER1_ANDY = "andy"; diff --git a/repository/src/test/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java b/repository/src/test/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java index 2400c01fc4..657bcb60bc 100644 --- a/repository/src/test/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/permissions/impl/PermissionServiceTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2024 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -73,6 +73,10 @@ import org.junit.runners.MethodSorters; @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class PermissionServiceTest extends AbstractPermissionTest { + // The number of permissions in the system. + // See permissionDefinitions.xml and PermissionService interface + private static final int NUM_OF_PERMISSIONS = 39; + private SimplePermissionEntry denyAndyAll; private SimplePermissionEntry allowAndyAll; @@ -1764,7 +1768,7 @@ public class PermissionServiceTest extends AbstractPermissionTest public void testGetSettablePermissionsForType() { Set answer = permissionService.getSettablePermissions(QName.createQName("sys", "base", namespacePrefixResolver)); - assertEquals(36, answer.size()); + assertEquals(NUM_OF_PERMISSIONS, answer.size()); answer = permissionService.getSettablePermissions(QName.createQName("cm", "ownable", namespacePrefixResolver)); assertEquals(0, answer.size()); @@ -1784,15 +1788,15 @@ public class PermissionServiceTest extends AbstractPermissionTest QName ownable = QName.createQName("cm", "ownable", namespacePrefixResolver); Set answer = permissionService.getSettablePermissions(rootNodeRef); - assertEquals(36, answer.size()); + assertEquals(NUM_OF_PERMISSIONS, answer.size()); nodeService.addAspect(rootNodeRef, ownable, null); answer = permissionService.getSettablePermissions(rootNodeRef); - assertEquals(36, answer.size()); + assertEquals(NUM_OF_PERMISSIONS, answer.size()); nodeService.removeAspect(rootNodeRef, ownable); answer = permissionService.getSettablePermissions(rootNodeRef); - assertEquals(36, answer.size()); + assertEquals(NUM_OF_PERMISSIONS, answer.size()); } @@ -1816,7 +1820,7 @@ public class PermissionServiceTest extends AbstractPermissionTest { runAs("andy"); - assertEquals(36, permissionService.getPermissions(rootNodeRef).size()); + assertEquals(NUM_OF_PERMISSIONS, permissionService.getPermissions(rootNodeRef).size()); assertEquals(0, countGranted(permissionService.getPermissions(rootNodeRef))); assertEquals(0, permissionService.getAllSetPermissions(rootNodeRef).size()); @@ -1828,7 +1832,7 @@ public class PermissionServiceTest extends AbstractPermissionTest assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); runAs("andy"); - assertEquals(36, permissionService.getPermissions(rootNodeRef).size()); + assertEquals(NUM_OF_PERMISSIONS, permissionService.getPermissions(rootNodeRef).size()); assertEquals(2, countGranted(permissionService.getPermissions(rootNodeRef))); assertTrue(permissionService.hasPermission(rootNodeRef, getPermission(PermissionService.READ_PROPERTIES)) == AccessStatus.ALLOWED); @@ -1933,7 +1937,7 @@ public class PermissionServiceTest extends AbstractPermissionTest permissionService.setPermission(allowAndyRead); runAs("andy"); - assertEquals(36, permissionService.getPermissions(rootNodeRef).size()); + assertEquals(NUM_OF_PERMISSIONS, permissionService.getPermissions(rootNodeRef).size()); assertEquals(7, countGranted(permissionService.getPermissions(rootNodeRef))); assertEquals(1, permissionService.getAllSetPermissions(rootNodeRef).size()); diff --git a/repository/src/test/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java b/repository/src/test/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java index ff4f4ce40e..bca9bb1c80 100644 --- a/repository/src/test/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/permissions/impl/model/PermissionModelTest.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco 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 Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.security.permissions.impl.model; import java.util.Collections; @@ -94,7 +94,7 @@ public class PermissionModelTest extends AbstractPermissionTest Set grantees = permissionModelDAO.getGranteePermissions(SimplePermissionReference.getPermissionReference(QName.createQName("cm", "cmobject", namespacePrefixResolver), "Coordinator")); - assertEquals(69, grantees.size()); + assertEquals(72, grantees.size()); } public void testIncludePermissionGroups6() @@ -109,17 +109,17 @@ public class PermissionModelTest extends AbstractPermissionTest { Set granters = permissionModelDAO.getGrantingPermissions(SimplePermissionReference.getPermissionReference(QName.createQName("sys", "base", namespacePrefixResolver), "ReadProperties")); - assertEquals(14, granters.size()); + assertEquals(17, granters.size()); granters = permissionModelDAO.getGrantingPermissions(SimplePermissionReference.getPermissionReference(QName.createQName("sys", "base", namespacePrefixResolver), "_ReadProperties")); - assertEquals(15, granters.size()); + assertEquals(18, granters.size()); } public void testGlobalPermissions() { Set globalPermissions = permissionModelDAO.getGlobalPermissionEntries(); - assertEquals(5, globalPermissions.size()); + assertEquals(NUMBER_OF_GLOBAL_PERMISSIONS, globalPermissions.size()); } public void testRequiredPermissions() diff --git a/repository/src/test/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistryImplTest.java b/repository/src/test/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistryImplTest.java new file mode 100644 index 0000000000..1afcdb7565 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/serviceaccount/ServiceAccountRegistryImplTest.java @@ -0,0 +1,157 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.serviceaccount; + +import static org.alfresco.service.cmr.security.PermissionService.ADMIN_SVC_AUTHORITY; +import static org.alfresco.service.cmr.security.PermissionService.COLLABORATOR_SVC_AUTHORITY; +import static org.alfresco.service.cmr.security.PermissionService.EDITOR_SVC_AUTHORITY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Optional; +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for the {@link ServiceAccountRegistryImpl} class. + * + * @author Jamal Kaabi-Mofrad + */ +public class ServiceAccountRegistryImplTest +{ + private ServiceAccountRegistryImpl serviceAccountService; + private Properties globalProperties; + + @Before + public void setUp() throws Exception + { + globalProperties = new Properties(); + globalProperties.put("system.test.property", "test-prop"); + globalProperties.put("repo.events.test.someKey", "test-event-value"); + + serviceAccountService = new ServiceAccountRegistryImpl(); + serviceAccountService.setGlobalProperties(globalProperties); + serviceAccountService.afterPropertiesSet(); + } + + @Test + public void testNoDefinedServiceAccount() + { + Optional nonExistentSa = serviceAccountService.getServiceAccountRole("nonExistentServiceAccount"); + assertTrue(nonExistentSa.isEmpty()); + assertTrue(serviceAccountService.getServiceAccountNames().isEmpty()); + } + + @Test + public void testInvalidServiceAccountName() + { + globalProperties.put("serviceaccount.role. ", ADMIN_SVC_AUTHORITY); + assertTrue("Invalid service account name.", serviceAccountService.getServiceAccountNames().isEmpty()); + } + + @Test + public void testInvalidServiceAccountRole() throws Exception + { + globalProperties.put("serviceaccount.role.testServiceAccount", ""); + serviceAccountService.afterPropertiesSet(); + + Optional testServiceAccount = serviceAccountService.getServiceAccountRole("testServiceAccount"); + assertTrue("Invalid service account role.", testServiceAccount.isEmpty()); + assertTrue(serviceAccountService.getServiceAccountNames().isEmpty()); + } + + @Test + public void testNotSupportedServiceAccountRole() throws Exception + { + globalProperties.put("serviceaccount.role.testServiceAccount", "testRole"); + serviceAccountService.afterPropertiesSet(); + + Optional testServiceAccount = serviceAccountService.getServiceAccountRole("testServiceAccount"); + assertTrue("Not supported service account role.", testServiceAccount.isEmpty()); + assertTrue(serviceAccountService.getServiceAccountNames().isEmpty()); + } + + @Test + public void testValidServiceAccount() throws Exception + { + globalProperties.put("serviceaccount.role.testServiceAccount", ADMIN_SVC_AUTHORITY); + serviceAccountService.afterPropertiesSet(); + + Optional testServiceAccount = serviceAccountService.getServiceAccountRole("testServiceAccount"); + assertFalse("The service account role is not empty.", testServiceAccount.isEmpty()); + assertEquals(ADMIN_SVC_AUTHORITY, testServiceAccount.get()); + assertEquals(1, serviceAccountService.getServiceAccountNames().size()); + } + + @Test + public void testManyServiceAccounts() throws Exception + { + globalProperties.put("serviceaccount.role.testEditorSA", EDITOR_SVC_AUTHORITY); + globalProperties.put("serviceaccount.role.testCollaboratorSA", COLLABORATOR_SVC_AUTHORITY); + globalProperties.put("serviceaccount.role.testAdminSA", ADMIN_SVC_AUTHORITY); + serviceAccountService.afterPropertiesSet(); + + assertEquals(3, serviceAccountService.getServiceAccountNames().size()); + + Optional editorSA = serviceAccountService.getServiceAccountRole("testEditorSA"); + assertFalse("The service account role is not empty.", editorSA.isEmpty()); + assertEquals(EDITOR_SVC_AUTHORITY, editorSA.get()); + + Optional collaboratorSA = serviceAccountService.getServiceAccountRole("testCollaboratorSA"); + assertFalse("The service account role is not empty.", collaboratorSA.isEmpty()); + assertEquals(COLLABORATOR_SVC_AUTHORITY, collaboratorSA.get()); + + Optional adminSA = serviceAccountService.getServiceAccountRole("testAdminSA"); + assertFalse("The service account role is not empty.", adminSA.isEmpty()); + assertEquals(ADMIN_SVC_AUTHORITY, adminSA.get()); + } + + @Test + public void testValidServiceAccountRoleValues() throws Exception + { + globalProperties.put("serviceaccount.role.testEditorSA", "EDITOR_SERVICE_ACCOUNT"); + globalProperties.put("serviceaccount.role.testCollaboratorSA", "COLLABORATOR_SERVICE_ACCOUNT"); + globalProperties.put("serviceaccount.role.testAdminSA", "ADMIN_SERVICE_ACCOUNT"); + serviceAccountService.afterPropertiesSet(); + + assertEquals(3, serviceAccountService.getServiceAccountNames().size()); + + Optional editorSA = serviceAccountService.getServiceAccountRole("testEditorSA"); + assertFalse("The service account role is not empty.", editorSA.isEmpty()); + assertEquals(EDITOR_SVC_AUTHORITY, editorSA.get()); + + Optional collaboratorSA = serviceAccountService.getServiceAccountRole("testCollaboratorSA"); + assertFalse("The service account role is not empty.", collaboratorSA.isEmpty()); + assertEquals(COLLABORATOR_SVC_AUTHORITY, collaboratorSA.get()); + + Optional adminSA = serviceAccountService.getServiceAccountRole("testAdminSA"); + assertFalse("The service account role is not empty.", adminSA.isEmpty()); + assertEquals(ADMIN_SVC_AUTHORITY, adminSA.get()); + } +} diff --git a/repository/src/test/resources/alfresco-global.properties b/repository/src/test/resources/alfresco-global.properties index bdbec71ca8..7aa761cf7b 100644 --- a/repository/src/test/resources/alfresco-global.properties +++ b/repository/src/test/resources/alfresco-global.properties @@ -61,3 +61,8 @@ encryption.keystore.backup.type=JCEKS # For CI override the default hashing algorithm for password storage to save build time. system.preferred.password.encoding=sha256 + +# Test service accounts +serviceaccount.role.test-editor-sa=ROLE_EDITOR_SERVICE_ACCOUNT +serviceaccount.role.test-collaborator-sa=ROLE_COLLABORATOR_SERVICE_ACCOUNT +serviceaccount.role.test-admin-sa=ROLE_ADMIN_SERVICE_ACCOUNT