From 62a7f79f517d397b7560dbb8c6a0141885760fe4 Mon Sep 17 00:00:00 2001 From: Viktor Scharf Date: Fri, 8 Aug 2025 10:16:57 +0200 Subject: [PATCH] multiTenancyTests (#1313) * multiTenancyTests * fix linter issues * fix after review --- .woodpecker.star | 87 +++++++++++++++++-- tests/acceptance/TestHelpers/GraphHelper.php | 12 +-- tests/acceptance/TestHelpers/OcHelper.php | 7 ++ tests/acceptance/bootstrap/SpacesContext.php | 2 +- tests/acceptance/config/behat.yml | 7 ++ .../features/apiTenancy/mutltiTenancy.feature | 77 ++++++++++++++++ tests/config/woodpecker/ldap/10_base.ldif | 25 ++++++ tests/config/woodpecker/ldap/20_users.ldif | 74 ++++++++++++++++ tests/config/woodpecker/ldap/30_groups.ldif | 13 +++ .../ldap/docker-entrypoint-override.sh | 42 +++++++++ 10 files changed, 334 insertions(+), 12 deletions(-) create mode 100644 tests/acceptance/features/apiTenancy/mutltiTenancy.feature create mode 100644 tests/config/woodpecker/ldap/10_base.ldif create mode 100644 tests/config/woodpecker/ldap/20_users.ldif create mode 100644 tests/config/woodpecker/ldap/30_groups.ldif create mode 100644 tests/config/woodpecker/ldap/docker-entrypoint-override.sh diff --git a/.woodpecker.star b/.woodpecker.star index 5008caff1..d14770d8f 100644 --- a/.woodpecker.star +++ b/.woodpecker.star @@ -33,6 +33,7 @@ PLUGINS_S3_CACHE = "plugins/s3-cache:1" PLUGINS_SLACK = "plugins/slack:1" REDIS = "redis:6-alpine" READY_RELEASE_GO = "woodpeckerci/plugin-ready-release-go:latest" +OPENLDAP = "bitnami/openldap:2.6" DEFAULT_PHP_VERSION = "8.2" DEFAULT_NODEJS_VERSION = "20" @@ -113,7 +114,7 @@ config = { "skip": False, "withRemotePhp": [True], "emailNeeded": True, - "extraEnvironment": { + "extraTestEnvironment": { "EMAIL_HOST": "email", "EMAIL_PORT": "9000", }, @@ -207,7 +208,7 @@ config = { "skip": False, "withRemotePhp": [True], "emailNeeded": True, - "extraEnvironment": { + "extraTestEnvironment": { "EMAIL_HOST": "email", "EMAIL_PORT": "9000", }, @@ -250,7 +251,7 @@ config = { "withRemotePhp": [True], "federationServer": True, "emailNeeded": True, - "extraEnvironment": { + "extraTestEnvironment": { "EMAIL_HOST": "email", "EMAIL_PORT": "9000", }, @@ -300,6 +301,36 @@ config = { "STORAGE_USERS_DRIVER": "decomposed", }, }, + "multiTenancy": { + "suites": [ + "apiTenancy", + ], + "skip": False, + "withRemotePhp": [True], + "ldapNeeded": True, + "extraTestEnvironment": { + "USE_PREPARED_LDAP_USERS": True, + }, + "extraServerEnvironment": { + "OC_LDAP_USER_SCHEMA_TENANT_ID": "departmentNumber", + "OC_LDAP_URI": "ldaps://ldap-server:1636", + "OC_LDAP_INSECURE": True, + "OC_LDAP_BIND_DN": "cn=admin,dc=opencloud,dc=eu", + "OC_LDAP_BIND_PASSWORD": "admin", + "OC_LDAP_GROUP_BASE_DN": "ou=groups,dc=opencloud,dc=eu", + "OC_LDAP_GROUP_SCHEMA_ID": "entryUUID", + "OC_LDAP_USER_BASE_DN": "ou=users,dc=opencloud,dc=eu", + "OC_LDAP_USER_FILTER": "(objectclass=inetOrgPerson)", + "OC_LDAP_USER_SCHEMA_ID": "entryUUID", + "OC_LDAP_DISABLE_USER_MECHANISM": "none", + "GRAPH_LDAP_SERVER_UUID": True, + "GRAPH_LDAP_GROUP_CREATE_BASE_DN": "ou=custom,ou=groups,dc=opencloud,dc=eu", + "GRAPH_LDAP_REFINT_ENABLED": True, + "FRONTEND_READONLY_USER_ATTRIBUTES": "user.onPremisesSamAccountName,user.displayName,user.mail,user.passwordProfile,user.accountEnabled,user.appRoleAssignments", + "OC_LDAP_SERVER_WRITE_ENABLED": False, + "OC_EXCLUDE_RUN_SERVICES": "idm", + }, + }, }, "apiTests": { "numberOfParts": 7, @@ -913,7 +944,7 @@ def localApiTestPipeline(ctx): defaults = { "suites": {}, "skip": False, - "extraEnvironment": {}, + "extraTestEnvironment": {}, "extraServerEnvironment": {}, "storages": storages, "accounts_hash_difficulty": 4, @@ -924,6 +955,7 @@ def localApiTestPipeline(ctx): "collaborationServiceNeeded": False, "extraCollaborationEnvironment": {}, "withRemotePhp": with_remote_php, + "ldapNeeded": False, } if "localApiTests" in config: @@ -941,11 +973,13 @@ def localApiTestPipeline(ctx): (waitForServices("online-offices", ["collabora:9980", "onlyoffice:443", "fakeoffice:8080"]) if params["collaborationServiceNeeded"] else []) + (waitForClamavService() if params["antivirusNeeded"] else []) + (waitForEmailService() if params["emailNeeded"] else []) + + (ldapService() if params["ldapNeeded"] else []) + + (waitForLdapService() if params["ldapNeeded"] else []) + opencloudServer(storage, params["accounts_hash_difficulty"], extra_server_environment = params["extraServerEnvironment"], with_wrapper = True, tika_enabled = params["tikaNeeded"]) + (opencloudServer(storage, params["accounts_hash_difficulty"], deploy_type = "federation", extra_server_environment = params["extraServerEnvironment"]) if params["federationServer"] else []) + ((wopiCollaborationService("fakeoffice") + wopiCollaborationService("collabora") + wopiCollaborationService("onlyoffice")) if params["collaborationServiceNeeded"] else []) + (openCloudHealthCheck("wopi", ["wopi-collabora:9304", "wopi-onlyoffice:9304", "wopi-fakeoffice:9304"]) if params["collaborationServiceNeeded"] else []) + - localApiTests(name, params["suites"], storage, params["extraEnvironment"], run_with_remote_php) + + localApiTests(name, params["suites"], storage, params["extraTestEnvironment"], run_with_remote_php) + logRequests(), "services": (emailService() if params["emailNeeded"] else []) + (clamavService() if params["antivirusNeeded"] else []) + @@ -2823,6 +2857,49 @@ def waitForClamavService(): ], }] +def ldapService(): + return [ + { + "name": "ldap-server", + "image": OPENLDAP, + "detach": True, + "environment": { + "BITNAMI_DEBUG": "true", + "LDAP_TLS_VERIFY_CLIENT": "never", + "LDAP_ENABLE_TLS": "yes", + "LDAP_TLS_CA_FILE": "/opt/bitnami/openldap/share/openldap.crt", + "LDAP_TLS_CERT_FILE": "/opt/bitnami/openldap/share/openldap.crt", + "LDAP_TLS_KEY_FILE": "/opt/bitnami/openldap/share/openldap.key", + "LDAP_ROOT": "dc=opencloud,dc=eu", + "LDAP_ADMIN_PASSWORD": "admin", + }, + "commands": [ + "mkdir -p /opt/bitnami/openldap/share", + "mkdir -p /tmp/custom-scripts", + "mkdir -p /tmp/ldif-files", + "cp tests/config/woodpecker/ldap/*.ldif /tmp/ldif-files/", + "cp tests/config/woodpecker/ldap/docker-entrypoint-override.sh /tmp/custom-scripts/", + "chmod +x /tmp/custom-scripts/docker-entrypoint-override.sh", + "ls -la /tmp/ldif-files/", + "/tmp/custom-scripts/docker-entrypoint-override.sh /opt/bitnami/scripts/openldap/run.sh", + ], + "backend_options": { + "docker": { + "user": "0:0", + }, + }, + }, + ] + +def waitForLdapService(): + return [{ + "name": "wait-for-ldap", + "image": OC_CI_WAIT_FOR, + "commands": [ + "wait-for -it ldap-server:1636 -t 600", + ], + }] + def fakeOffice(): return [ { diff --git a/tests/acceptance/TestHelpers/GraphHelper.php b/tests/acceptance/TestHelpers/GraphHelper.php index 795d0d9f1..95268e93a 100644 --- a/tests/acceptance/TestHelpers/GraphHelper.php +++ b/tests/acceptance/TestHelpers/GraphHelper.php @@ -344,8 +344,8 @@ class GraphHelper { /** * @param string $baseUrl * @param string $xRequestId - * @param string $adminUser - * @param string $adminPassword + * @param string $user + * @param string $password * @param string $searchTerm * * @return ResponseInterface @@ -353,16 +353,16 @@ class GraphHelper { public static function searchUser( string $baseUrl, string $xRequestId, - string $adminUser, - string $adminPassword, + string $user, + string $password, string $searchTerm ): ResponseInterface { $url = self::getFullUrl($baseUrl, "users?\$search=$searchTerm"); return HttpRequestHelper::get( $url, $xRequestId, - $adminUser, - $adminPassword, + $user, + $password, self::getRequestHeaders() ); } diff --git a/tests/acceptance/TestHelpers/OcHelper.php b/tests/acceptance/TestHelpers/OcHelper.php index 66a52324c..180876951 100644 --- a/tests/acceptance/TestHelpers/OcHelper.php +++ b/tests/acceptance/TestHelpers/OcHelper.php @@ -89,6 +89,13 @@ class OcHelper { return (\getenv("TEST_REVA") === "true"); } + /** + * @return bool + */ + public static function isUsingPreparedLdapUsers(): bool { + return (\getenv("USE_PREPARED_LDAP_USERS") === "true"); + } + /** * @return bool|string false if no command given or the command as string */ diff --git a/tests/acceptance/bootstrap/SpacesContext.php b/tests/acceptance/bootstrap/SpacesContext.php index d395867e4..54f043adc 100644 --- a/tests/acceptance/bootstrap/SpacesContext.php +++ b/tests/acceptance/bootstrap/SpacesContext.php @@ -450,7 +450,7 @@ class SpacesContext implements Context { * @throws Exception|GuzzleException */ public function cleanDataAfterTests(): void { - if (OcHelper::isTestingOnReva()) { + if (OcHelper::isTestingOnReva() || OcHelper::isUsingPreparedLdapUsers()) { return; } $this->deleteAllProjectSpaces(); diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 4fb3ca79a..7894cc188 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -442,6 +442,13 @@ default: - AuthAppContext: - CliContext: - OcConfigContext: + + apiTenancy: + paths: + - "%paths.base%/../features/apiTenancy" + context: *common_ldap_suite_context + contexts: + - FeatureContext: *common_feature_context_params cliCommands: paths: diff --git a/tests/acceptance/features/apiTenancy/mutltiTenancy.feature b/tests/acceptance/features/apiTenancy/mutltiTenancy.feature new file mode 100644 index 000000000..e989a0789 --- /dev/null +++ b/tests/acceptance/features/apiTenancy/mutltiTenancy.feature @@ -0,0 +1,77 @@ +Feature: Multi-tenancy + I want to make sure that users from different tenants are isolated from each other, + so that each tenant's data and users remain private and secure. + + Note: + All users are managed via LDAP and are assumed to exist. + Tests will use existing users without creating or deleting them. + + Prepared LDAP users: + | user | tenant | group | + |-------|----------|----------------------| + | alice | tenant-1 | new-features-lovers | + | brian | tenant-1 | - | + | carol | tenant-2 | - | + | david | tenant-2 | release-lover | + + + Scenario: users from the same tenant can see each other + When user "Brian" searches for user "ali" using Graph API + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": [ + "displayName", + "id", + "onPremisesSamAccountName", + "userType" + ], + "properties": { + "displayName": { + "const": "Alice Hansen" + }, + "id": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + }, + "onPremisesSamAccountName": { + "const": "" + }, + "userType": { + "const": "Member" + } + } + } + } + } + } + """ + + + Scenario: users from different tenants cannot see each other + When user "David" searches for user "brian" using Graph API + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": "array", + "minItems": 0, + "maxItems": 0 + } + } + } + """ diff --git a/tests/config/woodpecker/ldap/10_base.ldif b/tests/config/woodpecker/ldap/10_base.ldif new file mode 100644 index 000000000..874f77ae4 --- /dev/null +++ b/tests/config/woodpecker/ldap/10_base.ldif @@ -0,0 +1,25 @@ +dn: dc=opencloud,dc=eu +objectClass: organization +objectClass: dcObject +dc: opencloud +o: openCloud + +dn: ou=users,dc=opencloud,dc=eu +objectClass: organizationalUnit +ou: users + +dn: cn=admin,dc=opencloud,dc=eu +objectClass: inetOrgPerson +objectClass: person +cn: admin +sn: admin +uid: ldapadmin +departmentNumber: {{ .TenantID }} + +dn: ou=groups,dc=opencloud,dc=eu +objectClass: organizationalUnit +ou: groups + +dn: ou=custom,ou=groups,dc=opencloud,dc=eu +objectClass: organizationalUnit +ou: custom diff --git a/tests/config/woodpecker/ldap/20_users.ldif b/tests/config/woodpecker/ldap/20_users.ldif new file mode 100644 index 000000000..82791a4b1 --- /dev/null +++ b/tests/config/woodpecker/ldap/20_users.ldif @@ -0,0 +1,74 @@ +dn: uid=alice,ou=users,dc=opencloud,dc=eu +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: alice +givenName: Alice +sn: Hansen +cn: alice +displayName: Alice Hansen +description: Senior DevOps engineer responsible for cloud infrastructure automation. +mail: alice@example.org +departmentNumber: tenant-1 +userPassword:: e1NTSEF9eGY5OXlPOXVxSVJ5eTFxdFhQYVYxTnd6WE4wUWRReVU= + +dn: uid=brian,ou=users,dc=opencloud,dc=eu +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: brian +givenName: Brian +sn: Murphy +cn: brian +displayName: Brian Murphy +description: IT support specialist focused on end-user systems and service desk operations. +mail: brian@example.org +departmentNumber: tenant-1 +userPassword:: e1NTSEF9Y0xpdnEzdUxzNzB3SjU0R1dmR0EybndxbUZoRmNoOXQ= + +dn: uid=carol,ou=users,dc=opencloud,dc=eu +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: carol +givenName: Carol +sn: King +cn: carol +displayName: Carol King +description: Project manager leading enterprise IT solutions and digital transformation projects. +mail: carol@example.org +departmentNumber: tenant-2 +userPassword:: e1NTSEF9bWFiT2FyNEE4UWlJUm1Pb2JhNm1pYm1QMjQraDkzSEw= + +dn: uid=david,ou=users,dc=opencloud,dc=eu +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: david +givenName: David +sn: Lopez +cn: david +displayName: David Lopez +description: Systems architect working on scalable backend services and platform reliability. +mail: david@example.org +departmentNumber: tenant-2 +userPassword:: e1NTSEF9MEh2a3J0UTVONmZNSUhyL1NlWVQvclBYTjg0bi9SYlc= + +dn: uid=admin,ou=users,dc=opencloud,dc=eu +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: admin +givenName: Admin +sn: Admin +cn: Admin +displayName: Admin +description: System administrator +mail: admin@example.org +departmentNumber: tenant-1 +userPassword:: e1NTSEF9bFU2dDRHSC9Cb28wV2lnM1A0SVAzQTIyWE9aL2pCa1M= diff --git a/tests/config/woodpecker/ldap/30_groups.ldif b/tests/config/woodpecker/ldap/30_groups.ldif new file mode 100644 index 000000000..e12b32471 --- /dev/null +++ b/tests/config/woodpecker/ldap/30_groups.ldif @@ -0,0 +1,13 @@ +dn: cn=new-features-lovers,ou=groups,dc=opencloud,dc=eu +objectClass: groupOfNames +objectClass: top +cn: new-features-lovers +description: New features lovers +member: uid=alice,ou=users,dc=opencloud,dc=eu + +dn: cn=release-lovers,ou=groups,dc=opencloud,dc=eu +objectClass: groupOfNames +objectClass: top +cn: release-lovers +description: Release lovers +member: uid=david,ou=users,dc=opencloud,dc=eu diff --git a/tests/config/woodpecker/ldap/docker-entrypoint-override.sh b/tests/config/woodpecker/ldap/docker-entrypoint-override.sh new file mode 100644 index 000000000..3a971c3e1 --- /dev/null +++ b/tests/config/woodpecker/ldap/docker-entrypoint-override.sh @@ -0,0 +1,42 @@ +#!/bin/bash +printenv + +if [ ! -f /opt/bitnami/openldap/share/openldap.key ] +then + openssl req -x509 -newkey rsa:4096 -keyout /opt/bitnami/openldap/share/openldap.key -out /opt/bitnami/openldap/share/openldap.crt -sha256 -days 365 -batch -nodes +fi + +mkdir -p /opt/bitnami/openldap/ldifs + +if [ -d "/tmp/ldif-files" ]; then + cp /tmp/ldif-files/*.ldif /opt/bitnami/openldap/ldifs/ +fi + +/opt/bitnami/scripts/openldap/entrypoint.sh "$@" & +ENTRYPOINT_PID=$! + +echo "Waiting for LDAP server to start..." +while ! ldapsearch -x -H ldap://localhost:1389 -D "cn=admin,dc=opencloud,dc=eu" -w admin -b "dc=opencloud,dc=eu" > /dev/null 2>&1; do + sleep 2 +done + +echo "LDAP server is running, importing LDIF files..." + +if [ -f "/opt/bitnami/openldap/ldifs/10_base.ldif" ]; then + echo "Importing 10_base.ldif..." + ldapadd -x -H ldap://localhost:1389 -D "cn=admin,dc=opencloud,dc=eu" -w admin -f /opt/bitnami/openldap/ldifs/10_base.ldif +fi + +if [ -f "/opt/bitnami/openldap/ldifs/20_users.ldif" ]; then + echo "Importing 20_users.ldif..." + ldapadd -x -H ldap://localhost:1389 -D "cn=admin,dc=opencloud,dc=eu" -w admin -f /opt/bitnami/openldap/ldifs/20_users.ldif +fi + +if [ -f "/opt/bitnami/openldap/ldifs/30_groups.ldif" ]; then + echo "Importing 30_groups.ldif..." + ldapadd -x -H ldap://localhost:1389 -D "cn=admin,dc=opencloud,dc=eu" -w admin -f /opt/bitnami/openldap/ldifs/30_groups.ldif +fi + +echo "LDIF import completed!" + +wait $ENTRYPOINT_PID