From 392525c19152fcd916e0c61e70c436a484bf391c Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 7 Mar 2023 11:24:04 -0500 Subject: [PATCH] feat(agama): update gama deployment endpoint to support configuration properties (#4049) * docs: update API description #4032 * feat: add configs endpoint #4032 * chore: sync swagger descriptor #4032 * chore: reduce code smells #4032 * chore: reduce code smells #4032 --- docs/admin/developer/agama/gama-deployment.md | 19 +++ .../io/jans/ads/model/ProjectMetadata.java | 14 +++ .../docs/jans-config-api-swagger.yaml | 60 ++++++++-- .../auth/AgamaDeploymentsResource.java | 109 ++++++++++++++++++ 4 files changed, 193 insertions(+), 9 deletions(-) diff --git a/docs/admin/developer/agama/gama-deployment.md b/docs/admin/developer/agama/gama-deployment.md index 43ac67d718d..c78b38977ef 100644 --- a/docs/admin/developer/agama/gama-deployment.md +++ b/docs/admin/developer/agama/gama-deployment.md @@ -108,6 +108,7 @@ Once a `.gama` file is built the deployment process follows. Here is a typical w 1. Send (POST) the archive contents to the deployment endpoint. Normally a 202 response should be obtained meaning a task has been queued for processing 1. Poll (via GET) the status of the deployment. When the archive has been effectively deployed a status of 200 should be obtained. It may take up to 30 seconds for the process to complete once the archive is sent. This time may extend if there is another deployment in course +1. Optionally supply configuration parameters for flows if needed. This is done via PUT to the `/configs` endpoint. The response of the previous step may contain descriptive/sample configurations that can be used as a guide 1. Test the deployed flows and adjust the archive for any changes required 1. Go to point 1 if needed 1. If desired, a request can be sent to undeploy the flows (via DELETE) @@ -161,6 +162,24 @@ The following tables summarize the available endpoints. All URLs are relative to |Status|202 (the task was created and scheduled for deployment), 409 (there is a task already for this project and it hasn't finished yet), 400 (a param is missing)| +|Endpoint -> |`/agama-deployment/configs`| +|-|-| +|Purpose|Retrieve the configurations associated to flows that belong to the project of interest. The project must have been already processed fully| +|Method|GET| +|Query params|`name` (the project's name) - mandatory| +|Output|A JSON object whose properties are flow names and values correspond to configuration properties defined (JSON objects too)| +|Status|200 (successful response), 409 (the project is being deployed currently), 404 (unknown project), 400 (a param is missing)| + + +|Endpoint -> |`/agama-deployment/configs`| +|-|-| +|Purpose|Set or replace the configurations associated to flows that belong to the project of interest. The project must have been already processed fully| +|Method|PUT| +|Query params|`name` (the project's name) - mandatory| +|Output|A JSON object whose properties are flow names and values correspond to a boolean indicating the success of the update for the given flow| +|Status|200 (successful response), 409 (the project is being deployed currently), 404 (unknown project), 400 (a param is missing)| + + |Endpoint -> |`/agama-deployment`| |-|-| |Purpose|Undeploy an ADS project from the server. Entails removing flows and assets initally supplied| diff --git a/jans-auth-server/agama/model/src/main/java/io/jans/ads/model/ProjectMetadata.java b/jans-auth-server/agama/model/src/main/java/io/jans/ads/model/ProjectMetadata.java index 1e138206513..5764625731b 100644 --- a/jans-auth-server/agama/model/src/main/java/io/jans/ads/model/ProjectMetadata.java +++ b/jans-auth-server/agama/model/src/main/java/io/jans/ads/model/ProjectMetadata.java @@ -1,6 +1,9 @@ package io.jans.ads.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) public class ProjectMetadata { @@ -10,6 +13,9 @@ public class ProjectMetadata { private String type; private String description; + @JsonProperty("configs") + private Map configHints; + public String getProjectName() { return projectName; } @@ -42,4 +48,12 @@ public void setDescription(String description) { this.description = description; } + public Map getConfigHints() { + return configHints; + } + + public void setConfigHints(Map configHints) { + this.configHints = configHints; + } + } diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index 54a84fbee6f..a5e952e3f82 100644 --- a/jans-config-api/docs/jans-config-api-swagger.yaml +++ b/jans-config-api/docs/jans-config-api-swagger.yaml @@ -280,6 +280,40 @@ paths: security: - oauth2: - https://jans.io/oauth/config/agama.delete + /api/v1/agama-deployment/configs: + get: + operationId: getConfigs + parameters: + - name: name + in: query + schema: + type: string + responses: + default: + description: default response + content: + application/json: {} + put: + operationId: setConfigs + parameters: + - name: name + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: + type: object + additionalProperties: + type: object + responses: + default: + description: default response + content: + application/json: {} /api/v1/agama-deployment/list: get: tags: @@ -7355,6 +7389,10 @@ components: type: string description: type: string + configs: + type: object + additionalProperties: + type: object PagedResult: type: object properties: @@ -7535,20 +7573,20 @@ components: $ref: '#/components/schemas/AttributeValidation' tooltip: type: string - adminCanEdit: - type: boolean - userCanView: + adminCanAccess: type: boolean adminCanView: type: boolean - userCanEdit: + adminCanEdit: type: boolean userCanAccess: type: boolean - adminCanAccess: + userCanView: type: boolean whitePagesCanView: type: boolean + userCanEdit: + type: boolean baseDn: type: string PatchRequest: @@ -7899,6 +7937,8 @@ components: type: boolean returnClientSecretOnRead: type: boolean + rotateClientRegistrationAccessTokenOnUsage: + type: boolean rejectJwtWithNoneAlg: type: boolean expirationNotificatorEnabled: @@ -7953,6 +7993,8 @@ components: type: boolean disablePromptLogin: type: boolean + disablePromptConsent: + type: boolean sessionIdLifetime: type: integer format: int32 @@ -8268,6 +8310,8 @@ components: type: object additionalProperties: type: string + fapi: + type: boolean allResponseTypesSupported: uniqueItems: true type: array @@ -8277,8 +8321,6 @@ components: - code - token - id_token - fapi: - type: boolean AuthenticationFilter: required: - baseDn @@ -8915,10 +8957,10 @@ components: type: array items: type: object - displayValue: - type: string value: type: object + displayValue: + type: string LocalizedString: type: object properties: diff --git a/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AgamaDeploymentsResource.java b/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AgamaDeploymentsResource.java index c22f71ab22b..1273b9536a0 100644 --- a/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AgamaDeploymentsResource.java +++ b/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AgamaDeploymentsResource.java @@ -1,6 +1,11 @@ package io.jans.configapi.rest.resource.auth; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.jans.ads.model.Deployment; +import io.jans.agama.model.Flow; +import io.jans.as.model.util.Pair; import io.jans.orm.model.PagedResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -13,12 +18,19 @@ import io.jans.configapi.util.ApiAccessConstants; import io.jans.configapi.util.ApiConstants; import io.jans.configapi.service.auth.AgamaDeploymentsService; +import io.jans.configapi.service.auth.AgamaFlowService; +import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.HashMap; + @Path(ApiConstants.AGAMA_DEPLOYMENTS) @Produces(MediaType.APPLICATION_JSON) public class AgamaDeploymentsResource extends ConfigBaseResource { @@ -26,6 +38,11 @@ public class AgamaDeploymentsResource extends ConfigBaseResource { @Inject private AgamaDeploymentsService ads; + @Inject + private AgamaFlowService flowService; + + private ObjectMapper mapper; + @Operation(summary = "Retrieve the list of projects deployed currently.", description = "Retrieve the list of projects deployed currently.", operationId = "get-agama-dev-prj", tags = { "Agama - Developer Studio" }, security = @SecurityRequirement(name = "oauth2", scopes = { ApiAccessConstants.AGAMA_READ_ACCESS, ApiAccessConstants.AGAMA_WRITE_ACCESS, @@ -148,4 +165,96 @@ public Response undeploy(@QueryParam("name") String projectName) { } + @GET + @Path("configs") + @ProtectedApi(scopes = { ApiAccessConstants.AGAMA_READ_ACCESS }, groupScopes = { + ApiAccessConstants.AGAMA_WRITE_ACCESS }, superScopes = { ApiAccessConstants.SUPER_ADMIN_READ_ACCESS }) + public Response getConfigs(@QueryParam("name") String projectName) throws JsonProcessingException { + + Pair> pair = projectFlows(projectName); + Response resp = pair.getFirst(); + if (resp != null) return resp; + + Map> configs = new HashMap<>(); + + for (String qname : pair.getSecond()) { + Map config = Optional.ofNullable(flowService.getFlowByName(qname)) + .map(f -> f.getMetadata().getProperties()).orElse(null); + + if (config == null) { + logger.warn("Flow {} does not exist or has no configuration properties defined", qname); + } else { + logger.debug("Adding flow properties of {}", qname); + configs.put(qname, config); + } + } + //Use own mapper so any empty maps that may be found inside flows configurations are not ignored + return Response.ok(mapper.writeValueAsString(configs)).build(); + + } + + @PUT + @Path("configs") + @Consumes(MediaType.APPLICATION_JSON) + @ProtectedApi(scopes = { ApiAccessConstants.AGAMA_WRITE_ACCESS }, + superScopes = { ApiAccessConstants.SUPER_ADMIN_WRITE_ACCESS }) + public Response setConfigs(@QueryParam("name") String projectName, + Map> flowsConfigs) { + + if (flowsConfigs == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Mapping of flows vs. configs not provided").build(); + } + + Pair> pair = projectFlows(projectName); + Response resp = pair.getFirst(); + if (resp != null) return resp; + + Set flowIds = pair.getSecond(); + Map results = new HashMap<>(); + + for (String qname : flowsConfigs.keySet()) { + if (qname != null && flowIds.contains(qname)) { + + Flow flow = flowService.getFlowByName(qname); + boolean success = false; + + if (flow == null) { + logger.warn("Unable to retrieve flow {}", qname); + } else { + try { + flow.getMetadata().setProperties(flowsConfigs.get(qname)); + flowService.updateFlow(flow); + success = true; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + results.put(qname, success); + + } else if (logger.isWarnEnabled()) { + logger.warn("Flow {} is not part of project {}, config ignored", qname, + projectName.replaceAll("[\n\r]", "_")); + } + } + return Response.ok(results).build(); + + } + + private Pair> projectFlows(String projectName) { + + Response res = getDeployment(projectName); + if (res.getStatus() != Response.Status.OK.getStatusCode()) return new Pair<>(res, null); + + Deployment d = (Deployment) res.getEntity(); + //Retrieve the flows this project contains + return new Pair<>(null, d.getDetails().getFlowsError().keySet()); + + } + + @PostConstruct + private void init() { + mapper = new ObjectMapper(); + } + } \ No newline at end of file