diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMSystemSchemaExtensionBuilder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMSystemSchemaExtensionBuilder.java new file mode 100644 index 000000000..bdded34a5 --- /dev/null +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMSystemSchemaExtensionBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. 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.wso2.charon3.core.config; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.wso2.charon3.core.exceptions.CharonException; +import org.wso2.charon3.core.exceptions.InternalErrorException; +import org.wso2.charon3.core.schema.AttributeSchema; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import static org.wso2.charon3.core.schema.SCIMConstants.SYSTEM_USER_SCHEMA_URI; + +/** + * This class is to build the extension system schema through the config file. + */ +public class SCIMSystemSchemaExtensionBuilder extends ExtensionBuilder { + + private static final SCIMSystemSchemaExtensionBuilder instance = new SCIMSystemSchemaExtensionBuilder(); + private static final Map extensionConfig = new HashMap<>(); + private static final Map attributeSchemas = new HashMap<>(); + private AttributeSchema extensionSchema = null; + private String extensionRootAttributeName = null; + private static final String EXTENSION_ROOT_ATTRIBUTE_URI = SYSTEM_USER_SCHEMA_URI; + private static final String DELIMITER = "\\A"; + + /** + * Get the instance of the SCIMSystemSchemaExtensionBuilder. + * + * @return The instance of the SCIMSystemSchemaExtensionBuilder. + */ + public static SCIMSystemSchemaExtensionBuilder getInstance() { + + return instance; + } + + /** + * Get the extension schema. + * + * @return The extension schema. + */ + public AttributeSchema getExtensionSchema() { + + return extensionSchema; + } + + /** + * Build the system schema extension from the config file. + * + * @param configFilePath Path to the config file. + * @throws CharonException If an error occurred while reading the config file. + * @throws InternalErrorException If an error occurred while building the schema. + */ + public void buildSystemSchemaExtension(String configFilePath) throws CharonException, InternalErrorException { + + File provisioningConfig = new File(configFilePath); + try (InputStream configFilePathInputStream = new FileInputStream(provisioningConfig)) { + buildSystemSchemaExtension(configFilePathInputStream); + } catch (FileNotFoundException e) { + throw new CharonException(configFilePath + " file not found!", e); + } catch (JSONException e) { + throw new CharonException("Error while parsing " + configFilePath + " file!", e); + } catch (IOException e) { + throw new CharonException("Error while closing " + configFilePath + " file!", e); + } + } + + /** + * Build the system schema extension from the input stream. + * + * @param inputStream The input stream. + * @throws CharonException If an error occurred while reading the configuration. + * @throws InternalErrorException If an error occurred while building the schema. + */ + public void buildSystemSchemaExtension(InputStream inputStream) throws CharonException, InternalErrorException { + + readConfiguration(inputStream); + for (Map.Entry attributeSchemaConfig : extensionConfig.entrySet()) { + // If there are no children it is a simple attribute, build it. + if (!attributeSchemaConfig.getValue().hasChildren()) { + buildSimpleAttributeSchema(attributeSchemaConfig.getValue(), attributeSchemas); + } else { + // Need to build child schemas first. + buildComplexAttributeSchema(attributeSchemaConfig.getValue(), attributeSchemas, extensionConfig); + } + } + + extensionSchema = attributeSchemas.get(EXTENSION_ROOT_ATTRIBUTE_URI); + } + + /** + * Read the configuration from the input stream. + * + * @param inputStream The input stream. + * @throws CharonException If an error occurred while reading the configuration. + */ + public void readConfiguration(InputStream inputStream) throws CharonException { + + if (inputStream == null) { + throw new CharonException("Input stream is null."); + } + Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name()).useDelimiter(DELIMITER); + String jsonString = scanner.hasNext() ? scanner.next() : StringUtils.EMPTY; + + JSONArray attributeConfigArray = new JSONArray(jsonString); + + for (int index = 0; index < attributeConfigArray.length(); ++index) { + JSONObject rawAttributeConfig = attributeConfigArray.getJSONObject(index); + ExtensionAttributeSchemaConfig schemaAttributeConfig = + new ExtensionAttributeSchemaConfig(rawAttributeConfig); + if (schemaAttributeConfig.getURI().startsWith(EXTENSION_ROOT_ATTRIBUTE_URI)) { + extensionConfig.put(schemaAttributeConfig.getURI(), schemaAttributeConfig); + } + + if (EXTENSION_ROOT_ATTRIBUTE_URI.equals(schemaAttributeConfig.getURI())) { + extensionRootAttributeName = schemaAttributeConfig.getName(); + } + } + } + + @Override + public String getURI() { + + return EXTENSION_ROOT_ATTRIBUTE_URI; + } + + @Override + protected boolean isRootConfig(ExtensionAttributeSchemaConfig config) { + + return StringUtils.equals(extensionRootAttributeName, config.getName()); + } +} diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMUserSchemaExtensionBuilder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMUserSchemaExtensionBuilder.java index 5d7b3a040..751563362 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMUserSchemaExtensionBuilder.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMUserSchemaExtensionBuilder.java @@ -28,10 +28,13 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Scanner; +import static org.wso2.charon3.core.schema.SCIMConstants.ENTERPRISE_USER_SCHEMA_URI; + /** * This class is to build the extension user schema though the config file. */ @@ -42,9 +45,9 @@ public class SCIMUserSchemaExtensionBuilder extends ExtensionBuilder { private static Map extensionConfig = new HashMap<>(); // Extension root attribute name. String extensionRootAttributeName = null; - String extensionRootAttributeURI = null; + String extensionRootAttributeURI = ENTERPRISE_USER_SCHEMA_URI; // built schema map - private static Map attributeSchemas = new HashMap(); + private static Map attributeSchemas = new HashMap<>(); // extension root attribute schema private AttributeSchema extensionSchema = null; @@ -80,25 +83,21 @@ public void buildUserSchemaExtension(InputStream inputStream) throws CharonExcep readConfiguration(inputStream); for (Map.Entry attributeSchemaConfig : extensionConfig.entrySet()) { - // if there are no children its a simple attribute, build it + // If there are no children it's a simple attribute, build it. if (!attributeSchemaConfig.getValue().hasChildren()) { buildSimpleAttributeSchema(attributeSchemaConfig.getValue(), attributeSchemas); } else { - // need to build child schemas first + // Need to build child schemas first. buildComplexAttributeSchema(attributeSchemaConfig.getValue(), attributeSchemas, extensionConfig); } } - // now get the extension schema - /* - * Assumption : Final config in the configuration file is the extension - * root attribute - */ + extensionSchema = attributeSchemas.get(extensionRootAttributeURI); } public void readConfiguration(InputStream inputStream) throws CharonException { - //Scanner scanner = new Scanner(new FileInputStream(provisioningConfig)); - Scanner scanner = new Scanner(inputStream, "utf-8").useDelimiter("\\A"); + + Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name()).useDelimiter("\\A"); String jsonString = scanner.hasNext() ? scanner.next() : ""; JSONArray attributeConfigArray = new JSONArray(jsonString); @@ -107,13 +106,11 @@ public void readConfiguration(InputStream inputStream) throws CharonException { JSONObject rawAttributeConfig = attributeConfigArray.getJSONObject(index); ExtensionAttributeSchemaConfig schemaAttributeConfig = new ExtensionAttributeSchemaConfig(rawAttributeConfig); - extensionConfig.put(schemaAttributeConfig.getURI(), schemaAttributeConfig); + if (schemaAttributeConfig.getURI().startsWith(extensionRootAttributeURI)) { + extensionConfig.put(schemaAttributeConfig.getURI(), schemaAttributeConfig); + } - /** - * NOTE: Assume last config is the root config - */ - if (index == attributeConfigArray.length() - 1) { - extensionRootAttributeURI = schemaAttributeConfig.getURI(); + if (extensionRootAttributeURI.equals(schemaAttributeConfig.getURI())) { extensionRootAttributeName = schemaAttributeConfig.getName(); } } @@ -126,6 +123,7 @@ public String getURI() { return extensionRootAttributeURI; } + @Override protected boolean isRootConfig(ExtensionAttributeSchemaConfig config) { return StringUtils.isNotBlank(extensionRootAttributeName) && diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java index 0b790254a..115eb34e0 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java @@ -536,11 +536,12 @@ public ComplexAttribute buildComplexAttribute(AttributeSchema complexAttributeSc List subAttributeSchemas = ((AttributeSchema) complexAttributeSchema).getSubAttributeSchemas(); String userExtensionName = SCIMResourceSchemaManager.getInstance().getExtensionName(); + String systemExtensionName = SCIMResourceSchemaManager.getInstance().getSystemSchemaExtensionName(); String customExtensionName = SCIMResourceSchemaManager.getInstance().getCustomSchemaExtensionURI(); //iterate through the complex attribute schema and extract the sub attributes. for (AttributeSchema subAttributeSchema : subAttributeSchemas) { - //obtain the user defined value for given key- attribute schema name + //obtain the user defined value for given key-attribute schema name Object attributeValObj = jsonObject.opt(subAttributeSchema.getName()); SCIMDefinitions.DataType subAttributeSchemaType = subAttributeSchema.getType(); if (subAttributeSchemaType.equals(STRING) || subAttributeSchemaType.equals(BINARY) || @@ -579,8 +580,9 @@ public ComplexAttribute buildComplexAttribute(AttributeSchema complexAttributeSc //this case is only valid for the extension schema //As according to the spec we have complex attribute inside complex attribute only for extension, //we need to treat it separately - } else if ((complexAttributeSchema.getName().equals(userExtensionName)) || - (complexAttributeSchema.getName().equals(customExtensionName))) { + } else if ((complexAttributeSchema.getName().equals(userExtensionName)) + || (complexAttributeSchema.getName().equals(customExtensionName)) + || (complexAttributeSchema.getName().equals(systemExtensionName))) { if (subAttributeSchemaType.equals(COMPLEX)) { //check for user defined extension's schema violation List subList = subAttributeSchema.getSubAttributeSchemas(); @@ -656,7 +658,6 @@ public ComplexAttribute buildComplexAttribute(AttributeSchema complexAttributeSc return (ComplexAttribute) DefaultAttributeFactory.createAttribute(complexAttributeSchema, complexAttribute); } - /* * To build a complex type value of a Multi Valued Attribute. (eg. Email with value,type,primary as sub attributes * diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java index a410aabec..27a1cff86 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java @@ -467,13 +467,13 @@ public String buildServiceProviderConfigJsonBody(HashMap config) } - /* - * Build the user resource type json representation. - * @return + /** + * Build the user resource type json representation. + * @return json representation of user resource type. */ public String buildUserResourceTypeJsonBody() throws JSONException { - JSONObject userResourceTypeObject = new JSONObject(); + JSONObject userResourceTypeObject = new JSONObject(); userResourceTypeObject.put( SCIMConstants.CommonSchemaConstants.SCHEMAS, SCIMConstants.RESOURCE_TYPE_SCHEMA_URI); userResourceTypeObject.put( @@ -499,6 +499,15 @@ public String buildUserResourceTypeJsonBody() throws JSONException { SCIMResourceSchemaManager.getInstance().getExtensionRequired()); schemaExtensions.put(extensionSchemaObject); + JSONObject systemSchemaObject = new JSONObject(); + systemSchemaObject.put( + SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_SCHEMA, + SCIMResourceSchemaManager.getInstance().getSystemSchemaExtensionURI()); + systemSchemaObject.put( + SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_REQUIRED, + SCIMResourceSchemaManager.getInstance().getSystemSchemaExtensionRequired()); + schemaExtensions.put(systemSchemaObject); + // Add custom user schema as a schema extension. if (StringUtils.isNotBlank(SCIMResourceSchemaManager.getInstance().getCustomSchemaExtensionURI())) { JSONObject customSchemaObject = new JSONObject(); diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java index b0c57abd7..95949dbc2 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java @@ -35,8 +35,8 @@ import java.util.Map; /** - * This is the interface for usermanager extension. - * An implementation can plugin their own user manager-(either LDAP based, DB based etc) + * This is the interface for user manager extension. + * An implementation can plug in their own user manager-(either LDAP based, DB based etc.) * by implementing this interface and mentioning it in configuration. */ public interface UserManager { @@ -265,6 +265,20 @@ default List getEnterpriseUserSchema() throws CharonException, NotImp throw new NotImplementedException(); } + /** + * Retrieve schema of the system user. + * + * @return List of attributes of system user schema. + * @throws CharonException Charon exception. + * @throws NotImplementedException Functionality no implemented exception. + * @throws BadRequestException Bad request exception. + */ + default List getSystemUserSchema() throws CharonException, NotImplementedException, + BadRequestException { + + throw new NotImplementedException(); + } + /** * Return Custom schema. * @return Custom schema. diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/SchemaResourceManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/SchemaResourceManager.java index ae96a21d3..a4a122daf 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/SchemaResourceManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/SchemaResourceManager.java @@ -47,6 +47,9 @@ import static org.wso2.charon3.core.schema.SCIMConstants.ENTERPRISE_USER_SCHEMA_URI; import static org.wso2.charon3.core.schema.SCIMConstants.EnterpriseUserSchemaConstants.ENTERPRISE_USER_DESC; import static org.wso2.charon3.core.schema.SCIMConstants.ResourceTypeSchemaConstants.USER_ACCOUNT; +import static org.wso2.charon3.core.schema.SCIMConstants.SYSTEM_USER; +import static org.wso2.charon3.core.schema.SCIMConstants.SYSTEM_USER_SCHEMA_URI; +import static org.wso2.charon3.core.schema.SCIMConstants.SystemUserSchemaConstants.SYSTEM_USER_DESC; import static org.wso2.charon3.core.schema.SCIMConstants.USER; import static org.wso2.charon3.core.schema.SCIMConstants.USER_CORE_SCHEMA_URI; @@ -81,6 +84,7 @@ public SCIMResponse get(String id, UserManager userManager, String attributes, S List coreSchemaAttributes = userManager.getCoreSchema(); List userSchemaAttributes = userManager.getUserSchema(); List userEnterpriseSchemaAttributes = userManager.getEnterpriseUserSchema(); + List userSystemSchemaAttributes = userManager.getSystemUserSchema(); List userCustomSchemaAttributes = userManager.getCustomUserSchemaAttributes(); String customUserSchemaURI = SCIMCustomSchemaExtensionBuilder.getInstance().getURI(); @@ -90,6 +94,7 @@ public SCIMResponse get(String id, UserManager userManager, String attributes, S schemas.put(CORE_SCHEMA_URI, coreSchemaAttributes); schemas.put(USER_CORE_SCHEMA_URI, userSchemaAttributes); schemas.put(ENTERPRISE_USER_SCHEMA_URI, userEnterpriseSchemaAttributes); + schemas.put(SYSTEM_USER_SCHEMA_URI, userSystemSchemaAttributes); if (StringUtils.isNotBlank(customUserSchemaURI)) { schemas.put(customUserSchemaURI, userCustomSchemaAttributes); } @@ -103,6 +108,8 @@ public SCIMResponse get(String id, UserManager userManager, String attributes, S schemas.put(USER_CORE_SCHEMA_URI, userSchemaAttributes); } else if (ENTERPRISE_USER_SCHEMA_URI.equalsIgnoreCase(id)) { schemas.put(ENTERPRISE_USER_SCHEMA_URI, userEnterpriseSchemaAttributes); + } else if (SYSTEM_USER_SCHEMA_URI.equalsIgnoreCase(id)) { + schemas.put(SYSTEM_USER_SCHEMA_URI, userSystemSchemaAttributes); } else if (StringUtils.isNotBlank(customUserSchemaURI) && customUserSchemaURI.equalsIgnoreCase(id)) { schemas.put(customUserSchemaURI, userCustomSchemaAttributes); } else { @@ -155,6 +162,10 @@ private JSONArray buildSchemasResponseBody(Map> schemas) JSONObject enterpriseUserSchemaObject = buildEnterpriseUserSchema(schemas.get(ENTERPRISE_USER_SCHEMA_URI)); rootObject.put(enterpriseUserSchemaObject); } + if (schemas.get(SYSTEM_USER_SCHEMA_URI) != null) { + JSONObject systemUserSchemaObject = buildSystemUserSchema(schemas.get(SYSTEM_USER_SCHEMA_URI)); + rootObject.put(systemUserSchemaObject); + } String customSchemaURI = SCIMCustomSchemaExtensionBuilder.getInstance().getURI(); if (StringUtils.isNotBlank(customSchemaURI) && schemas.get(customSchemaURI) != null) { JSONObject customUserSchemaObject = buildCustomUserSchema(customSchemaURI, schemas.get(customSchemaURI)); @@ -189,6 +200,31 @@ private JSONObject buildEnterpriseUserSchema(List enterpriseUserSchem } } + /** + * Builds a JSON object containing system user schema attribute information. + * + * @param systemUserSchemaList Attribute list of SCIM system user schema + * @return JSON object of system user schema + * @throws CharonException + */ + private JSONObject buildSystemUserSchema(List systemUserSchemaList) throws CharonException { + + try { + JSONEncoder encoder = getEncoder(); + + JSONObject systemUserSchemaObject = new JSONObject(); + systemUserSchemaObject.put(SCIMConstants.CommonSchemaConstants.ID, SYSTEM_USER_SCHEMA_URI); + systemUserSchemaObject.put(SCIMConstants.SystemUserSchemaConstants.NAME, SYSTEM_USER); + systemUserSchemaObject.put(SCIMConstants.SystemUserSchemaConstants.DESCRIPTION, SYSTEM_USER_DESC); + + JSONArray systemUserAttributeArray = buildSchemaAttributeArray(systemUserSchemaList, encoder); + systemUserSchemaObject.put(ATTRIBUTES, systemUserAttributeArray); + return systemUserSchemaObject; + } catch (JSONException e) { + throw new CharonException("Error while encoding system user schema.", e); + } + } + private JSONObject buildCustomUserSchema(String customSchemaURI, List customUserSchemaList) throws CharonException { diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/AbstractValidator.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/AbstractValidator.java index 027600964..fc2d71ade 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/AbstractValidator.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/AbstractValidator.java @@ -990,8 +990,13 @@ private static void checkIfReadOnlyAndImmutableSubAttributesModified(Map subAttributeSchemaList = attributeSchema.getSubAttributeSchemas(); if (subAttributeSchemaList != null) { - if (SCIMResourceSchemaManager.getInstance().getExtensionName() != null) { - if (attributeSchema.getName().equals(SCIMResourceSchemaManager.getInstance().getExtensionName())) { + String attributeName = attributeSchema.getName(); + SCIMResourceSchemaManager schemaManager = SCIMResourceSchemaManager.getInstance(); + + if (attributeName != null) { + String extensionName = schemaManager.getExtensionName(); + String systemSchemaExtensionName = schemaManager.getSystemSchemaExtensionName(); + if (attributeName.equals(extensionName) || attributeName.equals(systemSchemaExtensionName)) { checkIfReadOnlyAndImmutableExtensionAttributesModified(subAttributeSchemaList, newAttribute, oldAttribute); } diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java index 458986ec1..44f31afaf 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java @@ -27,7 +27,10 @@ public class SCIMConstants { public static final String USER_CORE_SCHEMA_URI = "urn:ietf:params:scim:schemas:core:2.0:User"; public static final String ENTERPRISE_USER_SCHEMA_URI = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"; + @Deprecated public static final String CUSTOM_USER_SCHEMA_URI = "urn:scim:wso2:schema"; + public static final String SYSTEM_USER_SCHEMA_URI = "urn:scim:wso2:schema"; + public static final String CUSTOM_EXTENSION_SCHEMA_URI = "urn:scim:schemas:extension:custom:User"; public static final String GROUP_CORE_SCHEMA_URI = "urn:ietf:params:scim:schemas:core:2.0:Group"; public static final String ROLE_SCHEMA_URI = "urn:ietf:params:scim:schemas:extension:2.0:Role"; public static final String LISTED_RESOURCE_CORE_SCHEMA_URI = "urn:ietf:params:scim:api:messages:2.0:ListResponse"; @@ -53,6 +56,7 @@ public class SCIMConstants { public static final String GROUP = "Group"; public static final String ROLE = "Role"; public static final String ENTERPRISE_USER = "EnterpriseUser"; + public static final String SYSTEM_USER = "SystemUser"; public static final String CUSTOM_USER = "CustomUser"; public static final String RESOURCE_TYPE = "ResourceType"; @@ -775,6 +779,16 @@ public static class EnterpriseUserSchemaConstants { public static final String ENTERPRISE_USER_DESC = "Enterprise User"; } + /** + * Constants found in system user schema. + */ + public static class SystemUserSchemaConstants { + + public static final String NAME = "name"; + public static final String DESCRIPTION = "description"; + public static final String SYSTEM_USER_DESC = "System User"; + } + /** * Constants found in custom user schema. */ diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManager.java index 209759bf5..83eabd718 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManager.java @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wso2.charon3.core.config.SCIMCustomSchemaExtensionBuilder; +import org.wso2.charon3.core.config.SCIMSystemSchemaExtensionBuilder; import org.wso2.charon3.core.config.SCIMUserSchemaExtensionBuilder; import org.wso2.charon3.core.exceptions.BadRequestException; import org.wso2.charon3.core.exceptions.CharonException; @@ -34,7 +35,7 @@ /** * This is to check for extension schema for the user and buildTree a custom user schema with it. -* Unless a extension is defined, core-user schema need to be returned. +* Unless an extension is defined, core-user schema need to be returned. */ public class SCIMResourceSchemaManager { @@ -52,35 +53,49 @@ public static SCIMResourceSchemaManager getInstance() { */ public SCIMResourceTypeSchema getUserResourceSchema() { - AttributeSchema schemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); - if (schemaExtension != null) { - return SCIMResourceTypeSchema.createSCIMResourceSchema( - new ArrayList(Arrays.asList(SCIMConstants.USER_CORE_SCHEMA_URI, schemaExtension.getURI())), - SCIMSchemaDefinitions.ID, SCIMSchemaDefinitions.EXTERNAL_ID, SCIMSchemaDefinitions.META, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USERNAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.DISPLAY_NAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NICK_NAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PROFILE_URL, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TITLE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USER_TYPE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PREFERRED_LANGUAGE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.LOCALE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TIME_ZONE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ACTIVE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PASSWORD, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.EMAILS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHONE_NUMBERS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.IMS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHOTOS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ADDRESSES, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.GROUPS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ENTITLEMENTS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ROLES, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.X509CERTIFICATES, - schemaExtension); + AttributeSchema enterpriseSchemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); + AttributeSchema systemSchemaExtension = SCIMSystemSchemaExtensionBuilder.getInstance().getExtensionSchema(); + + List schemaURIs = new ArrayList<>(); + schemaURIs.add(SCIMConstants.USER_CORE_SCHEMA_URI); + + List schemaDefinitions = new ArrayList<>(Arrays.asList( + SCIMSchemaDefinitions.ID, + SCIMSchemaDefinitions.EXTERNAL_ID, + SCIMSchemaDefinitions.META, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USERNAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.DISPLAY_NAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NICK_NAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PROFILE_URL, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TITLE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USER_TYPE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PREFERRED_LANGUAGE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.LOCALE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TIME_ZONE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ACTIVE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PASSWORD, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.EMAILS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHONE_NUMBERS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.IMS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHOTOS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ADDRESSES, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.GROUPS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ENTITLEMENTS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ROLES, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.X509CERTIFICATES + )); + + if (Boolean.TRUE.equals(SCIMResourceSchemaManager.getInstance().isExtensionSet())) { + schemaURIs.add(enterpriseSchemaExtension.getURI()); + schemaURIs.add(systemSchemaExtension.getURI()); + + schemaDefinitions.add(enterpriseSchemaExtension); + schemaDefinitions.add(systemSchemaExtension); } - return SCIMSchemaDefinitions.SCIM_USER_SCHEMA; + + return SCIMResourceTypeSchema.createSCIMResourceSchema( + schemaURIs, schemaDefinitions.toArray(new AttributeSchema[0])); } /* @@ -92,65 +107,76 @@ public SCIMResourceTypeSchema getUserResourceSchema(UserManager userManager) throws BadRequestException, NotImplementedException, CharonException { AttributeSchema enterpriseSchemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); + AttributeSchema systemSchemaExtension = SCIMSystemSchemaExtensionBuilder.getInstance().getExtensionSchema(); AttributeSchema customSchemaExtension = userManager.getCustomUserSchemaExtension(); - if (enterpriseSchemaExtension != null) { - List schemas = new ArrayList<>(); - schemas.add(SCIMConstants.USER_CORE_SCHEMA_URI); + + List schemas = new ArrayList<>(); + schemas.add(SCIMConstants.USER_CORE_SCHEMA_URI); + + List schemaDefinitions = new ArrayList<>(Arrays.asList( + SCIMSchemaDefinitions.ID, + SCIMSchemaDefinitions.EXTERNAL_ID, + SCIMSchemaDefinitions.META, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USERNAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.DISPLAY_NAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NICK_NAME, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PROFILE_URL, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TITLE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USER_TYPE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PREFERRED_LANGUAGE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.LOCALE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TIME_ZONE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ACTIVE, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PASSWORD, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.EMAILS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHONE_NUMBERS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.IMS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHOTOS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ADDRESSES, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.GROUPS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ENTITLEMENTS, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ROLES, + SCIMSchemaDefinitions.SCIMUserSchemaDefinition.X509CERTIFICATES + )); + + if (Boolean.TRUE.equals(SCIMResourceSchemaManager.getInstance().isExtensionSet())) { schemas.add(enterpriseSchemaExtension.getURI()); - if (customSchemaExtension != null) { - schemas.add(customSchemaExtension.getURI()); - } else { - log.warn("Could not find Custom schema."); - } - return SCIMResourceTypeSchema.createSCIMResourceSchema( - schemas, - SCIMSchemaDefinitions.ID, SCIMSchemaDefinitions.EXTERNAL_ID, SCIMSchemaDefinitions.META, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USERNAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.DISPLAY_NAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.NICK_NAME, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PROFILE_URL, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TITLE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.USER_TYPE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PREFERRED_LANGUAGE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.LOCALE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.TIME_ZONE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ACTIVE, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PASSWORD, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.EMAILS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHONE_NUMBERS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.IMS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.PHOTOS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ADDRESSES, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.GROUPS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ENTITLEMENTS, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.ROLES, - SCIMSchemaDefinitions.SCIMUserSchemaDefinition.X509CERTIFICATES, - enterpriseSchemaExtension, customSchemaExtension); + schemas.add(systemSchemaExtension.getURI()); + + schemaDefinitions.add(enterpriseSchemaExtension); + schemaDefinitions.add(systemSchemaExtension); } - return SCIMSchemaDefinitions.SCIM_USER_SCHEMA; + + if (customSchemaExtension != null) { + schemas.add(customSchemaExtension.getURI()); + schemaDefinitions.add(customSchemaExtension); + } else { + log.warn("Could not find custom schema."); + } + + return SCIMResourceTypeSchema.createSCIMResourceSchema( + schemas, schemaDefinitions.toArray(new AttributeSchema[0])); } - /* - * check whether the extension is enabled + /** + * Check whether the extension is enabled. * - * @return + * @return true if extension is enabled. */ public Boolean isExtensionSet() { + AttributeSchema schemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); - if (schemaExtension != null) { - return true; - } else { - return false; - } + return schemaExtension != null; } - /* - * return the extension name + /** + * Return the extension name. * - * @return + * @return extension name */ public String getExtensionName() { + AttributeSchema schemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); if (schemaExtension == null) { return null; @@ -158,22 +184,37 @@ public String getExtensionName() { return schemaExtension.getName(); } - /* - * return the extension name + /** + * Return the system schema extension name. * - * @return + * @return system schema extension name + */ + public String getSystemSchemaExtensionName() { + + AttributeSchema schemaExtension = SCIMSystemSchemaExtensionBuilder.getInstance().getExtensionSchema(); + if (schemaExtension == null) { + return null; + } + return schemaExtension.getName(); + } + + /** + * Return the custom schema extension name. + * + * @return custom schema extension name */ public String getCustomSchemaExtensionURI() { return SCIMCustomSchemaExtensionBuilder.getInstance().getURI(); } - /* - * return the extension uri + /** + * Return the extension uri. * - * @return + * @return extension uri */ public String getExtensionURI() { + AttributeSchema schemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); if (schemaExtension == null) { return null; @@ -181,10 +222,38 @@ public String getExtensionURI() { return schemaExtension.getURI(); } - /* - * return the extension's required property + /** + * Return the system schema extension uri. * - * @return + * @return system schema extension uri + */ + public String getSystemSchemaExtensionURI() { + + AttributeSchema schemaExtension = SCIMSystemSchemaExtensionBuilder.getInstance().getExtensionSchema(); + if (schemaExtension == null) { + return null; + } + return schemaExtension.getURI(); + } + + /** + * Return the system schema extension's required property. + * + * @return extension's required property + */ + public boolean getSystemSchemaExtensionRequired() { + + AttributeSchema schemaExtension = SCIMSystemSchemaExtensionBuilder.getInstance().getExtensionSchema(); + if (schemaExtension == null) { + return false; + } + return schemaExtension.getRequired(); + } + + /** + * Return the extension's required property. + * + * @return extension's required property */ public boolean getExtensionRequired() { AttributeSchema schemaExtension = SCIMUserSchemaExtensionBuilder.getInstance().getExtensionSchema(); diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java index 982a445b6..e9a310336 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java @@ -134,7 +134,7 @@ public static class SCIMUserSchemaDefinition { //sub attributes of email attribute - //"Email addresses for the user. + //Email addresses for the user. public static final SCIMAttributeSchema EMAIL_VALUE = SCIMAttributeSchema.createSCIMAttributeSchema(SCIMConstants.UserSchemaConstants.EMAILS_VALUE_URI, SCIMConstants.CommonSchemaConstants.VALUE, diff --git a/modules/charon-core/src/test/java/org/wso2/charon3/core/config/SCIMSystemSchemaExtensionBuilderTest.java b/modules/charon-core/src/test/java/org/wso2/charon3/core/config/SCIMSystemSchemaExtensionBuilderTest.java new file mode 100644 index 000000000..97ab211ed --- /dev/null +++ b/modules/charon-core/src/test/java/org/wso2/charon3/core/config/SCIMSystemSchemaExtensionBuilderTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. 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.wso2.charon3.core.config; + +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.charon3.core.schema.AttributeSchema; + +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.wso2.charon3.core.schema.SCIMConstants.SYSTEM_USER_SCHEMA_URI; + +/** + * Unit test for SCIMSystemSchemaExtensionBuilder. + */ +public class SCIMSystemSchemaExtensionBuilderTest { + + private SCIMSystemSchemaExtensionBuilder builder; + + @Test + void testGetInstance() { + + SCIMSystemSchemaExtensionBuilder instance1 = SCIMSystemSchemaExtensionBuilder.getInstance(); + SCIMSystemSchemaExtensionBuilder instance2 = SCIMSystemSchemaExtensionBuilder.getInstance(); + assertNotNull(instance1, "The getInstance method should return a non-null instance."); + assertSame(instance1, instance2, "The getInstance method should return the same instance every time."); + } + + @Test + void testGetExtensionSchema() throws NoSuchFieldException, IllegalAccessException { + + AttributeSchema mockSchema = Mockito.mock(AttributeSchema.class); + Field extensionSchemaField = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField("extensionSchema"); + extensionSchemaField.setAccessible(true); + extensionSchemaField.set(builder, mockSchema); + + AttributeSchema result = builder.getExtensionSchema(); + assertNotNull(result, "The getExtensionSchema method should not return null."); + assertSame(mockSchema, result, + "The getExtensionSchema method should return the correct schema instance."); + } + + @Test + void testGetURI() { + + String result = builder.getURI(); + assertNotNull(result, "The getURI method should not return null."); + assertEquals(result, SYSTEM_USER_SCHEMA_URI, "The getURI method should return the correct URI."); + } + + @Test + void testIsRootConfig() throws NoSuchFieldException, IllegalAccessException { + + ExtensionBuilder.ExtensionAttributeSchemaConfig mockConfig = Mockito.mock( + ExtensionBuilder.ExtensionAttributeSchemaConfig.class); + Field rootNameField = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField( + "extensionRootAttributeName"); + rootNameField.setAccessible(true); + String testName = "testRootAttributeName"; + rootNameField.set(builder, testName); + + Mockito.when(mockConfig.getName()).thenReturn(testName); + boolean result = builder.isRootConfig(mockConfig); + assertTrue(result, "The isRootConfig method should return true for the root configuration."); + + Mockito.when(mockConfig.getName()).thenReturn("nonMatchingName"); + result = builder.isRootConfig(mockConfig); + assertFalse(result, "The isRootConfig method should return false for a non-root configuration."); + } + + @Test + void testReadConfiguration() throws Exception { + + InputStream inputStream = getClass().getClassLoader().getResourceAsStream( + "scim2-schema-extension-test.config"); + assertNotNull(inputStream, "The test configuration file should exist."); + + builder.readConfiguration(inputStream); + + Field configField = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField("extensionConfig"); + configField.setAccessible(true); + @SuppressWarnings("unchecked") + Map extensionConfig = + (Map) configField.get(builder); + + assertNotNull(extensionConfig, "The extensionConfig map should not be null."); + assertEquals(extensionConfig.size(), 3, "The extensionConfig map should contain 2 entries."); + assertTrue(extensionConfig.containsKey("urn:scim:wso2:schema:testURI1"), + "The extensionConfig map should contain testURI1."); + assertTrue(extensionConfig.containsKey("urn:scim:wso2:schema:testURI2"), + "The extensionConfig map should contain testURI2."); + assertTrue(extensionConfig.containsKey("urn:scim:wso2:schema"), + "The extensionConfig map should contain testURI2."); + } + + @Test + void testBuildSystemSchemaExtension() throws Exception { + + InputStream inputStream = getClass().getClassLoader().getResourceAsStream( + "scim2-schema-extension-test.config"); + assertNotNull(inputStream, "The test configuration file should exist."); + + builder.buildSystemSchemaExtension(inputStream); + + Field schemaField = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField("extensionSchema"); + schemaField.setAccessible(true); + AttributeSchema extensionSchema = (AttributeSchema) schemaField.get(builder); + + assertNotNull(extensionSchema, "The extensionSchema should not be null after building."); + assertEquals(extensionSchema.getURI(), "urn:scim:wso2:schema", + "The root URI of the extension schema should match."); + } + + @Test + void testBuildSystemSchemaExtensionWithConfigFilePath() throws Exception { + + builder.buildSystemSchemaExtension("src/test/resources/scim2-schema-extension-test.config"); + + Field schemaField = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField("extensionSchema"); + schemaField.setAccessible(true); + AttributeSchema extensionSchema = (AttributeSchema) schemaField.get(builder); + + assertNotNull(extensionSchema, "The extensionSchema should not be null after building from file."); + assertEquals(extensionSchema.getURI(), "urn:scim:wso2:schema", + "The root URI of the extension schema should match."); + } + + @BeforeMethod + void setUp() throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, + InstantiationException { + + builder = SCIMSystemSchemaExtensionBuilder.getInstance(); + + Constructor constructor = + SCIMSystemSchemaExtensionBuilder.class.getDeclaredConstructor(); + constructor.setAccessible(true); + SCIMSystemSchemaExtensionBuilder newBuilderInstance = constructor.newInstance(); + + resetSingletonField("EXTENSION_ROOT_ATTRIBUTE_URI", SYSTEM_USER_SCHEMA_URI); + resetSingletonField("instance", newBuilderInstance); + resetSingletonField("extensionConfig", new HashMap<>()); + resetSingletonField("attributeSchemas", new HashMap<>()); + + setInstanceField("extensionSchema", null); + setInstanceField("extensionRootAttributeName", null); + } + + private void resetSingletonField(String fieldName, Object newValue) throws NoSuchFieldException, + IllegalAccessException { + + Field field = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + + field.set(null, newValue); + } + + private void setInstanceField(String fieldName, Object value) throws NoSuchFieldException, + IllegalAccessException { + + Field field = SCIMSystemSchemaExtensionBuilder.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(builder, value); + } +} diff --git a/modules/charon-core/src/test/java/org/wso2/charon3/core/config/encoder/JSONEncoderTest.java b/modules/charon-core/src/test/java/org/wso2/charon3/core/config/encoder/JSONEncoderTest.java new file mode 100644 index 000000000..a8acff007 --- /dev/null +++ b/modules/charon-core/src/test/java/org/wso2/charon3/core/config/encoder/JSONEncoderTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. 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.wso2.charon3.core.config.encoder; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mockito.MockedStatic; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.charon3.core.encoder.JSONEncoder; +import org.wso2.charon3.core.schema.SCIMConstants; +import org.wso2.charon3.core.schema.SCIMResourceSchemaManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.wso2.charon3.core.schema.SCIMConstants.ENTERPRISE_USER_SCHEMA_URI; +import static org.wso2.charon3.core.schema.SCIMConstants.SYSTEM_USER_SCHEMA_URI; + +public class JSONEncoderTest { + + private JSONEncoder jsonEncoder; + private SCIMResourceSchemaManager resourceSchemaManager; + private static MockedStatic resourceSchemaManagerMock; + + @BeforeClass + public void setUp() { + + jsonEncoder = new JSONEncoder(); + resourceSchemaManager = mock(SCIMResourceSchemaManager.class); + resourceSchemaManagerMock = mockStatic(SCIMResourceSchemaManager.class); + resourceSchemaManagerMock.when(SCIMResourceSchemaManager::getInstance).thenReturn(resourceSchemaManager); + } + + @AfterClass + public void tearDown() { + + resourceSchemaManagerMock.close(); + } + + @Test + public void testBuildUserResourceTypeJsonBody() throws JSONException { + + when(resourceSchemaManager.isExtensionSet()).thenReturn(true); + when(resourceSchemaManager.getExtensionURI()).thenReturn(ENTERPRISE_USER_SCHEMA_URI); + when(resourceSchemaManager.getExtensionRequired()).thenReturn(true); + when(resourceSchemaManager.getSystemSchemaExtensionURI()).thenReturn(SYSTEM_USER_SCHEMA_URI); + when(resourceSchemaManager.getSystemSchemaExtensionRequired()).thenReturn(true); + when(resourceSchemaManager.getCustomSchemaExtensionURI()).thenReturn("customSchemaURI"); + + String jsonBody = jsonEncoder.buildUserResourceTypeJsonBody(); + + JSONObject jsonObject = new JSONObject(jsonBody); + + assertEquals(jsonObject.getString(SCIMConstants.ResourceTypeSchemaConstants.ID), SCIMConstants.USER); + assertEquals(jsonObject.getString(SCIMConstants.ResourceTypeSchemaConstants.NAME), SCIMConstants.USER); + assertEquals(jsonObject.getString(SCIMConstants.ResourceTypeSchemaConstants.ENDPOINT), + SCIMConstants.USER_ENDPOINT); + assertEquals(jsonObject.getString(SCIMConstants.ResourceTypeSchemaConstants.DESCRIPTION), + SCIMConstants.ResourceTypeSchemaConstants.USER_ACCOUNT); + assertEquals(jsonObject.getString(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA), + SCIMConstants.USER_CORE_SCHEMA_URI); + + JSONArray schemaExtensions = jsonObject.getJSONArray( + SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS); + assertEquals(schemaExtensions.length(), 3, "There should be three schema extensions."); + + JSONObject extensionSchema = schemaExtensions.getJSONObject(0); + assertEquals(extensionSchema.getString(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_SCHEMA), + ENTERPRISE_USER_SCHEMA_URI); + assertTrue(extensionSchema.getBoolean(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_REQUIRED)); + + JSONObject systemSchema = schemaExtensions.getJSONObject(1); + assertEquals(systemSchema.getString(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_SCHEMA), + SYSTEM_USER_SCHEMA_URI); + assertTrue(systemSchema.getBoolean(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_REQUIRED)); + + JSONObject customSchema = schemaExtensions.getJSONObject(2); + assertEquals(customSchema.getString(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_SCHEMA), + "customSchemaURI"); + assertFalse(customSchema.getBoolean(SCIMConstants.ResourceTypeSchemaConstants.SCHEMA_EXTENSIONS_REQUIRED)); + + verify(resourceSchemaManager).isExtensionSet(); + verify(resourceSchemaManager).getExtensionURI(); + verify(resourceSchemaManager).getExtensionRequired(); + verify(resourceSchemaManager).getSystemSchemaExtensionURI(); + verify(resourceSchemaManager).getSystemSchemaExtensionRequired(); + verify(resourceSchemaManager, times(2)).getCustomSchemaExtensionURI(); + } +} diff --git a/modules/charon-core/src/test/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManagerTest.java b/modules/charon-core/src/test/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManagerTest.java new file mode 100644 index 000000000..045068f5c --- /dev/null +++ b/modules/charon-core/src/test/java/org/wso2/charon3/core/schema/SCIMResourceSchemaManagerTest.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. 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.wso2.charon3.core.schema; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.charon3.core.config.SCIMCustomSchemaExtensionBuilder; +import org.wso2.charon3.core.config.SCIMSystemSchemaExtensionBuilder; +import org.wso2.charon3.core.config.SCIMUserSchemaExtensionBuilder; +import org.wso2.charon3.core.exceptions.BadRequestException; +import org.wso2.charon3.core.exceptions.CharonException; +import org.wso2.charon3.core.exceptions.NotImplementedException; +import org.wso2.charon3.core.extensions.UserManager; + +import java.util.List; + +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.wso2.charon3.core.schema.SCIMConstants.ENTERPRISE_USER_SCHEMA_URI; +import static org.wso2.charon3.core.schema.SCIMConstants.SYSTEM_USER_SCHEMA_URI; + +public class SCIMResourceSchemaManagerTest { + + private SCIMResourceSchemaManager manager; + private static MockedStatic userSchemaBuilderMock; + private static MockedStatic systemSchemaBuilderMock; + private static MockedStatic customSchemaBuilderMock; + private SCIMUserSchemaExtensionBuilder userSchemaExtensionBuilder; + private SCIMSystemSchemaExtensionBuilder systemSchemaExtensionBuilder; + private SCIMCustomSchemaExtensionBuilder customSchemaExtensionBuilder; + private AttributeSchema userSchemaExtension; + private AttributeSchema systemSchemaExtension; + + @BeforeMethod + public void setUp() { + + manager = SCIMResourceSchemaManager.getInstance(); + clearInvocations(userSchemaExtensionBuilder, systemSchemaExtensionBuilder); + } + + @Test + public void testGetInstance() { + + SCIMResourceSchemaManager instance1 = SCIMResourceSchemaManager.getInstance(); + SCIMResourceSchemaManager instance2 = SCIMResourceSchemaManager.getInstance(); + + assertNotNull(instance1, "The getInstance method should return a non-null instance."); + assertSame(instance1, instance2, "The getInstance method should always return the same instance."); + } + + @Test + public void testGetUserResourceSchema() { + + userSchemaBuilderMock.when(SCIMUserSchemaExtensionBuilder::getInstance).thenReturn(userSchemaExtensionBuilder); + systemSchemaBuilderMock.when(SCIMSystemSchemaExtensionBuilder::getInstance) + .thenReturn(systemSchemaExtensionBuilder); + + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(userSchemaExtension); + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(systemSchemaExtension); + when(userSchemaExtension.getURI()).thenReturn(ENTERPRISE_USER_SCHEMA_URI); + when(systemSchemaExtension.getURI()).thenReturn(SYSTEM_USER_SCHEMA_URI); + + SCIMResourceTypeSchema schema = manager.getUserResourceSchema(); + + assertNotNull(schema, "The getUserResourceSchema method should return a non-null schema."); + + List schemaURIs = schema.getSchemasList(); + assertTrue(schemaURIs.contains(SCIMConstants.USER_CORE_SCHEMA_URI), "Core schema URI should be present."); + assertTrue(schemaURIs.contains(ENTERPRISE_USER_SCHEMA_URI), "User extension URI should be present."); + assertTrue(schemaURIs.contains(SYSTEM_USER_SCHEMA_URI), "System extension URI should be present."); + + verify(userSchemaExtensionBuilder, times(2)).getExtensionSchema(); + verify(systemSchemaExtensionBuilder).getExtensionSchema(); + } + + @Test + public void testGetUserResourceSchemaWithUserManager() throws BadRequestException, NotImplementedException, + CharonException { + + UserManager userManager = mock(UserManager.class); + AttributeSchema customSchemaExtension = mock(AttributeSchema.class); + + when(userManager.getCustomUserSchemaExtension()).thenReturn(customSchemaExtension); + when(customSchemaExtension.getURI()).thenReturn("customExtensionURI"); + + SCIMResourceTypeSchema schema = manager.getUserResourceSchema(userManager); + + assertNotNull(schema, "The getUserResourceSchema(UserManager) method should return a non-null schema."); + + List schemaURIs = schema.getSchemasList(); + assertTrue(schemaURIs.contains(SCIMConstants.USER_CORE_SCHEMA_URI), "Core schema URI should be present."); + assertTrue(schemaURIs.contains(ENTERPRISE_USER_SCHEMA_URI), "User extension URI should be present."); + assertTrue(schemaURIs.contains(SYSTEM_USER_SCHEMA_URI), "System extension URI should be present."); + assertTrue(schemaURIs.contains("customExtensionURI"), "Custom extension URI should be present."); + + verify(userSchemaExtensionBuilder, times(2)).getExtensionSchema(); + verify(systemSchemaExtensionBuilder).getExtensionSchema(); + verify(userManager).getCustomUserSchemaExtension(); + } + + @Test + public void testIsExtensionSet() { + + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(userSchemaExtension); + Boolean isSet = manager.isExtensionSet(); + assertTrue(isSet, "The isExtensionSet method should return true when the extension is set."); + + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + isSet = manager.isExtensionSet(); + assertFalse(isSet, "The isExtensionSet method should return false when the extension is not set."); + + verify(userSchemaExtensionBuilder, times(2)).getExtensionSchema(); + } + + @Test + public void testGetExtensionName() { + + userSchemaBuilderMock.when(SCIMUserSchemaExtensionBuilder::getInstance).thenReturn(userSchemaExtensionBuilder); + when(userSchemaExtension.getName()).thenReturn(ENTERPRISE_USER_SCHEMA_URI); + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(userSchemaExtension); + String extensionName = manager.getExtensionName(); + assertEquals(extensionName, ENTERPRISE_USER_SCHEMA_URI, + "The getExtensionName method should return the correct name."); + + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + extensionName = manager.getExtensionName(); + assertNull(extensionName, "The getExtensionName method should return null when no extension is set."); + + verify(userSchemaExtensionBuilder, times(2)).getExtensionSchema(); + } + + @Test + public void testGetSystemSchemaExtensionName() { + + systemSchemaBuilderMock.when(SCIMSystemSchemaExtensionBuilder::getInstance) + .thenReturn(systemSchemaExtensionBuilder); + when(systemSchemaExtension.getName()).thenReturn(SYSTEM_USER_SCHEMA_URI); + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(systemSchemaExtension); + String extensionName = manager.getSystemSchemaExtensionName(); + assertEquals(extensionName, SYSTEM_USER_SCHEMA_URI, + "The getSystemSchemaExtensionName method should return the correct name."); + + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + extensionName = manager.getSystemSchemaExtensionName(); + assertNull(extensionName, + "The getSystemSchemaExtensionName method should return null when no system extension is set."); + + verify(systemSchemaExtensionBuilder, times(2)).getExtensionSchema(); + } + + @Test + public void testGetCustomSchemaExtensionURI() { + + customSchemaBuilderMock = mockStatic(SCIMCustomSchemaExtensionBuilder.class); + customSchemaExtensionBuilder = mock(SCIMCustomSchemaExtensionBuilder.class); + + customSchemaBuilderMock.when(SCIMCustomSchemaExtensionBuilder::getInstance) + .thenReturn(customSchemaExtensionBuilder); + when(customSchemaExtensionBuilder.getURI()).thenReturn("customSchemaURI"); + + String customSchemaURI = manager.getCustomSchemaExtensionURI(); + + assertEquals(customSchemaURI, "customSchemaURI", + "The getCustomSchemaExtensionURI method should return the correct URI."); + verify(customSchemaExtensionBuilder).getURI(); + customSchemaBuilderMock.close(); + } + + @Test + public void testGetExtensionURI() { + + when(userSchemaExtension.getURI()).thenReturn(ENTERPRISE_USER_SCHEMA_URI); + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(userSchemaExtension); + String extensionURI = manager.getExtensionURI(); + assertEquals(extensionURI, ENTERPRISE_USER_SCHEMA_URI, + "The getExtensionURI method should return the correct URI."); + + // When extension schema is not available + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + extensionURI = manager.getExtensionURI(); + assertNull(extensionURI, "The getExtensionURI method should return null when no extension is set."); + + verify(userSchemaExtensionBuilder, times(2)).getExtensionSchema(); + } + + @Test + public void testGetSystemSchemaExtensionURI() { + + when(systemSchemaExtension.getURI()).thenReturn(SYSTEM_USER_SCHEMA_URI); + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(systemSchemaExtension); + String systemExtensionURI = manager.getSystemSchemaExtensionURI(); + assertEquals(systemExtensionURI, SYSTEM_USER_SCHEMA_URI, + "The getSystemSchemaExtensionURI method should return the correct URI."); + + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + systemExtensionURI = manager.getSystemSchemaExtensionURI(); + assertNull(systemExtensionURI, + "The getSystemSchemaExtensionURI method should return null when no system extension is set."); + + verify(systemSchemaExtensionBuilder, times(2)).getExtensionSchema(); + } + + @Test + public void testGetSystemSchemaExtensionRequired() { + + when(systemSchemaExtension.getRequired()).thenReturn(true); + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(systemSchemaExtension); + boolean isRequired = manager.getSystemSchemaExtensionRequired(); + assertTrue(isRequired, + "The getSystemSchemaExtensionRequired method should return true when the extension is required."); + + when(systemSchemaExtension.getRequired()).thenReturn(false); + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(systemSchemaExtension); + isRequired = manager.getSystemSchemaExtensionRequired(); + assertFalse(isRequired, + "The getSystemSchemaExtensionRequired method should return false when the extension is not required."); + + when(systemSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + isRequired = manager.getSystemSchemaExtensionRequired(); + assertFalse(isRequired, + "The getSystemSchemaExtensionRequired method should return false when no system extension is set."); + + verify(systemSchemaExtensionBuilder, times(3)).getExtensionSchema(); + } + + @Test + public void testGetExtensionRequired() { + + when(userSchemaExtension.getRequired()).thenReturn(true); + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(userSchemaExtension); + boolean isRequired = manager.getExtensionRequired(); + assertTrue(isRequired, "The getExtensionRequired method should return true when the extension is required."); + + when(userSchemaExtension.getRequired()).thenReturn(false); + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(userSchemaExtension); + isRequired = manager.getExtensionRequired(); + assertFalse(isRequired, + "The getExtensionRequired method should return false when the extension is not required."); + + when(userSchemaExtensionBuilder.getExtensionSchema()).thenReturn(null); + isRequired = manager.getExtensionRequired(); + assertFalse(isRequired, "The getExtensionRequired method should return false when no extension is set."); + + verify(userSchemaExtensionBuilder, times(3)).getExtensionSchema(); + } + + @BeforeClass + public void initMocks() { + + userSchemaBuilderMock = mockStatic(SCIMUserSchemaExtensionBuilder.class); + systemSchemaBuilderMock = mockStatic(SCIMSystemSchemaExtensionBuilder.class); + + userSchemaExtensionBuilder = mock(SCIMUserSchemaExtensionBuilder.class); + systemSchemaExtensionBuilder = mock(SCIMSystemSchemaExtensionBuilder.class); + userSchemaExtension = mock(AttributeSchema.class); + systemSchemaExtension = mock(AttributeSchema.class); + } + + @AfterClass + static void closeMocks() { + + userSchemaBuilderMock.close(); + systemSchemaBuilderMock.close(); + } +} diff --git a/modules/charon-core/src/test/resources/scim2-schema-extension-test.config b/modules/charon-core/src/test/resources/scim2-schema-extension-test.config new file mode 100644 index 000000000..400d1da3b --- /dev/null +++ b/modules/charon-core/src/test/resources/scim2-schema-extension-test.config @@ -0,0 +1,58 @@ +[ +{"attributeURI": "urn:scim:wso2:schema:testURI1", +"attributeName": "testURI1", +"dataType": "string", +"multiValued": false, +"description": "Test description 1", +"required": "true", +"caseExact": "false", +"mutability": "readWrite", +"returned": "default", +"uniqueness": "none", +"subAttributes": "null", +"canonicalValues": ["value1", "value2"], +"referenceTypes": ["refType1", "refType2"] +}, +{"attributeURI": "urn:scim:wso2:schema:testURI2", +"attributeName": "testURI2", +"dataType": "integer", +"multiValued": true, +"description": "Test description 2", +"required": "false", +"caseExact": "true", +"mutability": "immutable", +"returned": "always", +"uniqueness": "global", +"subAttributes": "null", +"canonicalValues": ["value3", "value4"], +"referenceTypes": ["refType3", "refType4"] +}, +{"attributeURI": "testURI3", +"attributeName": "testName3", +"dataType": "integer", +"multiValued": true, +"description": "Test description 3", +"required": "false", +"caseExact": "false", +"mutability": "readWrite", +"returned": "default", +"uniqueness": "global", +"subAttributes": "sub5", +"canonicalValues": [], +"referenceTypes": [] +}, +{"attributeURI": "urn:scim:wso2:schema", +"attributeName": "urn:scim:wso2:schema", +"dataType": "complex", +"multiValued": false, +"description": "System Schema", +"required": "true", +"caseExact": "false", +"mutability": "readWrite", +"returned": "default", +"uniqueness": "none", +"subAttributes": "testURI1 testURI2", +"canonicalValues": [], +"referenceTypes": [] +} +] diff --git a/modules/charon-core/src/test/resources/testng.xml b/modules/charon-core/src/test/resources/testng.xml index a7a87babd..4d13e84e0 100644 --- a/modules/charon-core/src/test/resources/testng.xml +++ b/modules/charon-core/src/test/resources/testng.xml @@ -31,6 +31,9 @@ + + +