diff --git a/.github/workflows/fit_WA_Multitenancy.yml b/.github/workflows/fit_WA_Multitenancy.yml new file mode 100644 index 0000000000..b9ca7c4ab6 --- /dev/null +++ b/.github/workflows/fit_WA_Multitenancy.yml @@ -0,0 +1,45 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +name: "FIT WA Multitenancy" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + +jobs: + fit_WA_Multitenancy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Setup Java JDK + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: 25 + - name: Setup Maven + uses: stCarolas/setup-maven@v5.1 + with: + maven-version: 3.9.6 + - name: Build + run: mvn -U -T 1C -P 'skipTests,all' + - name: 'WA / Multitenancy' + run: mvn -f fit/wa-reference/pom.xml verify -Dinvoker.streamLogs=true -Dmodernizer.skip=true -Drat.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true -Dspring.profiles.active=embedded,https,all,multitenancy -Dit.test=MultitenancyITCase diff --git a/core/persistence-jpa/src/test/resources/domains/TwoContent.xml b/core/persistence-jpa/src/test/resources/domains/TwoContent.xml index 1a9a25dc7d..518e8c882b 100644 --- a/core/persistence-jpa/src/test/resources/domains/TwoContent.xml +++ b/core/persistence-jpa/src/test/resources/domains/TwoContent.xml @@ -110,6 +110,4 @@ we are happy to inform you that the password request was successfully executed f version="${connid.ldap.version}" jsonConf='[{"schema":{"name":"synchronizePasswords","displayName":"Enable Password Synchronization","helpMessage":"If true, the connector will synchronize passwords. The Password Capture Plugin needs to be installed for password synchronization to work.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"maintainLdapGroupMembership","displayName":"Maintain LDAP Group Membership","helpMessage":"When enabled and a user is renamed or deleted, update any LDAP groups to which the user belongs to reflect the new name. Otherwise, the LDAP resource must maintain referential integrity with respect to group membership.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["true"]},{"schema":{"name":"host","displayName":"Host","helpMessage":"The name or IP address of the host where the LDAP server is running.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["localhost"]},{"schema":{"name":"passwordHashAlgorithm","displayName":"Password Hash Algorithm","helpMessage":"Indicates the algorithm that the Identity system should use to hash the password. Currently supported values are SSHA, SHA, SSHA1, and SHA1. A blank value indicates that the system will not hash passwords. This will cause cleartext passwords to be stored in LDAP unless the LDAP server performs the hash (Netscape Directory Server and iPlanet Directory Server do).","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["SHA"]},{"schema":{"name":"port","displayName":"TCP Port","helpMessage":"TCP/IP port number used to communicate with the LDAP server.","type":"int","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[1389]},{"schema":{"name":"vlvSortAttribute","displayName":"VLV Sort Attribute","helpMessage":"Specify the sort attribute to use for VLV indexes on the resource.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"statusManagementClass","displayName":"Status management class ","helpMessage":"Class to be used to manage enabled/disabled status. If no class is specified then identity status management wont be possible.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["net.tirasa.connid.bundles.ldap.commons.AttributeStatusManagement"]},{"schema":{"name":"accountObjectClasses","displayName":"Account Object Classes","helpMessage":"The object class or classes that will be used when creating new user objects in the LDAP tree. When entering more than one object class, each entry should be on its own line; do not use commas or semi-colons to separate multiple object classes. Some object classes may require that you specify all object classes in the class hierarchy.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["inetOrgPerson"]},{"schema":{"name":"accountUserNameAttributes","displayName":"Account User Name Attributes","helpMessage":"Attribute or attributes which holds the account user name. They will be used when authenticating to find the LDAP entry for the user name to authenticate.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["uid"]},{"schema":{"name":"baseContextsToSynchronize","displayName":"Base Contexts to Synchronize","helpMessage":"One or more starting points in the LDAP tree that will be used to determine if a change should be synchronized. The base contexts attribute will be used to synchronize a change if this property is not set.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["ou=people,o=isp","ou=groups,o=isp"]},{"schema":{"name":"accountSynchronizationFilter","displayName":"LDAP Filter for Accounts to Synchronize","helpMessage":"An optional LDAP filter for the objects to synchronize. Because the change log is for all objects, this filter updates only objects that match the specified filter. If you specify a filter, an object will be synchronized only if it matches the filter and includes a synchronized object class.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"removeLogEntryObjectClassFromFilter","displayName":"Remove Log Entry Object Class from Filter","helpMessage":"If this property is set (the default), the filter used to fetch change log entries does not contain the \"changeLogEntry\" object class, expecting that there are no entries of other object types in the change log.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"passwordDecryptionKey","displayName":"Password Decryption Key","helpMessage":"The key to decrypt passwords with when performing password synchronization.","type":"org.identityconnectors.common.security.GuardedByteArray","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"readSchema","displayName":"Read Schema","helpMessage":"If true, the connector will read the schema from the server. If false, the connector will provide a default schema based on the object classes in the configuration. This property must be true in order to use extended object classes.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"ssl","displayName":"SSL","helpMessage":"Select the check box to connect to the LDAP server using SSL.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"passwordAttributeToSynchronize","displayName":"Password Attribute to Synchronize","helpMessage":"The name of the password attribute to synchronize when performing password synchronization.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"accountSearchFilter","displayName":"LDAP Filter for Retrieving Accounts","helpMessage":"An optional LDAP filter to control which accounts are returned from the LDAP resource. If no filter is specified, only accounts that include all specified object classes are returned.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["uid=*"]},{"schema":{"name":"passwordDecryptionInitializationVector","displayName":"Password Decryption Initialization Vector","helpMessage":"The initialization vector to decrypt passwords with when performing password synchronization.","type":"org.identityconnectors.common.security.GuardedByteArray","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"groupMemberAttribute","displayName":"Group Member Attribute","helpMessage":"The name of the group attribute that will be updated with the distinguished name of the user when the user is added to the group.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"failover","displayName":"Failover Servers","helpMessage":"List all servers that should be used for failover in case the preferred server fails. If the preferred server fails, JNDI will connect to the next available server in the list. List all servers in the form of \"ldap://ldap.example.com:389/\", which follows the standard LDAP v3 URLs described in RFC 2255. Only the host and port parts of the URL are relevant in this setting.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"modifiersNamesToFilterOut","displayName":"Filter Out Changes By","helpMessage":"The names (DNs) of directory administrators to filter from the changes. Changes with the attribute \"modifiersName\" that match entries in this list will be filtered out. The standard value is the administrator name used by this adapter, to prevent loops. Entries should be of the format \"cn=Directory Manager\".","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"groupNameAttributes","displayName":"Group Name Attributes","helpMessage":"Attribute or attributes which holds the group name.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["cn"]},{"schema":{"name":"uidAttribute","displayName":"Uid Attribute","helpMessage":"The name of the LDAP attribute which is mapped to the Uid attribute.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["cn"]},{"schema":{"name":"respectResourcePasswordPolicyChangeAfterReset","displayName":"Respect Resource Password Policy Change-After-Reset","helpMessage":"When this resource is specified in a Login Module (i.e., this resource is a pass-through authentication target) and the resource password policy is configured for change-after-reset, a user whose resource account password has been administratively reset will be required to change that password after successfully authenticating.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"filterWithOrInsteadOfAnd","displayName":"Filter with Or Instead of And","helpMessage":"Normally the the filter used to fetch change log entries is an and-based filter retrieving an interval of change entries. If this property is set, the filter will or together the required change numbers instead.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"principal","displayName":"Principal","helpMessage":"The distinguished name with which to authenticate to the LDAP server.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["uid=admin,ou=system"]},{"schema":{"name":"changeLogBlockSize","displayName":"Change Log Block Size","helpMessage":"The number of change log entries to fetch per query.","type":"int","required":true,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[100]},{"schema":{"name":"baseContexts","displayName":"Base Contexts","helpMessage":"One or more starting points in the LDAP tree that will be used when searching the tree. Searches are performed when discovering users from the LDAP server or when looking for the groups of which a user is a member.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["ou=people,o=isp","ou=groups,o=isp"]},{"schema":{"name":"passwordAttribute","displayName":"Password Attribute","helpMessage":"The name of the LDAP attribute which holds the password. When changing an user password, the new password is set to this attribute.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["userpassword"]},{"schema":{"name":"changeNumberAttribute","displayName":"Change Number Attribute","helpMessage":"The name of the change number attribute in the change log entry.","type":"java.lang.String","required":true,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["changeNumber"]},{"schema":{"name":"objectClassesToSynchronize","displayName":"Object Classes to Synchronize","helpMessage":"The object classes to synchronize. The change log is for all objects; this filters updates to just the listed object classes. You should not list the superclasses of an object class unless you intend to synchronize objects with any of the superclass values. For example, if only \"inetOrgPerson\" objects should be synchronized, but the superclasses of \"inetOrgPerson\" (\"person\", \"organizationalperson\" and \"top\") should be filtered out, then list only \"inetOrgPerson\" here. All objects in LDAP are subclassed from \"top\". For this reason, you should never list \"top\", otherwise no object would be filtered.","type":"[Ljava.lang.String;","required":true,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["inetOrgPerson","groupOfUniqueNames"]},{"schema":{"name":"credentials","displayName":"Password","helpMessage":"Password for the principal.","type":"org.identityconnectors.common.security.GuardedString","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["secret"]},{"schema":{"name":"attributesToSynchronize","displayName":"Attributes to Synchronize","helpMessage":"The names of the attributes to synchronize. This ignores updates from the change log if they do not update any of the named attributes. For example, if only \"department\" is listed, then only changes that affect \"department\" will be processed. All other updates are ignored. If blank (the default), then all changes are processed.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"maintainPosixGroupMembership","displayName":"Maintain POSIX Group Membership","helpMessage":"When enabled and a user is renamed or deleted, update any POSIX groups to which the user belongs to reflect the new name. Otherwise, the LDAP resource must maintain referential integrity with respect to group membership.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["truemaintainLdapGroupMembership"]}]' capabilities='["CREATE","UPDATE","DELETE","SEARCH"]'/> - - diff --git a/core/persistence-neo4j/src/test/resources/domains/TwoContent.xml b/core/persistence-neo4j/src/test/resources/domains/TwoContent.xml index 043883e887..b8ba35dc18 100644 --- a/core/persistence-neo4j/src/test/resources/domains/TwoContent.xml +++ b/core/persistence-neo4j/src/test/resources/domains/TwoContent.xml @@ -118,6 +118,4 @@ we are happy to inform you that the password request was successfully executed f jsonConf='[{"schema":{"name":"synchronizePasswords","displayName":"Enable Password Synchronization","helpMessage":"If true, the connector will synchronize passwords. The Password Capture Plugin needs to be installed for password synchronization to work.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"maintainLdapGroupMembership","displayName":"Maintain LDAP Group Membership","helpMessage":"When enabled and a user is renamed or deleted, update any LDAP groups to which the user belongs to reflect the new name. Otherwise, the LDAP resource must maintain referential integrity with respect to group membership.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["true"]},{"schema":{"name":"host","displayName":"Host","helpMessage":"The name or IP address of the host where the LDAP server is running.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["localhost"]},{"schema":{"name":"passwordHashAlgorithm","displayName":"Password Hash Algorithm","helpMessage":"Indicates the algorithm that the Identity system should use to hash the password. Currently supported values are SSHA, SHA, SSHA1, and SHA1. A blank value indicates that the system will not hash passwords. This will cause cleartext passwords to be stored in LDAP unless the LDAP server performs the hash (Netscape Directory Server and iPlanet Directory Server do).","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["SHA"]},{"schema":{"name":"port","displayName":"TCP Port","helpMessage":"TCP/IP port number used to communicate with the LDAP server.","type":"int","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[1389]},{"schema":{"name":"vlvSortAttribute","displayName":"VLV Sort Attribute","helpMessage":"Specify the sort attribute to use for VLV indexes on the resource.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"statusManagementClass","displayName":"Status management class ","helpMessage":"Class to be used to manage enabled/disabled status. If no class is specified then identity status management wont be possible.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["net.tirasa.connid.bundles.ldap.commons.AttributeStatusManagement"]},{"schema":{"name":"accountObjectClasses","displayName":"Account Object Classes","helpMessage":"The object class or classes that will be used when creating new user objects in the LDAP tree. When entering more than one object class, each entry should be on its own line; do not use commas or semi-colons to separate multiple object classes. Some object classes may require that you specify all object classes in the class hierarchy.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["inetOrgPerson"]},{"schema":{"name":"accountUserNameAttributes","displayName":"Account User Name Attributes","helpMessage":"Attribute or attributes which holds the account user name. They will be used when authenticating to find the LDAP entry for the user name to authenticate.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["uid"]},{"schema":{"name":"baseContextsToSynchronize","displayName":"Base Contexts to Synchronize","helpMessage":"One or more starting points in the LDAP tree that will be used to determine if a change should be synchronized. The base contexts attribute will be used to synchronize a change if this property is not set.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["ou=people,o=isp","ou=groups,o=isp"]},{"schema":{"name":"accountSynchronizationFilter","displayName":"LDAP Filter for Accounts to Synchronize","helpMessage":"An optional LDAP filter for the objects to synchronize. Because the change log is for all objects, this filter updates only objects that match the specified filter. If you specify a filter, an object will be synchronized only if it matches the filter and includes a synchronized object class.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"removeLogEntryObjectClassFromFilter","displayName":"Remove Log Entry Object Class from Filter","helpMessage":"If this property is set (the default), the filter used to fetch change log entries does not contain the \"changeLogEntry\" object class, expecting that there are no entries of other object types in the change log.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"passwordDecryptionKey","displayName":"Password Decryption Key","helpMessage":"The key to decrypt passwords with when performing password synchronization.","type":"org.identityconnectors.common.security.GuardedByteArray","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"readSchema","displayName":"Read Schema","helpMessage":"If true, the connector will read the schema from the server. If false, the connector will provide a default schema based on the object classes in the configuration. This property must be true in order to use extended object classes.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"ssl","displayName":"SSL","helpMessage":"Select the check box to connect to the LDAP server using SSL.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"passwordAttributeToSynchronize","displayName":"Password Attribute to Synchronize","helpMessage":"The name of the password attribute to synchronize when performing password synchronization.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"accountSearchFilter","displayName":"LDAP Filter for Retrieving Accounts","helpMessage":"An optional LDAP filter to control which accounts are returned from the LDAP resource. If no filter is specified, only accounts that include all specified object classes are returned.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["uid=*"]},{"schema":{"name":"passwordDecryptionInitializationVector","displayName":"Password Decryption Initialization Vector","helpMessage":"The initialization vector to decrypt passwords with when performing password synchronization.","type":"org.identityconnectors.common.security.GuardedByteArray","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"groupMemberAttribute","displayName":"Group Member Attribute","helpMessage":"The name of the group attribute that will be updated with the distinguished name of the user when the user is added to the group.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"failover","displayName":"Failover Servers","helpMessage":"List all servers that should be used for failover in case the preferred server fails. If the preferred server fails, JNDI will connect to the next available server in the list. List all servers in the form of \"ldap://ldap.example.com:389/\", which follows the standard LDAP v3 URLs described in RFC 2255. Only the host and port parts of the URL are relevant in this setting.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"modifiersNamesToFilterOut","displayName":"Filter Out Changes By","helpMessage":"The names (DNs) of directory administrators to filter from the changes. Changes with the attribute \"modifiersName\" that match entries in this list will be filtered out. The standard value is the administrator name used by this adapter, to prevent loops. Entries should be of the format \"cn=Directory Manager\".","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"groupNameAttributes","displayName":"Group Name Attributes","helpMessage":"Attribute or attributes which holds the group name.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["cn"]},{"schema":{"name":"uidAttribute","displayName":"Uid Attribute","helpMessage":"The name of the LDAP attribute which is mapped to the Uid attribute.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["cn"]},{"schema":{"name":"respectResourcePasswordPolicyChangeAfterReset","displayName":"Respect Resource Password Policy Change-After-Reset","helpMessage":"When this resource is specified in a Login Module (i.e., this resource is a pass-through authentication target) and the resource password policy is configured for change-after-reset, a user whose resource account password has been administratively reset will be required to change that password after successfully authenticating.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"filterWithOrInsteadOfAnd","displayName":"Filter with Or Instead of And","helpMessage":"Normally the the filter used to fetch change log entries is an and-based filter retrieving an interval of change entries. If this property is set, the filter will or together the required change numbers instead.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["false"]},{"schema":{"name":"principal","displayName":"Principal","helpMessage":"The distinguished name with which to authenticate to the LDAP server.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["uid=admin,ou=system"]},{"schema":{"name":"changeLogBlockSize","displayName":"Change Log Block Size","helpMessage":"The number of change log entries to fetch per query.","type":"int","required":true,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[100]},{"schema":{"name":"baseContexts","displayName":"Base Contexts","helpMessage":"One or more starting points in the LDAP tree that will be used when searching the tree. Searches are performed when discovering users from the LDAP server or when looking for the groups of which a user is a member.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["ou=people,o=isp","ou=groups,o=isp"]},{"schema":{"name":"passwordAttribute","displayName":"Password Attribute","helpMessage":"The name of the LDAP attribute which holds the password. When changing an user password, the new password is set to this attribute.","type":"java.lang.String","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["userpassword"]},{"schema":{"name":"changeNumberAttribute","displayName":"Change Number Attribute","helpMessage":"The name of the change number attribute in the change log entry.","type":"java.lang.String","required":true,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["changeNumber"]},{"schema":{"name":"objectClassesToSynchronize","displayName":"Object Classes to Synchronize","helpMessage":"The object classes to synchronize. The change log is for all objects; this filters updates to just the listed object classes. You should not list the superclasses of an object class unless you intend to synchronize objects with any of the superclass values. For example, if only \"inetOrgPerson\" objects should be synchronized, but the superclasses of \"inetOrgPerson\" (\"person\", \"organizationalperson\" and \"top\") should be filtered out, then list only \"inetOrgPerson\" here. All objects in LDAP are subclassed from \"top\". For this reason, you should never list \"top\", otherwise no object would be filtered.","type":"[Ljava.lang.String;","required":true,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["inetOrgPerson","groupOfUniqueNames"]},{"schema":{"name":"credentials","displayName":"Password","helpMessage":"Password for the principal.","type":"org.identityconnectors.common.security.GuardedString","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["secret"]},{"schema":{"name":"attributesToSynchronize","displayName":"Attributes to Synchronize","helpMessage":"The names of the attributes to synchronize. This ignores updates from the change log if they do not update any of the named attributes. For example, if only \"department\" is listed, then only changes that affect \"department\" will be processed. All other updates are ignored. If blank (the default), then all changes are processed.","type":"[Ljava.lang.String;","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":[]},{"schema":{"name":"maintainPosixGroupMembership","displayName":"Maintain POSIX Group Membership","helpMessage":"When enabled and a user is renamed or deleted, update any POSIX groups to which the user belongs to reflect the new name. Otherwise, the LDAP resource must maintain referential integrity with respect to group membership.","type":"boolean","required":false,"order":0,"confidential":false,"defaultValues":null},"overridable":false,"values":["truemaintainLdapGroupMembership"]}]' capabilities='["CREATE","UPDATE","DELETE","SEARCH"]'/> - - diff --git a/fit/wa-reference/pom.xml b/fit/wa-reference/pom.xml index e279666471..f5b556207b 100644 --- a/fit/wa-reference/pom.xml +++ b/fit/wa-reference/pom.xml @@ -34,6 +34,8 @@ under the License. war + embedded,https,all + none ${basedir}/../.. @@ -241,7 +243,7 @@ under the License. - -Dspring.profiles.active=embedded,https,all + -Dspring.profiles.active=${spring.profiles.active} -Dopenfga.api-url=http://${docker.container.openfga.ip}:8080 -Xmx1024m -Xms512m @@ -414,7 +416,7 @@ under the License. - -Dspring.profiles.active=embedded,https,all + -Dspring.profiles.active=${spring.profiles.active} -Dopenfga.api-url=http://${docker.container.openfga.ip}:8080 -Dwicket.core.settings.general.configuration-type=development -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n @@ -427,72 +429,6 @@ under the License. - - hotswap - - - clean verify cargo:run - - - - org.apache.maven.plugins - maven-antrun-plugin - true - - - enableHotSwapForCoreAndConsoleAndEnduser - package - - - - - - - - - run - - - - - - - org.codehaus.cargo - cargo-maven3-plugin - true - - - - - -Dspring.profiles.active=embedded,all - -Dwicket.core.settings.general.configuration-type=development - -javaagent:${java.home}/lib/hotswap/hotswap-agent.jar=autoHotswap=true,disablePlugin=Spring,disablePlugin=Hibernate,disablePlugin=CxfJAXRS - -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 - -XX:+UseConcMarkSweepGC -Xmx1024m -Xms512m - - - - - - - - - src/test/resources - true - - hotswap-agent.properties - - - - - - apache-release diff --git a/fit/wa-reference/src/main/resources/wa-embedded.properties b/fit/wa-reference/src/main/resources/wa-embedded.properties index 40d6658489..c243c0ca03 100644 --- a/fit/wa-reference/src/main/resources/wa-embedded.properties +++ b/fit/wa-reference/src/main/resources/wa-embedded.properties @@ -44,4 +44,4 @@ cas.tgc.crypto.encryption.key=mW6lMvsSo48eZ1Ntt74a-O9jjQQQ_OLUE24RVN2_A_sPX43mpB cas.webflow.crypto.signing.key=Md6kkPlXx5L18TD0mFELpQXWnDbMffj-uPutPckMnAPPuJQEbfcLLYBnOynYIEDgnEpd7sxUwGYd8_sVYFMcjw cas.webflow.crypto.encryption.key=FhLgLpaPL8GVNuqqo7gtiw -management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent +management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent,multitenancy diff --git a/fit/wa-reference/src/main/resources/wa-multitenancy.properties b/fit/wa-reference/src/main/resources/wa-multitenancy.properties new file mode 100644 index 0000000000..a95a51ea7a --- /dev/null +++ b/fit/wa-reference/src/main/resources/wa-multitenancy.properties @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +cas.multitenancy.core.enabled=true diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java index 994a5e2d3c..d49586bf83 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java @@ -248,7 +248,8 @@ protected static CloseableHttpResponse authenticateToWA( final String password, final String body, final CloseableHttpClient httpclient, - final HttpClientContext context) + final HttpClientContext context, + final String... tenant) throws IOException { List form = new ArrayList<>(); @@ -258,7 +259,7 @@ protected static CloseableHttpResponse authenticateToWA( form.add(new BasicNameValuePair("password", password)); form.add(new BasicNameValuePair("geolocation", "")); - HttpPost post = new HttpPost(WA_ADDRESS + "/login"); + HttpPost post = new HttpPost(WA_ADDRESS + (tenant.length == 0 ? "" : "/tenants/" + tenant[0]) + "/login"); post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/MultitenancyITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/MultitenancyITCase.java new file mode 100644 index 0000000000..28bb560b18 --- /dev/null +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/MultitenancyITCase.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.syncope.fit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.util.Optional; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.apache.syncope.client.lib.SyncopeClientFactoryBean; +import org.apache.syncope.common.lib.SyncopeClientException; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.auth.SyncopeAuthModuleConf; +import org.apache.syncope.common.lib.request.UserCR; +import org.apache.syncope.common.lib.to.AuthModuleTO; +import org.apache.syncope.common.lib.to.ProvisioningResult; +import org.apache.syncope.common.lib.to.UserTO; +import org.apache.syncope.common.rest.api.beans.RealmQuery; +import org.apache.syncope.common.rest.api.service.AuthModuleService; +import org.apache.syncope.common.rest.api.service.RealmService; +import org.apache.syncope.common.rest.api.service.UserService; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class MultitenancyITCase extends AbstractITCase { + + protected static final String MT_USERNAME = "multitenancy"; + + protected static final String MT_PASSWORD = "password"; + + @BeforeAll + public static void multitenancySetup() { + assumeTrue(Optional.ofNullable(System.getProperties().getProperty("spring.profiles.active")). + map(profiles -> profiles.contains("multitenancy")).orElse(false)); + + CLIENT_FACTORY = new SyncopeClientFactoryBean().setAddress(CORE_ADDRESS).setDomain("Two"); + + ADMIN_CLIENT = CLIENT_FACTORY.create(ADMIN_UNAME, "password2"); + + // 1. create Syncope auth module + AuthModuleService authModuleService = ADMIN_CLIENT.getService(AuthModuleService.class); + AuthModuleTO syncopeAuthModule = null; + try { + syncopeAuthModule = authModuleService.read("syncopeTwo"); + } catch (SyncopeClientException e) { + if (e.getType().getResponseStatus() == Response.Status.NOT_FOUND) { + SyncopeAuthModuleConf conf = new SyncopeAuthModuleConf(); + conf.setDomain("Two"); + + syncopeAuthModule = new AuthModuleTO(); + syncopeAuthModule.setKey("syncopeTwo"); + syncopeAuthModule.setConf(conf); + + Response response = authModuleService.create(syncopeAuthModule); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode()); + } + } + assertNotNull(syncopeAuthModule); + + // 2. create user + assertNull(ADMIN_CLIENT.getService(RealmService.class). + search(new RealmQuery.Builder().build()).getResult().getFirst().getPasswordPolicy()); + + UserService userService = ADMIN_CLIENT.getService(UserService.class); + UserTO user = null; + try { + user = userService.read(MT_USERNAME); + } catch (SyncopeClientException e) { + if (e.getType().getResponseStatus() == Response.Status.NOT_FOUND) { + UserCR userCR = new UserCR(); + userCR.setRealm(SyncopeConstants.ROOT_REALM); + userCR.setUsername(MT_USERNAME); + userCR.setPassword(MT_PASSWORD); + + Response response = ADMIN_CLIENT.getService(UserService.class).create(userCR); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + ProvisioningResult result = response.readEntity(new GenericType<>() { + }); + user = result.getEntity(); + } + } + assertNotNull(user); + } + + @Test + public void login() throws IOException { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpClientContext context = HttpClientContext.create(); + context.setCookieStore(new BasicCookieStore()); + + String loginPageBody; + try (CloseableHttpResponse response = + httpclient.execute(new HttpGet(WA_ADDRESS + "/tenants/Two/login"), context)) { + + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + loginPageBody = EntityUtils.toString(response.getEntity()); + } + assertNotNull(loginPageBody); + + String location; + try (CloseableHttpResponse response = + authenticateToWA(MT_USERNAME, MT_PASSWORD, loginPageBody, httpclient, context, "Two")) { + + assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode()); + location = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); + } + assertTrue(location.endsWith("/account")); + + try (CloseableHttpResponse response = + httpclient.execute(new HttpGet(WA_ADDRESS + "/tenants/Two/account"), context)) { + + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + assertTrue(EntityUtils.toString(response.getEntity()).contains(MT_USERNAME)); + } + } + } +} diff --git a/src/main/asciidoc/reference-guide/concepts/domains.adoc b/src/main/asciidoc/reference-guide/concepts/domains.adoc index 065b2b7387..3eea561038 100644 --- a/src/main/asciidoc/reference-guide/concepts/domains.adoc +++ b/src/main/asciidoc/reference-guide/concepts/domains.adoc @@ -26,6 +26,10 @@ External Resources, Policies, Tasks, etc. from different domains (e.g. tenants) By default, a single `Master` domain is defined, which also bears the configuration for additional domains. +Every domain besides `Master` is mapped one-to-one with a +https://apereo.github.io/cas/7.3.x/multitenancy/Multitenancy-Overview.html[CAS tenant^] having the same identifier; this +allows for <> configuration to relate only to the given domain's database instance. + [.text-center] image::domains.png[title="Domains",alt="Domains"] diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WAPropertySourceLocator.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WAPropertySourceLocator.java index 40957c160f..ed59649778 100644 --- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WAPropertySourceLocator.java +++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WAPropertySourceLocator.java @@ -53,6 +53,26 @@ public class WAPropertySourceLocator implements PropertySourceLocator { protected static final Logger LOG = LoggerFactory.getLogger(WAPropertySourceLocator.class); + public static Map index(final Map map, final Map prefixes) { + Map indexed = map; + + if (!map.isEmpty()) { + String prefix = map.keySet().iterator().next(); + if (prefix.contains("[]")) { + prefix = StringUtils.substringBefore(prefix, "[]"); + Integer index = prefixes.getOrDefault(prefix, 0); + + indexed = map.entrySet().stream(). + map(e -> Pair.of(e.getKey().replace("[]", "[" + index + "]"), e.getValue())). + collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + + prefixes.put(prefix, index + 1); + } + } + + return indexed; + } + protected final WARestClient waRestClient; protected final AuthModulePropertySourceMapper authModulePropertySourceMapper; @@ -77,26 +97,6 @@ public WAPropertySourceLocator( this.configurationCipher = configurationCipher; } - protected Map index(final Map map, final Map prefixes) { - Map indexed = map; - - if (!map.isEmpty()) { - String prefix = map.keySet().iterator().next(); - if (prefix.contains("[]")) { - prefix = StringUtils.substringBefore(prefix, "[]"); - Integer index = prefixes.getOrDefault(prefix, 0); - - indexed = map.entrySet().stream(). - map(e -> Pair.of(e.getKey().replace("[]", "[" + index + "]"), e.getValue())). - collect(Collectors.toMap(Pair::getKey, Pair::getValue)); - - prefixes.put(prefix, index + 1); - } - } - - return indexed; - } - @Override public PropertySource locate(final Environment environment) { SyncopeClient syncopeClient = waRestClient.getSyncopeClient(); diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WARestClient.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WARestClient.java index c4b1438bdb..12920a4aef 100644 --- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WARestClient.java +++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/WARestClient.java @@ -29,6 +29,7 @@ import org.apache.syncope.common.keymaster.client.api.KeymasterException; import org.apache.syncope.common.keymaster.client.api.ServiceOps; import org.apache.syncope.common.keymaster.client.api.model.NetworkService; +import org.apache.syncope.common.lib.SyncopeConstants; import org.apereo.cas.util.spring.ApplicationContextProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,48 +90,46 @@ protected Optional getCore() { return Optional.empty(); } + public boolean isReady() { + try { + return getCore().isPresent(); + } catch (Exception e) { + LOG.trace("While checking Core's availability: {}", e.getMessage()); + } + return false; + } + + public Optional getSyncopeClient(final String domain) { + return getCore().flatMap(core -> { + try { + return Optional.of(new SyncopeClientFactoryBean(). + setAddress(core.getAddress()). + setUseCompression(useGZIPCompression). + setDomain(domain). + create(new BasicAuthenticationHandler(anonymousUser, anonymousKey))); + } catch (Exception e) { + LOG.error("Could not init SyncopeClient", e); + return Optional.empty(); + } + }); + } + public SyncopeClient getSyncopeClient() { synchronized (this) { if (client == null) { - getCore().ifPresent(core -> { - try { - client = new SyncopeClientFactoryBean(). - setAddress(core.getAddress()). - setUseCompression(useGZIPCompression). - create(new BasicAuthenticationHandler(anonymousUser, anonymousKey)); - } catch (Exception e) { - LOG.error("Could not init SyncopeClient", e); - } - }); + client = getSyncopeClient(SyncopeConstants.MASTER_DOMAIN).orElse(null); } - - return client; } + + return client; } @SuppressWarnings("unchecked") public T getService(final Class serviceClass) { if (!isReady()) { - throw new IllegalStateException("Syncope core is not yet ready"); - } - - T service; - if (services.containsKey(serviceClass)) { - service = (T) services.get(serviceClass); - } else { - service = getSyncopeClient().getService(serviceClass); - services.put(serviceClass, service); + throw new IllegalStateException("Syncope Core is not yet ready"); } - return service; - } - - public boolean isReady() { - try { - return getCore().isPresent(); - } catch (Exception e) { - LOG.trace("While checking Core's availability: {}", e.getMessage()); - } - return false; + return (T) services.computeIfAbsent(serviceClass, k -> getSyncopeClient().getService(k)); } } diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml index 79c47989b4..fcd5a9f25d 100644 --- a/wa/starter/pom.xml +++ b/wa/starter/pom.xml @@ -186,6 +186,20 @@ under the License. org.apereo.cas cas-server-support-reports + + org.apereo.cas + cas-server-support-jdbc-authentication + + + org.apereo.cas + cas-server-support-jdbc-drivers + + + + + org.apereo.cas + cas-server-support-rest-authentication + org.apereo.cas cas-server-support-syncope-authentication diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java index 4452c96ea8..8516f74915 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java @@ -32,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap; import javax.sql.DataSource; import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.keymaster.client.api.DomainOps; import org.apache.syncope.common.keymaster.client.api.model.NetworkService; import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStart; import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStop; @@ -39,6 +40,8 @@ import org.apache.syncope.wa.bootstrap.WAProperties; import org.apache.syncope.wa.bootstrap.WARestClient; import org.apache.syncope.wa.bootstrap.mapping.AttrReleaseMapper; +import org.apache.syncope.wa.bootstrap.mapping.AttrRepoPropertySourceMapper; +import org.apache.syncope.wa.bootstrap.mapping.AuthModulePropertySourceMapper; import org.apache.syncope.wa.starter.actuate.SyncopeCoreHealthIndicator; import org.apache.syncope.wa.starter.actuate.SyncopeWAInfoContributor; import org.apache.syncope.wa.starter.audit.WAAuditTrailManager; @@ -62,6 +65,7 @@ import org.apache.syncope.wa.starter.mapping.TicketExpirationMapper; import org.apache.syncope.wa.starter.mapping.TimeBasedAccessMapper; import org.apache.syncope.wa.starter.mfa.WAMultifactorAuthenticationTrustStorage; +import org.apache.syncope.wa.starter.multitenancy.WATenantsManager; import org.apache.syncope.wa.starter.oidc.WAOidcJsonWebKeystoreGeneratorService; import org.apache.syncope.wa.starter.pac4j.saml.WASAML2ClientCustomizer; import org.apache.syncope.wa.starter.saml.idp.metadata.WASamlIdPMetadataCacheRefresher; @@ -82,6 +86,7 @@ import org.apereo.cas.consent.ConsentRepository; import org.apereo.cas.gauth.CasGoogleAuthenticator; import org.apereo.cas.gauth.credential.LdapGoogleAuthenticatorTokenCredentialRepository; +import org.apereo.cas.multitenancy.TenantsManager; import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService; import org.apereo.cas.otp.repository.credentials.OneTimeTokenCredentialRepository; import org.apereo.cas.pm.LdapPasswordManagementService; @@ -598,6 +603,23 @@ public UserDetailsService actuatorUserDetailsService(final WAProperties waProper return new InMemoryUserDetailsManager(user); } + @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) + @ConditionalOnProperty( + prefix = "cas.multitenancy.core", name = "enabled", havingValue = "true", matchIfMissing = false) + @Bean(name = TenantsManager.BEAN_NAME) + public TenantsManager tenantsManager( + final DomainOps domainOps, + final WARestClient waRestClient, + final AuthModulePropertySourceMapper authModulePropertySourceMapper, + final AttrRepoPropertySourceMapper attrRepoPropertySourceMapper) { + + return new WATenantsManager( + domainOps, + waRestClient, + authModulePropertySourceMapper, + attrRepoPropertySourceMapper); + } + @ConditionalOnProperty( prefix = "keymaster", name = "enableAutoRegistration", havingValue = "true", matchIfMissing = true) @Bean diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/multitenancy/WATenantsManager.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/multitenancy/WATenantsManager.java new file mode 100644 index 0000000000..ac68ea3b5c --- /dev/null +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/multitenancy/WATenantsManager.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.syncope.wa.starter.multitenancy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import org.apache.syncope.client.lib.SyncopeClient; +import org.apache.syncope.common.keymaster.client.api.DomainOps; +import org.apache.syncope.common.rest.api.service.AttrRepoService; +import org.apache.syncope.common.rest.api.service.AuthModuleService; +import org.apache.syncope.common.rest.api.service.wa.WAConfigService; +import org.apache.syncope.wa.bootstrap.WAPropertySourceLocator; +import org.apache.syncope.wa.bootstrap.WARestClient; +import org.apache.syncope.wa.bootstrap.mapping.AttrRepoPropertySourceMapper; +import org.apache.syncope.wa.bootstrap.mapping.AuthModulePropertySourceMapper; +import org.apereo.cas.multitenancy.DefaultTenantAuthenticationPolicy; +import org.apereo.cas.multitenancy.DefaultTenantDelegatedAuthenticationPolicy; +import org.apereo.cas.multitenancy.TenantDefinition; +import org.apereo.cas.multitenancy.TenantsManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WATenantsManager implements TenantsManager { + + protected static final Logger LOG = LoggerFactory.getLogger(WATenantsManager.class); + + protected final DomainOps domainOps; + + protected final WARestClient waRestClient; + + protected final AuthModulePropertySourceMapper authModulePropertySourceMapper; + + protected final AttrRepoPropertySourceMapper attrRepoPropertySourceMapper; + + public WATenantsManager( + final DomainOps domainOps, + final WARestClient waRestClient, + final AuthModulePropertySourceMapper authModulePropertySourceMapper, + final AttrRepoPropertySourceMapper attrRepoPropertySourceMapper) { + + this.domainOps = domainOps; + this.waRestClient = waRestClient; + this.authModulePropertySourceMapper = authModulePropertySourceMapper; + this.attrRepoPropertySourceMapper = attrRepoPropertySourceMapper; + } + + protected TenantDefinition buildTenantDefinition(final SyncopeClient syncopeClient) { + TenantDefinition tenantDefinition = new TenantDefinition(); + tenantDefinition.setId(syncopeClient.getDomain()); + + Map properties = new TreeMap<>(); + Map prefixes = new HashMap<>(); + + DefaultTenantAuthenticationPolicy authPolicy = new DefaultTenantAuthenticationPolicy(); + tenantDefinition.setAuthenticationPolicy(authPolicy); + + DefaultTenantDelegatedAuthenticationPolicy delegatedAuthPolicy = + new DefaultTenantDelegatedAuthenticationPolicy(); + tenantDefinition.setDelegatedAuthenticationPolicy(delegatedAuthPolicy); + + authPolicy.setAuthenticationHandlers(new ArrayList<>()); + delegatedAuthPolicy.setAllowedProviders(new ArrayList<>()); + syncopeClient.getService(AuthModuleService.class).list().forEach(authModuleTO -> { + LOG.debug("Mapping auth module {} ", authModuleTO.getKey()); + + Map map = authModuleTO.getConf().map(authModuleTO, authModulePropertySourceMapper); + properties.putAll(WAPropertySourceLocator.index(map, prefixes)); + + if (map.keySet().stream().anyMatch(k -> k.contains("pac4j"))) { + delegatedAuthPolicy.getAllowedProviders().add(authModuleTO.getKey()); + } else { + authPolicy.getAuthenticationHandlers().add(authModuleTO.getKey()); + } + }); + + authPolicy.setAttributeRepositories(new ArrayList<>()); + syncopeClient.getService(AttrRepoService.class).list().forEach(attrRepoTO -> { + LOG.debug("Mapping attr repo {} ", attrRepoTO.getKey()); + + Map map = attrRepoTO.getConf().map(attrRepoTO, attrRepoPropertySourceMapper); + properties.putAll(WAPropertySourceLocator.index(map, prefixes)); + + authPolicy.getAttributeRepositories().add(attrRepoTO.getKey()); + }); + + syncopeClient.getService(WAConfigService.class).list(). + forEach(attr -> properties.put(attr.getSchema(), String.join(",", attr.getValues()))); + + tenantDefinition.setProperties(properties); + LOG.debug("Collected Tenant {} properties: {}", tenantDefinition.getId(), tenantDefinition.getProperties()); + + return tenantDefinition; + } + + @Override + public Optional findTenant(final String tenantId) { + return waRestClient.getSyncopeClient(tenantId).map(this::buildTenantDefinition); + } + + @Override + public List findTenants() { + List tenants = new ArrayList<>(); + domainOps.list().forEach(domain -> findTenant(domain.getKey()).ifPresent(tenants::add)); + return tenants; + } +} diff --git a/wa/starter/src/main/resources/wa.properties b/wa/starter/src/main/resources/wa.properties index 6adbeefd1e..d22b2fb564 100644 --- a/wa/starter/src/main/resources/wa.properties +++ b/wa/starter/src/main/resources/wa.properties @@ -37,7 +37,7 @@ spring.web.resources.static-locations=classpath:/thymeleaf/static,classpath:/syn cas.monitor.endpoints.endpoint.defaults.access=AUTHENTICATED management.endpoints.access.default=UNRESTRICTED -management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent +management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent,multitenancy management.endpoint.health.show-details=ALWAYS management.endpoint.env.show-values=WHEN_AUTHORIZED spring.cloud.discovery.client.health-indicator.enabled=false diff --git a/wa/starter/src/test/resources/debug/wa-debug.properties b/wa/starter/src/test/resources/debug/wa-debug.properties index 35bd946735..6f14c491ba 100644 --- a/wa/starter/src/test/resources/debug/wa-debug.properties +++ b/wa/starter/src/test/resources/debug/wa-debug.properties @@ -16,12 +16,12 @@ # under the License. spring.main.allow-circular-references=true -#keymaster.address=http://localhost:9080/syncope/rest/keymaster -keymaster.address=https://localhost:9443/syncope/rest/keymaster +keymaster.address=http://localhost:9080/syncope/rest/keymaster +#keymaster.address=https://localhost:9443/syncope/rest/keymaster keymaster.username=${anonymousUser} keymaster.password=${anonymousKey} -management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent +management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent,multitenancy cas.server.name=http://localhost:8080 cas.server.prefix=${cas.server.name}/syncope-wa