From dac7805221663ad053ba92e2f69e3520553555b4 Mon Sep 17 00:00:00 2001 From: RovinKYK Date: Wed, 28 Jan 2026 20:10:48 +0530 Subject: [PATCH 1/3] Add file-based import and export endpoints for OIDC scopes --- .../management/common/OidcScopeConstants.java | 13 +- .../oidc/scope/management/v1/OidcApi.java | 65 ++++++- .../scope/management/v1/OidcApiService.java | 8 + .../scope/management/v1/model/FileUpload.java | 102 +++++++++++ .../oidc/scope/management/v1/model/Scope.java | 12 +- .../v1/core/OidcScopeManagementService.java | 167 ++++++++++++++++++ .../v1/impl/OidcApiServiceImpl.java | 37 ++++ .../main/resources/oidc-scope-management.yaml | 159 +++++++++++++++++ 8 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/FileUpload.java diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java index 47a001f552..90e025383f 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java @@ -30,7 +30,18 @@ public enum ErrorMessage { INVALID_REQUEST("OAUTH-60001", "Invalid Request."), ERROR_CONFLICT_REQUEST("41004", "Scope already exists."), - SCOPE_NOT_FOUND("41003", "Scope not found."); + SCOPE_NOT_FOUND("41003", "Scope not found."), + + // Server Errors - 650xx + ERROR_CODE_ERROR_EXPORTING_SCOPE("65001", + "Unable to export the OIDC scope configurations."), + ERROR_CODE_ERROR_IMPORTING_SCOPE("65002", + "Unable to import the OIDC scope configurations."), + ERROR_CODE_ERROR_UPDATING_SCOPE("65003", + "Unable to update the OIDC scope configurations."), + + // Client Errors - 600xx + ERROR_CODE_INVALID_INPUT("60001", "Invalid Input"); private final String code; private final String message; diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApi.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApi.java index e0cde5e890..f3e27eb427 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApi.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApi.java @@ -18,16 +18,22 @@ package org.wso2.carbon.identity.api.server.oidc.scope.management.v1; -import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.factories.OidcApiServiceFactory; +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.apache.cxf.jaxrs.ext.multipart.Multipart; +import java.io.InputStream; + import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ErrorResponse; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.Scope; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ScopeUpdateRequest; +import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.factories.OidcApiServiceFactory; import javax.validation.Valid; import javax.ws.rs.*; import javax.ws.rs.core.Response; import io.swagger.annotations.*; +import javax.validation.constraints.*; + @Path("/oidc") @Api(description = "The oidc API") @@ -76,6 +82,25 @@ public Response deleteScope(@ApiParam(value = "scope name as the id",required=tr return delegate.deleteScope(id ); } + @Valid + @GET + @Path("/scopes/{id}/export") + + @Produces({ "application/json", "application/xml", "application/yaml" }) + @ApiOperation(value = "Export an OIDC Scope in XML, YAML, or JSON format", notes = "", response = String.class, tags={ "OIDC Scope Endpoint", }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Successful response", response = String.class), + @ApiResponse(code = 400, message = "Bad Request.", response = ErrorResponse.class), + @ApiResponse(code = 401, message = "Unauthorized.", response = ErrorResponse.class), + @ApiResponse(code = 403, message = "Resource Forbidden.", response = ErrorResponse.class), + @ApiResponse(code = 404, message = "Resource Not Found.", response = ErrorResponse.class), + @ApiResponse(code = 500, message = "Internal Server Error.", response = ErrorResponse.class) + }) + public Response exportScopeToFile(@ApiParam(value = "scope name as the id",required=true) @PathParam("id") String id, @Valid @ApiParam(value = "Content type of the file. " , allowableValues="application/json, application/xml, application/yaml, application/x-yaml, text/yaml, text/xml, text/json", defaultValue="application/yaml")@HeaderParam("Accept") String accept) { + + return delegate.exportScopeToFile(id, accept ); + } + @Valid @GET @Path("/scopes/{id}") @@ -113,6 +138,25 @@ public Response getScopes() { return delegate.getScopes(); } + @Valid + @POST + @Path("/scopes/import") + @Consumes({ "multipart/form-data" }) + @Produces({ "application/json" }) + @ApiOperation(value = "Import an OIDC Scope from XML, YAML, or JSON file", notes = "", response = Void.class, tags={ "OIDC Scope Endpoint", }) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Successfully imported", response = Void.class), + @ApiResponse(code = 400, message = "Invalid input request.", response = ErrorResponse.class), + @ApiResponse(code = 401, message = "Unauthorized.", response = ErrorResponse.class), + @ApiResponse(code = 403, message = "Resource Forbidden.", response = ErrorResponse.class), + @ApiResponse(code = 409, message = "Scope already Exists.", response = ErrorResponse.class), + @ApiResponse(code = 500, message = "Internal Server Error.", response = ErrorResponse.class) + }) + public Response importScopeFromFile(@Multipart(value = "file") InputStream fileInputStream,@Multipart(value = "file" ) Attachment fileDetail) { + + return delegate.importScopeFromFile(fileInputStream, fileDetail ); + } + @Valid @PUT @Path("/scopes/{id}") @@ -132,4 +176,23 @@ public Response updateScope(@ApiParam(value = "scope name as the id",required=tr return delegate.updateScope(id, scopeUpdateRequest ); } + @Valid + @PUT + @Path("/scopes/{id}/import") + @Consumes({ "multipart/form-data" }) + @Produces({ "application/json" }) + @ApiOperation(value = "Update an OIDC Scope from XML, YAML, or JSON file", notes = "", response = Void.class, tags={ "OIDC Scope Endpoint" }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Successfully Updated.", response = Void.class), + @ApiResponse(code = 400, message = "Invalid input request.", response = ErrorResponse.class), + @ApiResponse(code = 401, message = "Unauthorized.", response = ErrorResponse.class), + @ApiResponse(code = 403, message = "Resource Forbidden.", response = ErrorResponse.class), + @ApiResponse(code = 404, message = "Resource Not Found.", response = ErrorResponse.class), + @ApiResponse(code = 500, message = "Internal Server Error.", response = ErrorResponse.class) + }) + public Response updateScopeFromFile(@ApiParam(value = "scope name as the ID",required=true) @PathParam("id") String id, @Multipart(value = "file") InputStream fileInputStream,@Multipart(value = "file" ) Attachment fileDetail) { + + return delegate.updateScopeFromFile(id, fileInputStream, fileDetail ); + } + } diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApiService.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApiService.java index a24ff3ea4e..1d99658910 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApiService.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/OidcApiService.java @@ -21,7 +21,9 @@ import org.apache.cxf.jaxrs.ext.multipart.Attachment; import org.apache.cxf.jaxrs.ext.multipart.Multipart; import java.io.InputStream; +import java.util.List; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ErrorResponse; +import java.io.File; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.Scope; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ScopeUpdateRequest; import javax.ws.rs.core.Response; @@ -33,9 +35,15 @@ public interface OidcApiService { public Response deleteScope(String id); + public Response exportScopeToFile(String id, String accept); + public Response getScope(String id); public Response getScopes(); + public Response importScopeFromFile(InputStream fileInputStream, Attachment fileDetail); + public Response updateScope(String id, ScopeUpdateRequest scopeUpdateRequest); + + public Response updateScopeFromFile(String id, InputStream fileInputStream, Attachment fileDetail); } diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/FileUpload.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/FileUpload.java new file mode 100644 index 0000000000..9c5ed492a7 --- /dev/null +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/FileUpload.java @@ -0,0 +1,102 @@ +/* + * 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.carbon.identity.api.server.oidc.scope.management.v1.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.io.File; +import javax.validation.constraints.*; + + +import io.swagger.annotations.*; +import java.util.Objects; +import javax.validation.Valid; +import javax.xml.bind.annotation.*; + +public class FileUpload { + + private File file; + + /** + * File uploaded during import. + **/ + public FileUpload file(File file) { + + this.file = file; + return this; + } + + @ApiModelProperty(required = true, value = "File uploaded during import.") + @JsonProperty("file") + @Valid + @NotNull(message = "Property file cannot be null.") + + public File getFile() { + return file; + } + public void setFile(File file) { + this.file = file; + } + + + + @Override + public boolean equals(java.lang.Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FileUpload fileUpload = (FileUpload) o; + return Objects.equals(this.file, fileUpload.file); + } + + @Override + public int hashCode() { + return Objects.hash(file); + } + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("class FileUpload {\n"); + + sb.append(" file: ").append(toIndentedString(file)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n"); + } +} + diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java index cc837afd45..c8fc747a9e 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java @@ -30,11 +30,21 @@ import javax.validation.Valid; import javax.xml.bind.annotation.*; +@XmlRootElement(name = "Scope") +@XmlAccessorType(XmlAccessType.FIELD) public class Scope { - + + @XmlElement(name = "name") private String name; + + @XmlElement(name = "displayName") private String displayName; + + @XmlElement(name = "description") private String description; + + @XmlElementWrapper(name = "claims") + @XmlElement(name = "claim") private List claims = new ArrayList<>(); diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java index 37289d03a1..6900f36661 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java @@ -18,10 +18,18 @@ package org.wso2.carbon.identity.api.server.oidc.scope.management.v1.core; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.cxf.jaxrs.ext.multipart.Attachment; import org.wso2.carbon.identity.api.server.common.error.APIError; import org.wso2.carbon.identity.api.server.common.error.ErrorResponse; +import org.wso2.carbon.identity.api.server.common.file.FileContent; +import org.wso2.carbon.identity.api.server.common.file.FileSerializationConfig; +import org.wso2.carbon.identity.api.server.common.file.FileSerializationException; +import org.wso2.carbon.identity.api.server.common.file.FileSerializationUtil; +import org.wso2.carbon.identity.api.server.common.file.YamlConfig; import org.wso2.carbon.identity.api.server.oidc.scope.management.common.OidcScopeConstants; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.Scope; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ScopeUpdateRequest; @@ -29,7 +37,11 @@ import org.wso2.carbon.identity.oauth.IdentityOAuthClientException; import org.wso2.carbon.identity.oauth.OAuthAdminServiceImpl; import org.wso2.carbon.identity.oauth.dto.ScopeDTO; +import org.yaml.snakeyaml.DumperOptions; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -138,6 +150,149 @@ public void updateScope(String id, ScopeUpdateRequest scopeUpdateObject) { } } + /** + * Export an OIDC scope identified by the scopeName, in the given format. + * + * @param scopeName Scope name. + * @param fileType File type (Accept header value). + * @return FileContent with scope data. + */ + public FileContent exportScopeToFile(String scopeName, String fileType) { + + if (LOG.isDebugEnabled()) { + LOG.debug("Exporting OIDC scope from the scope name: " + scopeName); + } + if (StringUtils.isBlank(fileType)) { + throw new UnsupportedOperationException("No valid media type found"); + } + + Scope scope = getScope(scopeName); + + if (scope == null) { + throw handleException(Response.Status.NOT_FOUND, + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_SCOPE, scopeName); + } + + FileContent fileContent; + try { + fileContent = generateFileFromModel(fileType, scope); + } catch (IdentityOAuthAdminException e) { + throw handleException(Response.Status.INTERNAL_SERVER_ERROR, + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_SCOPE, scopeName); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Successfully exported OIDC scope: %s as a file type of %s.", + scope.getName(), fileType)); + } + return fileContent; + } + + /** + * Create a new OIDC scope by importing an YAML, JSON or XML configuration file. + * + * @param fileInputStream File to be imported as an input stream. + * @param fileDetail File details. + * @return Scope name of the imported scope. + */ + public String importScopeFromFile(InputStream fileInputStream, Attachment fileDetail) { + + Scope scope; + try { + scope = getScopeFromFile(fileInputStream, fileDetail); + } catch (IdentityOAuthClientException e) { + throw handleException(Response.Status.BAD_REQUEST, + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_IMPORTING_SCOPE, null); + } + + return addScope(scope); + } + + /** + * Update an existing OIDC scope from an YAML, JSON or XML configuration file. + * + * @param scopeName Scope name to update. + * @param fileInputStream File to be imported as an input stream. + * @param fileDetail File details. + * @return Scope name of the updated scope. + */ + public String updateScopeFromFile(String scopeName, InputStream fileInputStream, Attachment fileDetail) { + + Scope scope; + try { + scope = getScopeFromFile(fileInputStream, fileDetail); + } catch (IdentityOAuthClientException e) { + throw handleException(Response.Status.BAD_REQUEST, + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_UPDATING_SCOPE, null); + } + + ScopeUpdateRequest updateRequest = new ScopeUpdateRequest(); + updateRequest.setDisplayName(scope.getDisplayName()); + updateRequest.setDescription(scope.getDescription()); + updateRequest.setClaims(scope.getClaims()); + + updateScope(scopeName, updateRequest); + return scopeName; + } + + private FileContent generateFileFromModel(String fileType, Scope scope) + throws IdentityOAuthAdminException { + + if (LOG.isDebugEnabled()) { + LOG.debug("Parsing OIDC scope object to file content of type: " + fileType); + } + + FileSerializationConfig config = new FileSerializationConfig(); + YamlConfig yamlConfig = new YamlConfig(); + yamlConfig.setDumperOptionsCustomizer(options -> { + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + }); + config.setYamlConfig(yamlConfig); + + try { + return FileSerializationUtil.serialize(scope, scope.getName(), fileType, config); + } catch (FileSerializationException e) { + throw new IdentityOAuthAdminException("Error when parsing OIDC scope to file.", e); + } + } + + private Scope getScopeFromFile(InputStream fileInputStream, Attachment fileDetail) + throws IdentityOAuthClientException { + + Scope scope; + try { + FileContent scopeFileContent = new FileContent(fileDetail.getDataHandler().getName(), + fileDetail.getDataHandler().getContentType(), + IOUtils.toString(fileInputStream, StandardCharsets.UTF_8.name())); + scope = generateModelFromFile(scopeFileContent); + } catch (IOException | IdentityOAuthClientException e) { + throw new IdentityOAuthClientException("Provided input file is not in the correct format", e); + } finally { + IOUtils.closeQuietly(fileInputStream); + } + + return scope; + } + + private Scope generateModelFromFile(FileContent fileContent) + throws IdentityOAuthClientException { + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Parsing OIDC scope from file: %s of type: %s.", fileContent.getFileName(), + fileContent.getFileType())); + } + if (StringUtils.isEmpty(fileContent.getContent())) { + throw new IdentityOAuthClientException(String.format( + "Empty Identity Provider configuration file %s uploaded.", fileContent.getFileName())); + } + + try { + return FileSerializationUtil.deserialize(fileContent, Scope.class); + } catch (FileSerializationException e) { + throw new IdentityOAuthClientException("Error when generating the OIDC scope model from file", e); + } + } + /** * Build scope list. * @@ -198,4 +353,16 @@ private APIError handleException(IdentityOAuthAdminException e, String message) } return new APIError(status, errorResponse); } + + private APIError handleException(Response.Status status, OidcScopeConstants.ErrorMessage errorMessage, + String data) { + + ErrorResponse errorResponse = new ErrorResponse.Builder() + .withCode(errorMessage.getCode()) + .withMessage(errorMessage.getMessage()) + .withDescription(data != null ? String.format("%s: %s", errorMessage.getMessage(), data) : + errorMessage.getMessage()) + .build(LOG, errorMessage.getMessage()); + return new APIError(status, errorResponse); + } } diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/impl/OidcApiServiceImpl.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/impl/OidcApiServiceImpl.java index fe7391dcb8..0ab0909793 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/impl/OidcApiServiceImpl.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/impl/OidcApiServiceImpl.java @@ -18,8 +18,11 @@ package org.wso2.carbon.identity.api.server.oidc.scope.management.v1.impl; +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.apache.http.HttpHeaders; import org.wso2.carbon.identity.api.server.common.Constants; import org.wso2.carbon.identity.api.server.common.ContextLoader; +import org.wso2.carbon.identity.api.server.common.file.FileContent; import org.wso2.carbon.identity.api.server.oidc.scope.management.common.OidcScopeConstants; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.OidcApiService; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.core.OidcScopeManagementService; @@ -27,7 +30,9 @@ import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.Scope; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ScopeUpdateRequest; +import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import javax.ws.rs.core.Response; @@ -61,6 +66,22 @@ public Response deleteScope(String id) { return Response.noContent().build(); } + @Override + public Response exportScopeToFile(String id, String accept) { + + FileContent fileContent = oidcScopeManagementService.exportScopeToFile(id, accept); + + return Response.ok() + .type(fileContent.getFileType()) + .header("Content-Disposition", "attachment; filename=\"" + + fileContent.getFileName() + "\"") + .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .header(HttpHeaders.PRAGMA, "no-cache") + .header(HttpHeaders.EXPIRES, "0") + .entity(fileContent.getContent().getBytes(StandardCharsets.UTF_8)) + .build(); + } + @Override public Response getScope(String id) { @@ -73,6 +94,13 @@ public Response getScopes() { return Response.ok().entity(oidcScopeManagementService.getScopes()).build(); } + @Override + public Response importScopeFromFile(InputStream fileInputStream, Attachment fileDetail) { + + String scopeName = oidcScopeManagementService.importScopeFromFile(fileInputStream, fileDetail); + return Response.created(getResourceLocation(scopeName)).build(); + } + @Override public Response updateScope(String id, ScopeUpdateRequest scopeUpdateRequest) { @@ -80,6 +108,15 @@ public Response updateScope(String id, ScopeUpdateRequest scopeUpdateRequest) { return Response.ok().build(); } + @Override + public Response updateScopeFromFile(String id, InputStream fileInputStream, + Attachment fileDetail) { + + String resourceId = + oidcScopeManagementService.updateScopeFromFile(id, fileInputStream, fileDetail); + return Response.ok().location(getResourceLocation(resourceId)).build(); + } + private URI getResourceLocation(String resourceId) { return ContextLoader.buildURIForHeader(Constants.V1_API_PATH_COMPONENT + diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/resources/oidc-scope-management.yaml b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/resources/oidc-scope-management.yaml index a69c2c8d3e..ae149ad22f 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/resources/oidc-scope-management.yaml +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/resources/oidc-scope-management.yaml @@ -90,6 +90,45 @@ paths: $ref: '#/components/responses/Conflict' 500: $ref: '#/components/responses/ServerError' + /oidc/scopes/import: + post: + tags: + - OIDC Scope Endpoint + summary: Import an OIDC Scope from XML, YAML, or JSON file + operationId: importScopeFromFile + description: This API provides the capability to import an OIDC scope from a file in XML, YAML, or JSON format.
+ Permission required:
+ * /permission/admin/manage/identity/applicationmgt/create
+ Scope required:
+ * internal_application_mgt_create + security: + - BasicAuth: [ ] + - OAuth2: [ ] + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/FileUpload' + description: OIDC scope configuration file + required: true + responses: + 201: + description: Successfully imported + headers: + Location: + description: Location of the newly created scope + schema: + type: string + 400: + $ref: '#/components/responses/InvalidInput' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 500: + $ref: '#/components/responses/ServerError' /oidc/scopes/{id}: get: tags: @@ -196,6 +235,117 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' + /oidc/scopes/{id}/export: + get: + tags: + - OIDC Scope Endpoint + summary: Export an OIDC Scope in XML, YAML, or JSON format + operationId: exportScopeToFile + description: This API provides the capability to retrieve an OIDC scope for a given scope name as an XML, YAML, or JSON file.
+ Permission required:
+ * /permission/admin/manage/identity/applicationmgt/view
+ Scope required:
+ * internal_application_mgt_view + security: + - BasicAuth: [] + - OAuth2: [] + parameters: + - name: id + in: path + description: scope name as the ID + required: true + schema: + type: string + example: scope1 + - name: Accept + in: header + required: false + description: | + Content type of the file. + schema: + type: string + enum: + - application/json + - application/xml + - application/yaml + - application/x-yaml + - text/yaml + - text/xml + - text/json + default: application/yaml + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: string + example: 'Sample OIDC scope configuration in the requested format' + application/xml: + schema: + type: string + example: 'Sample OIDC scope configuration in the requested format' + application/yaml: + schema: + type: string + example: 'Sample OIDC scope configuration in the requested format' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/ServerError' + /oidc/scopes/{id}/import: + put: + tags: + - OIDC Scope Endpoint + summary: Update an OIDC Scope from XML, YAML, or JSON file + operationId: updateScopeFromFile + description: This API provides the capability to update an existing OIDC scope from a file in XML, YAML, or JSON format.
+ Permission required:
+ * /permission/admin/manage/identity/applicationmgt/update
+ Scope required:
+ * internal_application_mgt_update + security: + - BasicAuth: [] + - OAuth2: [] + parameters: + - name: id + in: path + description: scope name as the ID + required: true + schema: + type: string + example: scope1 + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/FileUpload' + description: OIDC scope configuration file + required: true + responses: + 200: + description: Successfully Updated. + headers: + Location: + description: Location of the updated scope + schema: + type: string + 400: + $ref: '#/components/responses/InvalidInput' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/ServerError' components: responses: @@ -301,3 +451,12 @@ components: traceId: type: string example: 'e0fbcfeb-3617-43c4-8dd0-7b7d38e13047' + FileUpload: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: File uploaded during import. From f86751c6d74755613cc702ffedf40a0928600916 Mon Sep 17 00:00:00 2001 From: RovinKYK Date: Thu, 29 Jan 2026 11:00:21 +0530 Subject: [PATCH 2/3] Add separate OIDC ScopeConfiguration model to fix JSON/XML serialization conflict --- .../oidc/scope/management/v1/model/Scope.java | 11 -- .../v1/core/OidcScopeManagementService.java | 10 +- .../v1/model/ScopeConfiguration.java | 111 ++++++++++++++++++ 3 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/ScopeConfiguration.java diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java index c8fc747a9e..392022f208 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/gen/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/Scope.java @@ -28,23 +28,12 @@ import io.swagger.annotations.*; import java.util.Objects; import javax.validation.Valid; -import javax.xml.bind.annotation.*; -@XmlRootElement(name = "Scope") -@XmlAccessorType(XmlAccessType.FIELD) public class Scope { - @XmlElement(name = "name") private String name; - - @XmlElement(name = "displayName") private String displayName; - - @XmlElement(name = "description") private String description; - - @XmlElementWrapper(name = "claims") - @XmlElement(name = "claim") private List claims = new ArrayList<>(); diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java index 6900f36661..0b2041dc27 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java @@ -32,6 +32,7 @@ import org.wso2.carbon.identity.api.server.common.file.YamlConfig; import org.wso2.carbon.identity.api.server.oidc.scope.management.common.OidcScopeConstants; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.Scope; +import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ScopeConfiguration; import org.wso2.carbon.identity.api.server.oidc.scope.management.v1.model.ScopeUpdateRequest; import org.wso2.carbon.identity.oauth.IdentityOAuthAdminException; import org.wso2.carbon.identity.oauth.IdentityOAuthClientException; @@ -242,6 +243,8 @@ private FileContent generateFileFromModel(String fileType, Scope scope) LOG.debug("Parsing OIDC scope object to file content of type: " + fileType); } + ScopeConfiguration scopeConfig = new ScopeConfiguration(scope); + FileSerializationConfig config = new FileSerializationConfig(); YamlConfig yamlConfig = new YamlConfig(); yamlConfig.setDumperOptionsCustomizer(options -> { @@ -250,7 +253,7 @@ private FileContent generateFileFromModel(String fileType, Scope scope) config.setYamlConfig(yamlConfig); try { - return FileSerializationUtil.serialize(scope, scope.getName(), fileType, config); + return FileSerializationUtil.serialize(scopeConfig, scope.getName(), fileType, config); } catch (FileSerializationException e) { throw new IdentityOAuthAdminException("Error when parsing OIDC scope to file.", e); } @@ -283,11 +286,12 @@ private Scope generateModelFromFile(FileContent fileContent) } if (StringUtils.isEmpty(fileContent.getContent())) { throw new IdentityOAuthClientException(String.format( - "Empty Identity Provider configuration file %s uploaded.", fileContent.getFileName())); + "Empty OIDC scope configuration file %s uploaded.", fileContent.getFileName())); } try { - return FileSerializationUtil.deserialize(fileContent, Scope.class); + ScopeConfiguration scopeConfig = FileSerializationUtil.deserialize(fileContent, ScopeConfiguration.class); + return scopeConfig.toScope(); } catch (FileSerializationException e) { throw new IdentityOAuthClientException("Error when generating the OIDC scope model from file", e); } diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/ScopeConfiguration.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/ScopeConfiguration.java new file mode 100644 index 0000000000..a2a7c24249 --- /dev/null +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/model/ScopeConfiguration.java @@ -0,0 +1,111 @@ +/* + * 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.carbon.identity.api.server.oidc.scope.management.v1.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * OIDC Scope configuration model for file serialization (XML, YAML, JSON export/import). + */ +@XmlRootElement(name = "Scope") +@XmlAccessorType(XmlAccessType.FIELD) +public class ScopeConfiguration { + + @XmlElement(name = "name") + private String name; + + @XmlElement(name = "displayName") + private String displayName; + + @XmlElement(name = "description") + private String description; + + @XmlElementWrapper(name = "claims") + @XmlElement(name = "claim") + private List claims = new ArrayList<>(); + + public ScopeConfiguration() { + } + + /** + * Constructor to create ScopeConfiguration from Scope model. + * + * @param scope Scope object. + */ + public ScopeConfiguration(Scope scope) { + if (scope != null) { + this.name = scope.getName(); + this.displayName = scope.getDisplayName(); + this.description = scope.getDescription(); + this.claims = scope.getClaims() != null ? new ArrayList<>(scope.getClaims()) : new ArrayList<>(); + } + } + + /** + * Convert ScopeConfiguration to Scope model. + * + * @return Scope object. + */ + public Scope toScope() { + Scope scope = new Scope(); + scope.setName(this.name); + scope.setDisplayName(this.displayName); + scope.setDescription(this.description); + scope.setClaims(this.claims != null ? new ArrayList<>(this.claims) : new ArrayList<>()); + return scope; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getClaims() { + return claims; + } + + public void setClaims(List claims) { + this.claims = claims; + } +} From f4760f9bfb5e9cb42c6c549c88e027b5602141b9 Mon Sep 17 00:00:00 2001 From: RovinKYK Date: Tue, 3 Feb 2026 00:43:03 +0530 Subject: [PATCH 3/3] Improve error handling --- .../management/common/OidcScopeConstants.java | 25 ++++++++++++++----- .../v1/core/OidcScopeManagementService.java | 23 +++++++++++------ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java index 90e025383f..108155282d 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.common/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/common/OidcScopeConstants.java @@ -34,17 +34,18 @@ public enum ErrorMessage { // Server Errors - 650xx ERROR_CODE_ERROR_EXPORTING_SCOPE("65001", - "Unable to export the OIDC scope configurations."), + "Unable to export the OIDC scope configurations.", + "Server Encountered an error while exporting the OIDC scope configurations."), ERROR_CODE_ERROR_IMPORTING_SCOPE("65002", - "Unable to import the OIDC scope configurations."), + "Unable to import the OIDC scope configurations.", + "Server Encountered an error while importing the OIDC scope configurations."), ERROR_CODE_ERROR_UPDATING_SCOPE("65003", - "Unable to update the OIDC scope configurations."), - - // Client Errors - 600xx - ERROR_CODE_INVALID_INPUT("60001", "Invalid Input"); + "Unable to update the OIDC scope configurations.", + "Server Encountered an error while updating the OIDC scope configurations."); private final String code; private final String message; + private String description; ErrorMessage(String code, String message) { @@ -52,6 +53,13 @@ public enum ErrorMessage { this.message = message; } + ErrorMessage(String code, String message, String description) { + + this.code = code; + this.message = message; + this.description = description; + } + public String getCode() { return code; @@ -62,6 +70,11 @@ public String getMessage() { return message; } + public String getDescription() { + + return description; + } + @Override public String toString() { diff --git a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java index 0b2041dc27..90fc53dc27 100644 --- a/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java +++ b/components/org.wso2.carbon.identity.api.server.oidc.scope.management/org.wso2.carbon.identity.api.server.oidc.scope.management.v1/src/main/java/org/wso2/carbon/identity/api/server/oidc/scope/management/v1/core/OidcScopeManagementService.java @@ -171,7 +171,7 @@ public FileContent exportScopeToFile(String scopeName, String fileType) { if (scope == null) { throw handleException(Response.Status.NOT_FOUND, - OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_SCOPE, scopeName); + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_SCOPE); } FileContent fileContent; @@ -179,7 +179,7 @@ public FileContent exportScopeToFile(String scopeName, String fileType) { fileContent = generateFileFromModel(fileType, scope); } catch (IdentityOAuthAdminException e) { throw handleException(Response.Status.INTERNAL_SERVER_ERROR, - OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_SCOPE, scopeName); + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_SCOPE, e); } if (LOG.isDebugEnabled()) { @@ -203,7 +203,7 @@ public String importScopeFromFile(InputStream fileInputStream, Attachment fileDe scope = getScopeFromFile(fileInputStream, fileDetail); } catch (IdentityOAuthClientException e) { throw handleException(Response.Status.BAD_REQUEST, - OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_IMPORTING_SCOPE, null); + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_IMPORTING_SCOPE, e); } return addScope(scope); @@ -224,7 +224,7 @@ public String updateScopeFromFile(String scopeName, InputStream fileInputStream, scope = getScopeFromFile(fileInputStream, fileDetail); } catch (IdentityOAuthClientException e) { throw handleException(Response.Status.BAD_REQUEST, - OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_UPDATING_SCOPE, null); + OidcScopeConstants.ErrorMessage.ERROR_CODE_ERROR_UPDATING_SCOPE, e); } ScopeUpdateRequest updateRequest = new ScopeUpdateRequest(); @@ -359,14 +359,23 @@ private APIError handleException(IdentityOAuthAdminException e, String message) } private APIError handleException(Response.Status status, OidcScopeConstants.ErrorMessage errorMessage, - String data) { + IdentityOAuthAdminException e) { ErrorResponse errorResponse = new ErrorResponse.Builder() .withCode(errorMessage.getCode()) .withMessage(errorMessage.getMessage()) - .withDescription(data != null ? String.format("%s: %s", errorMessage.getMessage(), data) : - errorMessage.getMessage()) + .withDescription(e.getMessage()) .build(LOG, errorMessage.getMessage()); return new APIError(status, errorResponse); } + + private APIError handleException(Response.Status status, OidcScopeConstants.ErrorMessage errorMessage) { + + ErrorResponse errorResponse = new ErrorResponse.Builder() + .withCode(errorMessage.getCode()) + .withMessage(errorMessage.getMessage()) + .withDescription(errorMessage.getDescription()) + .build(LOG, errorMessage.getMessage()); + return new APIError(status, errorResponse); + } }