@@ -83,6 +88,17 @@
src/test/resources
true
+
+
+ **/*.cjar
+
+
+
+ src/test/resources
+ false
+
+ **/*.cjar
+
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/assembly/assembly.xml b/jans-config-api/plugins/admin-ui-plugin/src/main/assembly/assembly.xml
index 2d4d732de45..16bd260ccec 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/main/assembly/assembly.xml
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/assembly/assembly.xml
@@ -1,11 +1,22 @@
-
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
distribution
jar
false
+
+
+ true
+ /
+ false
+
+ org.apache.tika:tika-core
+
+ runtime
+
+
${project.build.directory}/classes
@@ -15,4 +26,4 @@
-
\ No newline at end of file
+
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/AdminUIPolicyStore.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/AdminUIPolicyStore.java
index 22b8acaf962..acd2b038708 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/AdminUIPolicyStore.java
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/AdminUIPolicyStore.java
@@ -1,74 +1,147 @@
package io.jans.ca.plugin.adminui.model.adminui;
-import io.jans.service.document.store.model.Document;
+import com.fasterxml.jackson.annotation.JsonView;
+import io.jans.ca.plugin.adminui.model.adminui.PolicyStoreViews.Create;
+import io.jans.ca.plugin.adminui.model.adminui.PolicyStoreViews.Edit;
+import io.jans.orm.annotation.AttributeName;
+import io.jans.orm.annotation.DN;
+import io.jans.orm.annotation.DataEntry;
+import io.jans.orm.annotation.ObjectClass;
import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotNull;
-import jakarta.ws.rs.FormParam;
-import jakarta.ws.rs.core.MediaType;
-import org.jboss.resteasy.annotations.providers.multipart.PartType;
-import java.io.InputStream;
-
-public class AdminUIPolicyStore {
-
- @NotNull
- @Valid
- @FormParam("document")
- @PartType(MediaType.APPLICATION_JSON)
- private Document document;
-
- @NotNull
- @FormParam("policyStore")
- @PartType(MediaType.APPLICATION_OCTET_STREAM)
- @Schema(implementation = String.class, format="binary")
- private InputStream policyStore;
-
- /**
- * Retrieve the uploaded policy store binary stream.
- *
- * @return the `policyStore` InputStream containing the binary content submitted via the multipart form field "policyStore"
- */
- public InputStream getPolicyStore() {
- return policyStore;
- }
+import java.util.Date;
- /**
- * Assigns the input stream that contains the binary policy store.
- *
- * @param policyStore the input stream for the policy store data
- */
- public void setPolicyStore(InputStream policyStore) {
- this.policyStore = policyStore;
- }
+ @DataEntry(sortBy = {"jansStatus"})
+ @ObjectClass(value = "adminUIPolicyStore")
+ public class AdminUIPolicyStore {
+ // Read-only on edit: settable only when creating the policy store.
+ @DN
+ @JsonView(Create.class)
+ @Schema(description = "Distinguished Name (DN) of the policy store entry. Server-generated on create; read-only and ignored on edit.", accessMode = Schema.AccessMode.READ_ONLY, example = "inum=e1a2b3c4-1234-5678-9abc-def012345678,ou=policy-stores,ou=admin-ui,o=jans")
+ private String dn;
+ @AttributeName(
+ ignoreDuringUpdate = true
+ )
+ @JsonView(Create.class)
+ @Schema(description = "Unique identifier (inum) of the policy store. Server-generated on create; read-only and ignored on edit.", accessMode = Schema.AccessMode.READ_ONLY, example = "e1a2b3c4-1234-5678-9abc-def012345678")
+ private String inum;
+ // Editable metadata: writable on both create and edit.
+ @AttributeName(name = "displayname")
+ @JsonView({Create.class, Edit.class})
+ @Schema(description = "Human-readable display name of the policy store.", example = "Admin UI Cedarling Policy Store")
+ private String displayname;
+ @AttributeName(name = "description")
+ @JsonView({Create.class, Edit.class})
+ @Schema(description = "Free-text description of the policy store and its purpose.", example = "Cedarling policy store used to derive Admin UI role-to-scope mappings")
+ private String description;
+ // Read-only on edit: the policy store document itself is immutable once uploaded.
+ @AttributeName(name = "document")
+ @JsonView(Create.class)
+ @Schema(description = "Base64-encoded Cedar policy store archive (.cjar zip). Required on create; immutable and ignored on edit.", example = "UEsDBBQACAgIAABb...==")
+ private String policyStore;
+ // Read-only on edit: owner cannot be reassigned via edit.
+ @AttributeName(name = "jansUsrDN")
+ @JsonView(Create.class)
+ @Schema(description = "DN of the user who owns the policy store. Set at create time; cannot be reassigned via edit.", example = "inum=abc123,ou=people,o=jans")
+ private String jansUsrDN;
+ // Editable: status may be changed on both create and edit.
+ @AttributeName(name = "jansStatus")
+ @JsonView({Create.class, Edit.class})
+ @Schema(description = "Status of the policy store. Only one store should be 'active' at a time; newly created stores default to 'inactive'.", allowableValues = {"active", "inactive"}, example = "active")
+ private String jansStatus;
+ // Read-only on edit: creation timestamp is fixed at create time.
+ @AttributeName(name = "creationDate")
+ @JsonView(Create.class)
+ @Schema(description = "Timestamp when the policy store was created. Server-managed and fixed at create time.", accessMode = Schema.AccessMode.READ_ONLY, example = "2026-07-14T10:15:30.000Z")
+ private Date creationDate = new Date();
+ // Server-managed: overwritten on every create/edit, so any client-supplied value is ignored.
+ @AttributeName(name = "jansLastUpd")
+ @Schema(description = "Timestamp of the last update. Server-managed and overwritten on every create/edit; any client-supplied value is ignored.", accessMode = Schema.AccessMode.READ_ONLY, example = "2026-07-14T11:20:45.000Z")
+ private Date jansLastUpd;
- /**
- * Retrieves the JSON `Document` supplied in the multipart form field named "document".
- *
- * @return the `Document` parsed from the multipart "document" field
- */
- public Document getDocument() {
- return document;
- }
+ public String getDn() {
+ return dn;
+ }
- /**
- * Assigns the JSON document corresponding to the multipart form field named "document".
- *
- * @param document the JSON `Document` bound from the multipart form part named "document"
- */
- public void setDocument(Document document) {
- this.document = document;
- }
+ public void setDn(String dn) {
+ this.dn = dn;
+ }
+
+ public String getInum() {
+ return inum;
+ }
+
+ public void setInum(String inum) {
+ this.inum = inum;
+ }
+
+ 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 String getPolicyStore() {
+ return policyStore;
+ }
+
+ public void setPolicyStore(String policyStore) {
+ this.policyStore = policyStore;
+ }
+
+ public String getJansUsrDN() {
+ return jansUsrDN;
+ }
+
+ public void setJansUsrDN(String jansUsrDN) {
+ this.jansUsrDN = jansUsrDN;
+ }
+
+ public String getJansStatus() {
+ return jansStatus;
+ }
+
+ public void setJansStatus(String jansStatus) {
+ this.jansStatus = jansStatus;
+ }
+
+ public Date getCreationDate() {
+ return creationDate;
+ }
+
+ public void setCreationDate(Date creationDate) {
+ this.creationDate = creationDate;
+ }
+
+ public Date getJansLastUpd() {
+ return jansLastUpd;
+ }
+
+ public void setJansLastUpd(Date jansLastUpd) {
+ this.jansLastUpd = jansLastUpd;
+ }
- /**
- * Produce a string representation of this AdminUIPolicyStore that includes the `policyStore` field.
- *
- * @return a string containing the class name and the `policyStore` value
- */
- @Override
- public String toString() {
- return "AdminUIPolicyStore{" +
- "document=" + document +
- ", policyStore=" + policyStore +
- '}';
+ @Override
+ public String toString() {
+ return "AdminUIPolicyStore{" +
+ "dn='" + dn + '\'' +
+ ", inum='" + inum + '\'' +
+ ", displayname='" + displayname + '\'' +
+ ", description='" + description + '\'' +
+ ", policyStore='" + policyStore + '\'' +
+ ", jansUsrDN='" + jansUsrDN + '\'' +
+ ", jansStatus='" + jansStatus + '\'' +
+ ", creationDate=" + creationDate +
+ ", jansLastUpd=" + jansLastUpd +
+ '}';
+ }
}
-}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/PolicyStoreViews.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/PolicyStoreViews.java
new file mode 100644
index 00000000000..ec386c153a1
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/adminui/PolicyStoreViews.java
@@ -0,0 +1,28 @@
+package io.jans.ca.plugin.adminui.model.adminui;
+
+/**
+ * Jackson {@code @JsonView} marker interfaces used to control which
+ * {@link AdminUIPolicyStore} fields may be supplied by clients on different
+ * REST operations.
+ *
+ *
+ * - {@link Create} - fields writable on POST (create).
+ * - {@link Edit} - fields writable on PUT (edit). Read-only fields
+ * (e.g. {@code inum}, {@code dn}, {@code creationDate},
+ * {@code jansUsrDN}, {@code policyStore}) are intentionally excluded
+ * from this view so they cannot be modified once created.
+ *
+ */
+public final class PolicyStoreViews {
+
+ private PolicyStoreViews() {
+ }
+
+ /** Fields accepted when creating a policy store (POST). */
+ public interface Create {
+ }
+
+ /** Fields accepted when editing an existing policy store (PUT). */
+ public interface Edit {
+ }
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/rest/adminui/AdminUISecurityResource.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/rest/adminui/AdminUISecurityResource.java
index cd566839889..89a2154a9b4 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/rest/adminui/AdminUISecurityResource.java
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/rest/adminui/AdminUISecurityResource.java
@@ -1,31 +1,43 @@
package io.jans.ca.plugin.adminui.rest.adminui;
+import com.fasterxml.jackson.annotation.JsonView;
import io.jans.ca.plugin.adminui.model.adminui.AdminUIPolicyStore;
+import io.jans.ca.plugin.adminui.model.adminui.PolicyStoreViews;
import io.jans.ca.plugin.adminui.model.auth.GenericResponse;
import io.jans.ca.plugin.adminui.model.exception.ApplicationException;
import io.jans.ca.plugin.adminui.service.adminui.AdminUISecurityService;
import io.jans.ca.plugin.adminui.utils.AppConstants;
import io.jans.ca.plugin.adminui.utils.CommonUtils;
import io.jans.ca.plugin.adminui.utils.ErrorResponse;
+import io.jans.configapi.core.rest.BaseResource;
import io.jans.configapi.core.rest.ProtectedApi;
+import io.jans.configapi.util.ApiConstants;
+import io.jans.model.SearchRequest;
+import io.jans.orm.model.PagedResult;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.inject.Inject;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
import org.slf4j.Logger;
+import static io.jans.as.model.util.Util.escapeLog;
+
@Path("/admin-ui/security")
-public class AdminUISecurityResource {
+public class AdminUISecurityResource extends BaseResource {
static final String POLICY_STORE = "policyStore";
+ public static final String POLICY_STORE_INUM = "/{INUM}";
static final String SYNC_ROLE_SCOPES_MAPPING = "/syncRoleScopesMapping";
static final String SECURITY_READ = "https://jans.io/oauth/jans-auth-server/config/adminui/security.readonly";
static final String SECURITY_WRITE = "https://jans.io/oauth/jans-auth-server/config/adminui/security.write";
@@ -37,19 +49,26 @@ public class AdminUISecurityResource {
AdminUISecurityService adminUISecurityService;
/**
- * Retrieve the Admin UI policy store.
+ * Searches Admin UI Cedarling policy stores.
*
- * On success returns a response whose entity is a GenericResponse containing the policy store
- * payload. On error returns a response whose entity is a GenericResponse with error details and
- * an appropriate HTTP status code.
+ *
Builds a paginated {@link SearchRequest} from the supplied query parameters and
+ * delegates the lookup to {@link AdminUISecurityService}. Results are ordered and
+ * paged according to the request parameters.
*
- * @return a Response whose entity is a GenericResponse with the policy store on success or error details on failure
+ * @param limit maximum number of results to return
+ * @param pattern substring pattern used to filter results (empty matches all)
+ * @param startIndex 1-based index of the first result to return
+ * @param sortBy attribute used to order the results (defaults to inum)
+ * @param sortOrder sort direction ("ascending" or "descending")
+ * @param fieldValuePair comma-separated field/value pairs used for exact-match filtering
+ * @return HTTP 200 with the matching page of policy stores, or an error response
+ * (400/500) wrapped in a {@link GenericResponse} on failure
*/
@Operation(summary = "Get Admin UI policy store", description = "Get Admin UI policy store", operationId = "get-adminui-policy-store", tags = {
"Admin UI - Cedarling"}, security = @SecurityRequirement(name = "oauth2", scopes = {
SECURITY_READ}))
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Get Admin UI policy store")))),
+ @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AdminUIPolicyStore.class, description = "Get Admin UI policy store")), examples = @ExampleObject(name = "Response json example", value = "example/adminui/security/get-all-policy-store-response.json"))),
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Bad Request"))),
@ApiResponse(responseCode = "401", description = "Unauthorized"),
@ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "InternalServerError")))})
@@ -57,20 +76,33 @@ public class AdminUISecurityResource {
@Path(POLICY_STORE)
@Produces(MediaType.APPLICATION_JSON)
@ProtectedApi(scopes = {SECURITY_READ}, groupScopes = {SECURITY_WRITE}, superScopes = {AppConstants.SCOPE_ADMINUI_WRITE})
- public Response getPolicyStore() {
+ public Response getPolicyStore(
+ @Parameter(description = "Search size - max size of the results to return") @DefaultValue(ApiConstants.DEFAULT_LIST_SIZE) @QueryParam(value = ApiConstants.LIMIT) int limit,
+ @Parameter(description = "Search pattern") @DefaultValue("") @QueryParam(value = ApiConstants.PATTERN) String pattern,
+ @Parameter(description = "The 1-based index of the first query result") @DefaultValue(ApiConstants.DEFAULT_LIST_START_INDEX) @QueryParam(value = ApiConstants.START_INDEX) int startIndex,
+ @Parameter(description = "Attribute whose value will be used to order the returned response") @DefaultValue(AppConstants.INUM) @QueryParam(value = ApiConstants.SORT_BY) String sortBy,
+ @Parameter(description = "Order in which the sortBy param is applied. Allowed values are \"ascending\" and \"descending\"") @DefaultValue(ApiConstants.ASCENDING) @QueryParam(value = ApiConstants.SORT_ORDER) String sortOrder,
+ @Parameter(description = "Field and value pair for seraching", examples = @ExampleObject(name = "Field value example", value = "scopeType=spontaneous,defaultScope=true")) @DefaultValue("") @QueryParam(value = ApiConstants.FIELD_VALUE_PAIR) String fieldValuePair
+ ) {
try {
- log.info("Get Admin UI policy store.");
- GenericResponse response = adminUISecurityService.getPolicyStore();
+ if (log.isInfoEnabled()) {
+ log.info("Policy Store search param - limit:{}, pattern:{}, startIndex:{}, sortBy:{}, sortOrder:{}, fieldValuePair:{}",
+ escapeLog(limit), escapeLog(pattern), escapeLog(startIndex), escapeLog(sortBy),
+ escapeLog(sortOrder), escapeLog(fieldValuePair));
+ }
+ SearchRequest searchReq = createSearchRequest(AppConstants.POLICY_STORE_DN, pattern, sortBy, sortOrder,
+ startIndex, limit, null, null, adminUISecurityService.getRecordMaxCount(), fieldValuePair, AdminUIPolicyStore.class);
log.info("Policy Store received.");
- return Response.ok(response).build();
+ return Response.ok(this.doSearch(searchReq)).build();
+
} catch (ApplicationException e) {
- log.error(ErrorResponse.GET_ADMIUI_CONFIG_ERROR.getDescription(), e);
+ log.error(ErrorResponse.POLICY_STORE_GET_ERROR.getDescription(), e);
return Response
.status(e.getErrorCode())
.entity(CommonUtils.createGenericResponse(false, e.getErrorCode(), e.getMessage()))
.build();
} catch (Exception e) {
- log.error(ErrorResponse.GET_ADMIUI_CONFIG_ERROR.getDescription(), e);
+ log.error(ErrorResponse.POLICY_STORE_GET_ERROR.getDescription(), e);
return Response
.serverError()
.entity(CommonUtils.createGenericResponse(false, 500, e.getMessage()))
@@ -79,32 +111,34 @@ public Response getPolicyStore() {
}
/**
- * Upload an Admin UI policy store provided as a multipart form.
+ * Creates a new Admin UI Cedarling policy store.
*
- * @param adminUIPolicyStore the multipart form representing the policy store to upload
- * @return a JAX-RS Response whose entity is a GenericResponse describing the operation result;
- * on success the GenericResponse indicates the upload completed, on error it contains
- * an error code and message
+ * The request body is deserialized using the {@link PolicyStoreViews.Create} view, so
+ * only creation-time fields are accepted; the store is validated and persisted with a
+ * server-generated inum and an {@code inactive} status.
+ *
+ * @param adminUIPolicyStore the policy store to create (must include the base64 policy-store document)
+ * @return HTTP 200 with a {@link GenericResponse} describing the outcome, or an error
+ * response (400/500) on validation or persistence failure
*/
- @Operation(summary = "Upload Admin UI Policy Store", description = "Upload Admin UI Policy Store", operationId = "upload-adminui-policy-store", tags = {
+ @Operation(summary = "Create Admin UI Policy Store", description = "Create Admin UI Policy Store", operationId = "create-adminui-policy-store", tags = {
"Admin UI - Cedarling"}, security = @SecurityRequirement(name = "oauth2", scopes = {
SECURITY_WRITE}))
- @RequestBody(description = "String multipart form.", content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = AdminUIPolicyStore.class)))
+ @RequestBody(description = "Policy Store Object", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = AdminUIPolicyStore.class), examples = @ExampleObject(name = "Request json example", value = "example/adminui/security/create-policy-store-request.json")))
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Upload Admin UI policy store")))),
+ @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Create Admin UI policy store")), examples = @ExampleObject(name = "Response json example", value = "example/adminui/security/create-policy-store-response.json"))),
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Bad Request"))),
@ApiResponse(responseCode = "401", description = "Unauthorized"),
@ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "InternalServerError")))})
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- @PUT
+ @POST
@Path(POLICY_STORE)
@Produces(MediaType.APPLICATION_JSON)
@ProtectedApi(scopes = {SECURITY_WRITE}, superScopes = {AppConstants.SCOPE_ADMINUI_WRITE})
- public Response uploadPolicyStore(@MultipartForm AdminUIPolicyStore adminUIPolicyStore) {
+ public Response createPolicyStore(@JsonView(PolicyStoreViews.Create.class) @Valid @NotNull AdminUIPolicyStore adminUIPolicyStore) {
try {
- log.info("Uploading Admin UI Policy Store.");
+ log.info("Create Admin UI Policy Store.");
GenericResponse response = adminUISecurityService.uploadPolicyStore(adminUIPolicyStore);
- log.info("Successfully uploaded Admin UI Policy Store");
+ log.info("Successfully created Admin UI Policy Store");
return Response.ok(response).build();
} catch (ApplicationException e) {
log.error(ErrorResponse.POLICY_STORE_UPLOAD_ERROR.getDescription(), e);
@@ -121,11 +155,113 @@ public Response uploadPolicyStore(@MultipartForm AdminUIPolicyStore adminUIPolic
}
}
+ /**
+ * Edits an existing Admin UI Cedarling policy store.
+ *
+ * The request body is deserialized using the {@link PolicyStoreViews.Edit} view, so only
+ * editable fields (display name, description, status) are applied; read-only fields are
+ * preserved from persistence. Activating a store demotes any other active store to keep a
+ * single active policy store.
+ *
+ * @param inum the inum of the policy store to edit
+ * @param adminUIPolicyStore the editable fields to apply
+ * @return HTTP 200 with a {@link GenericResponse} describing the outcome, or an error
+ * response (400/404/500) if the request is invalid, the store is missing, or update fails
+ */
+ @Operation(summary = "Edit Admin UI Policy Store", description = "Edit Admin UI Policy Store", operationId = "edit-adminui-policy-store", tags = {
+ "Admin UI - Cedarling"}, security = @SecurityRequirement(name = "oauth2", scopes = {
+ SECURITY_WRITE}))
+ @RequestBody(description = "Policy Store Object", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = AdminUIPolicyStore.class), examples = @ExampleObject(name = "Request json example", value = "example/adminui/security/edit-policy-store-request.json")))
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Edit Admin UI policy store")), examples = @ExampleObject(name = "Response json example", value = "example/adminui/security/edit-policy-store-response.json"))),
+ @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Bad Request"))),
+ @ApiResponse(responseCode = "401", description = "Unauthorized"),
+ @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Not Found"))),
+ @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "InternalServerError")))})
+ @PUT
+ @Path(POLICY_STORE + POLICY_STORE_INUM)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ProtectedApi(scopes = {SECURITY_WRITE}, superScopes = {AppConstants.SCOPE_ADMINUI_WRITE})
+ public Response editPolicyStore(
+ @Parameter(description = "Policy store inum") @PathParam("INUM") @NotNull String inum,
+ @JsonView(PolicyStoreViews.Edit.class) @Valid @NotNull AdminUIPolicyStore adminUIPolicyStore) {
+ try {
+ log.info("Edit Admin UI Policy Store: {}", escapeLog(inum));
+ GenericResponse response = adminUISecurityService.editPolicyStore(inum, adminUIPolicyStore);
+ log.info("Successfully edited Admin UI Policy Store");
+ return Response.ok(response).build();
+ } catch (ApplicationException e) {
+ log.error(ErrorResponse.POLICY_STORE_UPDATE_ERROR.getDescription(), e);
+ return Response
+ .status(e.getErrorCode())
+ .entity(CommonUtils.createGenericResponse(false, e.getErrorCode(), e.getMessage()))
+ .build();
+ } catch (Exception e) {
+ log.error(ErrorResponse.POLICY_STORE_UPDATE_ERROR.getDescription(), e);
+ return Response
+ .serverError()
+ .entity(CommonUtils.createGenericResponse(false, 500, e.getMessage()))
+ .build();
+ }
+ }
+
+ /**
+ * Deletes an Admin UI Cedarling policy store by its inum.
+ *
+ * @param inum the inum of the policy store to delete
+ * @return HTTP 200 with a {@link GenericResponse} on success, or an error response
+ * (400/404/500) if the inum is blank, the store is missing, or deletion fails
+ */
+ @Operation(summary = "Delete Admin UI Policy Store", description = "Delete Admin UI Policy Store", operationId = "delete-adminui-policy-store", tags = {
+ "Admin UI - Cedarling"}, security = @SecurityRequirement(name = "oauth2", scopes = {
+ SECURITY_WRITE}))
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Delete Admin UI policy store")), examples = @ExampleObject(name = "Response json example", value = "example/adminui/security/delete-policy-store-response.json"))),
+ @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Bad Request"))),
+ @ApiResponse(responseCode = "401", description = "Unauthorized"),
+ @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Not Found"))),
+ @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "InternalServerError")))})
+ @DELETE
+ @Path(POLICY_STORE + POLICY_STORE_INUM)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ProtectedApi(scopes = {SECURITY_WRITE}, superScopes = {AppConstants.SCOPE_ADMINUI_WRITE})
+ public Response deletePolicyStore(
+ @Parameter(description = "Policy store inum") @PathParam("INUM") @NotNull String inum) {
+ try {
+ log.info("Delete Admin UI Policy Store: {}", escapeLog(inum));
+ GenericResponse response = adminUISecurityService.deletePolicyStore(inum);
+ log.info("Successfully deleted Admin UI Policy Store");
+ return Response.ok(response).build();
+ } catch (ApplicationException e) {
+ log.error(ErrorResponse.POLICY_STORE_DELETE_ERROR.getDescription(), e);
+ return Response
+ .status(e.getErrorCode())
+ .entity(CommonUtils.createGenericResponse(false, e.getErrorCode(), e.getMessage()))
+ .build();
+ } catch (Exception e) {
+ log.error(ErrorResponse.POLICY_STORE_DELETE_ERROR.getDescription(), e);
+ return Response
+ .serverError()
+ .entity(CommonUtils.createGenericResponse(false, 500, e.getMessage()))
+ .build();
+ }
+ }
+
+ /**
+ * Synchronizes Admin UI role-to-scope mappings from the active policy store.
+ *
+ * Delegates to {@link AdminUISecurityService#syncRoleScopeMapping()}, which parses the
+ * active Cedar policy store and refreshes Admin UI roles and role-permission mappings so
+ * that Admin UI access control stays consistent with the Cedar authorization policies.
+ *
+ * @return HTTP 200 with a {@link GenericResponse} on success, or an error response
+ * (400/500) if synchronization fails
+ */
@Operation(summary = "Sync role-to-scope mappings from the policy store", description = "Sync the role-to-scope mappings from the policy store. If a remote policy store URL is configured and enabled, the mappings will be generated from the remote policy store; otherwise, they will be generated from the default policy store.", operationId = "sync-role-to-scopes-mappings", tags = {
"Admin UI - Cedarling"}, security = @SecurityRequirement(name = "oauth2", scopes = {
SECURITY_WRITE}))
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Sync Role-to-Scopes mapping from policy-store")))),
+ @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = GenericResponse.class, description = "Sync Role-to-Scopes mapping from policy-store")), examples = @ExampleObject(name = "Response json example", value = "example/adminui/security/sync-role-scopes-mapping-response.json"))),
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "Bad Request"))),
@ApiResponse(responseCode = "401", description = "Unauthorized"),
@ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = GenericResponse.class, description = "InternalServerError")))})
@@ -153,4 +289,30 @@ public Response syncRoleScopeMapping() {
.build();
}
}
+
+ /**
+ * Executes a policy-store search against the service and logs the paged result.
+ *
+ * @param searchReq the fully built search request
+ * @return the {@link PagedResult} of matching policy stores (may be {@code null} if none)
+ * @throws ApplicationException if the underlying search fails
+ */
+ private PagedResult doSearch(SearchRequest searchReq) throws ApplicationException {
+ if (log.isDebugEnabled()) {
+ log.debug("Policy Store search params - searchReq:{} ", searchReq);
+ }
+
+ PagedResult pagedResult = adminUISecurityService.searchPolicyStores(searchReq);
+ if (log.isTraceEnabled()) {
+ log.trace("PagedResult - pagedResult:{}", pagedResult);
+ }
+
+ if (pagedResult != null) {
+ log.debug(
+ "Policy Store fetched - pagedResult.getTotalEntriesCount():{}, pagedResult.getEntriesCount():{}, pagedResult.getEntries():{}",
+ pagedResult.getTotalEntriesCount(), pagedResult.getEntriesCount(), pagedResult.getEntries());
+ }
+ log.debug("Policy Store - pagedResult:{}", pagedResult);
+ return pagedResult;
+ }
}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/adminui/AdminUISecurityService.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/adminui/AdminUISecurityService.java
index 0f7c534372f..6c52b2fc735 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/adminui/AdminUISecurityService.java
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/adminui/AdminUISecurityService.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Strings;
+import io.jans.as.common.util.AttributeConstants;
import io.jans.as.model.config.adminui.AdminRole;
import io.jans.as.model.config.adminui.RolePermissionMapping;
import io.jans.ca.plugin.adminui.model.adminui.AdminUIPolicyStore;
@@ -13,13 +14,19 @@
import io.jans.ca.plugin.adminui.utils.CommonUtils;
import io.jans.ca.plugin.adminui.utils.ErrorResponse;
import io.jans.ca.plugin.adminui.utils.security.PolicyToScopeMapper;
+import io.jans.configapi.configuration.ConfigurationFactory;
import io.jans.configapi.core.model.adminui.AUIConfiguration;
+import io.jans.configapi.util.ApiConstants;
+import io.jans.model.SearchRequest;
import io.jans.orm.PersistenceEntryManager;
+import io.jans.orm.model.PagedResult;
+import io.jans.orm.model.SortOrder;
import io.jans.orm.search.filter.Filter;
-import io.jans.service.document.store.model.Document;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.Response;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.tika.Tika;
import org.json.JSONObject;
import org.slf4j.Logger;
@@ -32,28 +39,28 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
+import static io.jans.ca.plugin.adminui.utils.AppConstants.INUM;
+import static io.jans.ca.plugin.adminui.utils.AppConstants.POLICY_STORE_DN;
+
/**
* Service responsible for managing Admin UI security related operations such as
- * retrieving, uploading, synchronizing and updating the Cedarling policy store.
+ * searching, uploading, editing, deleting and synchronizing the Cedarling policy store.
*
* This service interacts with:
*
- * - Local policy store files
- * - Remote policy store endpoints
- * - Jans persistence layer
+ * - The Jans persistence layer, where policy stores are persisted (the Cedar
+ * archive is held as a base64-encoded document)
* - Admin UI role and permission configuration
*
*
- * It also supports synchronization between Cedar policy definitions and
- * Admin UI role-to-scope mappings.
+ * It also synchronizes Admin UI role-to-scope mappings from the active policy
+ * store held in persistence.
*/
@Singleton
@@ -68,47 +75,53 @@ public class AdminUISecurityService {
@Inject
AUIConfigurationService auiConfigurationService;
+ @Inject
+ ConfigurationFactory configurationFactory;
+
@Inject
AdminUIService adminUIService;
@Inject
PolicyToScopeMapper policyToScopeMapper;
+ Tika tika = new Tika();
+
private static final long MAX_ENTRY_SIZE = 3_000_000; // 3 MB
private static final String TRUSTED_ISSUER_FILE = "trusted-issuers/GluuFlexAdminUI.json";
/**
- * Retrieves the current policy store from the configured local file system path.
- *
- * The policy store path is resolved using the following precedence:
- *
- * - Configured value in {@link AUIConfiguration#getAuiCedarlingDefaultPolicyStorePath()}
- * - {@link AppConstants#DEFAULT_POLICY_STORE_FILE_PATH}
- *
+ * Searches persisted policy stores matching the given request.
*
- * If the file exists, the method returns the binary content of the policy store
- * (typically a .cjar archive). If the file does not exist, a 404 response is returned.
+ * Each filter-assertion value is matched (as a substring) against the inum, status and
+ * display name, combined with OR semantics, and the results are paged and sorted according
+ * to the request.
*
- * @return {@link GenericResponse} containing the policy store file as a byte array
- * @throws ApplicationException if an unexpected error occurs while retrieving the file
+ * @param searchRequest the search parameters (filters, paging and sort options)
+ * @return a {@link PagedResult} of matching policy stores
+ * @throws ApplicationException (HTTP 500) if the underlying persistence query fails
*/
- public GenericResponse getPolicyStore() throws ApplicationException {
+ public PagedResult searchPolicyStores(SearchRequest searchRequest) throws ApplicationException {
try {
- AUIConfiguration auiConfiguration = auiConfigurationService.getAUIConfiguration();
- // Resolve path for local policy store file
- String policyStorePath = Optional.ofNullable(auiConfiguration.getAuiCedarlingDefaultPolicyStorePath())
- .filter(path -> !Strings.isNullOrEmpty(path))
- .orElse(AppConstants.DEFAULT_POLICY_STORE_FILE_PATH);
- Path path = Paths.get(policyStorePath);
- if (!Files.exists(path)) {
- throw new ApplicationException(Response.Status.NOT_FOUND.getStatusCode(), "Policy store not found.");
+ Filter searchFilter = null;
+ List filters = new ArrayList<>();
+ if (searchRequest.getFilterAssertionValue() != null && !searchRequest.getFilterAssertionValue().isEmpty()) {
+
+ for (String assertionValue : searchRequest.getFilterAssertionValue()) {
+ String[] targetArray = new String[]{assertionValue};
+ Filter displayNameFilter = Filter.createSubstringFilter(AttributeConstants.DISPLAY_NAME, null,
+ targetArray, null);
+ Filter policyStoreIdFilter = Filter.createSubstringFilter(INUM, null, targetArray, null);
+ Filter statusFilter = Filter.createSubstringFilter(AppConstants.STATUS, null, targetArray, null);
+ filters.add(Filter.createORFilter(policyStoreIdFilter, statusFilter, displayNameFilter));
+ }
+ searchFilter = Filter.createORFilter(filters);
}
- byte[] zipBytes = Files.readAllBytes(path);
+ log.debug("AdminUIPolicyStore searchFilter:{}", searchFilter);
+ return entryManager.findPagedEntries(POLICY_STORE_DN, AdminUIPolicyStore.class, searchFilter, null,
+ searchRequest.getSortBy(), SortOrder.getByValue(searchRequest.getSortOrder()),
+ searchRequest.getStartIndex(), searchRequest.getCount(), searchRequest.getMaxCount());
- return CommonUtils.createResponseWithByteArray(true, 200, "Policy store fetched successfully.", zipBytes);
- } catch (ApplicationException e) {
- throw e; // Re-throw ApplicationException as is
} catch (Exception e) {
log.error(ErrorResponse.RETRIEVE_POLICY_STORE_ERROR.getDescription(), e);
throw new ApplicationException(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), ErrorResponse.RETRIEVE_POLICY_STORE_ERROR.getDescription());
@@ -116,79 +129,102 @@ public GenericResponse getPolicyStore() throws ApplicationException {
}
/**
- * Uploads and overwrites the existing policy store file on the server.
+ * Validates and persists a new policy store.
*
- * This method performs the following operations:
- *
- * - Validates the incoming request and file metadata
- * - Ensures the uploaded file has a valid
.cjar extension
- * - Validates the input stream of the uploaded policy store
- * - Resolves the configured policy store path
- * - Validates the domain inside the existing policy store against the server host
- * - Creates a backup of the existing policy store file (if present)
- * - Uploads and replaces the policy store with the new file
- *
+ * The request is validated (base64 decoding, zip MIME type and trusted-issuer domain
+ * check) and, on success, persisted with a freshly generated inum/dn, creation and last-update
+ * timestamps, and an {@code inactive} status. Any client-supplied inum, dn, status or
+ * timestamps are overwritten by the server.
*
- * @param adminUIPolicyStore the {@link AdminUIPolicyStore} containing the policy store file
- * and its associated metadata
- * @return a {@link GenericResponse} indicating success or failure of the upload operation
- * @throws ApplicationException if:
- *
- * - The request or document is null
- * - The file name is missing or does not have a
.cjar extension
- * - The input stream is invalid or empty
- * - The policy store domain does not match the configured server host
- * - Any error occurs during validation, backup, or file upload
- *
+ * @param adminUIPolicyStore the policy store to create (must contain a valid base64 document)
+ * @return a {@link GenericResponse} indicating success
+ * @throws ApplicationException (HTTP 500) if validation or persistence fails
*/
-
public GenericResponse uploadPolicyStore(AdminUIPolicyStore adminUIPolicyStore) throws ApplicationException {
try {
validateRequest(adminUIPolicyStore);
- Document cjarDocument = adminUIPolicyStore.getDocument();
- log.info("Uploading policy-store : {}", cjarDocument.getFileName());
-
- InputStream originalStream = adminUIPolicyStore.getPolicyStore();
- //copy into a variable so that it can be used later
- byte[] cjarBytes = originalStream.readAllBytes();
+ log.info("Uploading policy-store : {}", adminUIPolicyStore.getDisplayname());
+ String inum = UUID.randomUUID().toString();
- InputStream cjarStreamForValidation = new ByteArrayInputStream(cjarBytes);
- InputStream cjarStreamForUpload = new ByteArrayInputStream(cjarBytes);
+ adminUIPolicyStore.setInum(inum);
+ adminUIPolicyStore.setDn(getDnForPolicyStore(inum));
+ Date now = new Date();
+ adminUIPolicyStore.setCreationDate(now);
+ adminUIPolicyStore.setJansLastUpd(now);
+ adminUIPolicyStore.setJansStatus(AppConstants.STATUS_INACTIVE);
+ entryManager.persist(adminUIPolicyStore);
- validateInputStream(cjarStreamForValidation);
-
- AUIConfiguration auiConfiguration = auiConfigurationService.getAUIConfiguration();
-
- String policyStorePath = resolvePolicyStorePath(auiConfiguration);
-
- // Validate domain inside policy store
- validatePolicyStoreDomain(cjarStreamForValidation, auiConfiguration.getAuiWebServerHost());
+ return CommonUtils.createGenericResponse(true, 200,
+ "Policy store saved successfully.");
- Path path = Paths.get(policyStorePath);
+ } catch (Exception e) {
+ log.error(ErrorResponse.POLICY_STORE_UPLOAD_ERROR.getDescription(), e);
+ throw new ApplicationException(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
+ e.getMessage()
+ );
+ }
+ }
- // Backup existing file
- backupExistingPolicyStore(path);
- try {
- // Upload new file
- Files.copy(cjarStreamForUpload, path, StandardCopyOption.REPLACE_EXISTING);
+ /**
+ * Applies editable fields to an existing policy store.
+ *
+ * Only the display name, description and status are updated; read-only fields (inum, dn,
+ * creation date, owner and the policy-store document) are preserved from persistence. If the
+ * status is being set to {@code active}, any other currently-active store is demoted to
+ * {@code inactive} so that at most one policy store is active at a time.
+ *
+ * @param inum the inum of the policy store to edit
+ * @param adminUIPolicyStore the source of the editable field values
+ * @return a {@link GenericResponse} indicating success
+ * @throws ApplicationException with HTTP 400 if the request or inum is invalid, HTTP 404 if
+ * no store exists for the inum, or HTTP 500 if the update fails
+ */
+ public GenericResponse editPolicyStore(String inum, AdminUIPolicyStore adminUIPolicyStore) throws ApplicationException {
+ try {
+ if (adminUIPolicyStore == null || Strings.isNullOrEmpty(inum)) {
+ throw new ApplicationException(
+ Response.Status.BAD_REQUEST.getStatusCode(),
+ ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription()
+ );
+ }
- log.info("Uploaded policy-store : {}", cjarDocument.getFileName());
- } catch(Exception e) {
- log.error("Upload failed. Restoring backup...", e);
- restoreFromBackup(path);
+ AdminUIPolicyStore existing = getPolicyStoreByInum(inum);
+ if (existing == null) {
throw new ApplicationException(
- Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
- e.getMessage());
+ Response.Status.NOT_FOUND.getStatusCode(),
+ ErrorResponse.RETRIEVE_POLICY_STORE_ERROR.getDescription()
+ );
+ }
+
+ // Apply only the editable fields; read-only fields (inum, dn,
+ // creationDate, jansUsrDN, policyStore) are preserved from persistence.
+ if (!Strings.isNullOrEmpty(adminUIPolicyStore.getDisplayname())) {
+ existing.setDisplayname(adminUIPolicyStore.getDisplayname());
+ }
+ if (!Strings.isNullOrEmpty(adminUIPolicyStore.getDescription())) {
+ existing.setDescription(adminUIPolicyStore.getDescription());
}
+ if (!Strings.isNullOrEmpty(adminUIPolicyStore.getJansStatus())) {
+ // Only one policy-store may be active at a time. If this store is being
+ // activated, demote any other currently-active store to inactive first.
+ if (AppConstants.STATUS_ACTIVE.equalsIgnoreCase(adminUIPolicyStore.getJansStatus())) {
+ deactivateOtherActivePolicyStores(inum);
+ }
+ existing.setJansStatus(adminUIPolicyStore.getJansStatus());
+ }
+ existing.setJansLastUpd(new Date());
+
+ entryManager.merge(existing);
return CommonUtils.createGenericResponse(true, 200,
- "Policy store overwritten successfully.");
+ "Policy store updated successfully.");
} catch (ApplicationException e) {
throw e;
} catch (Exception e) {
- log.error(ErrorResponse.POLICY_STORE_UPLOAD_ERROR.getDescription(), e);
+ log.error(ErrorResponse.POLICY_STORE_UPDATE_ERROR.getDescription(), e);
throw new ApplicationException(
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
e.getMessage()
@@ -196,6 +232,93 @@ public GenericResponse uploadPolicyStore(AdminUIPolicyStore adminUIPolicyStore)
}
}
+ /**
+ * Loads a policy store by its inum.
+ *
+ * @param inum the policy store inum
+ * @return the matching {@link AdminUIPolicyStore}, or {@code null} if none exists
+ */
+ private AdminUIPolicyStore getPolicyStoreByInum(String inum) {
+ if (Strings.isNullOrEmpty(inum)) {
+ return null;
+ }
+
+ try {
+ Filter filter = Filter.createEqualityFilter(INUM, inum);
+ List stores = entryManager.findEntries(POLICY_STORE_DN, AdminUIPolicyStore.class, filter, 1);
+ return CollectionUtils.isEmpty(stores) ? null : stores.get(0);
+ } catch (Exception e) {
+ log.error("Failed to fetch policy store for inum: '{}'", inum, e);
+ return null;
+ }
+ }
+
+ /**
+ * Deletes the policy store identified by the given inum.
+ *
+ * @param inum the inum of the policy store to delete
+ * @return a {@link GenericResponse} indicating success
+ * @throws ApplicationException with HTTP 400 if the inum is blank, or HTTP 404 if no store
+ * exists for the inum
+ */
+ public GenericResponse deletePolicyStore(String inum) throws ApplicationException {
+ if (Strings.isNullOrEmpty(inum)) {
+ throw new ApplicationException(
+ Response.Status.BAD_REQUEST.getStatusCode(),
+ ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription()
+ );
+ }
+
+ AdminUIPolicyStore existing = getPolicyStoreByInum(inum);
+ if (existing == null) {
+ throw new ApplicationException(
+ Response.Status.NOT_FOUND.getStatusCode(),
+ ErrorResponse.RETRIEVE_POLICY_STORE_ERROR.getDescription()
+ );
+ }
+ log.info("delete policy-store : {}", inum);
+ entryManager.remove(existing);
+
+ return CommonUtils.createGenericResponse(true, 200,
+ "Policy store deleted successfully.");
+ }
+
+ /**
+ * Demotes every active policy-store other than {@code inumToKeep} to inactive,
+ * enforcing the "only one active policy-store" invariant.
+ *
+ * @param inumToKeep the inum of the store that is about to become active and must be left untouched
+ */
+ private void deactivateOtherActivePolicyStores(String inumToKeep) {
+ Filter activeFilter = Filter.createEqualityFilter(AppConstants.STATUS, AppConstants.STATUS_ACTIVE);
+ List activeStores =
+ entryManager.findEntries(POLICY_STORE_DN, AdminUIPolicyStore.class, activeFilter);
+
+ if (CollectionUtils.isEmpty(activeStores)) {
+ return;
+ }
+ for (AdminUIPolicyStore activeStore : activeStores) {
+ if (inumToKeep.equals(activeStore.getInum())) {
+ continue;
+ }
+ log.info("Deactivating previously active policy-store : {}", activeStore.getInum());
+ activeStore.setJansStatus(AppConstants.STATUS_INACTIVE);
+ activeStore.setJansLastUpd(new Date());
+ entryManager.merge(activeStore);
+ }
+ }
+
+ /**
+ * Validates a policy-store upload request.
+ *
+ * Ensures the request and its base64 document are present, that the decoded bytes are a
+ * zip archive, and that the trusted-issuer domain inside the archive matches the configured
+ * Admin UI web-server host.
+ *
+ * @param adminUIPolicyStore the policy store to validate
+ * @throws ApplicationException with HTTP 400 if the request is missing/malformed, not a zip,
+ * or its domain does not match the configured host
+ */
private void validateRequest(AdminUIPolicyStore adminUIPolicyStore) throws ApplicationException {
if (adminUIPolicyStore == null) {
log.error(ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription());
@@ -205,47 +328,57 @@ private void validateRequest(AdminUIPolicyStore adminUIPolicyStore) throws Appli
);
}
- Document document = adminUIPolicyStore.getDocument();
- if (document == null || document.getFileName() == null) {
- log.error("Document details not provided.");
+ String base64PolicyStore = adminUIPolicyStore.getPolicyStore();
+ if (Strings.isNullOrEmpty(base64PolicyStore)) {
+ log.error("PolicyStore is null.");
throw new ApplicationException(
Response.Status.BAD_REQUEST.getStatusCode(),
ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription()
);
}
- if (!document.getFileName().endsWith(".cjar")) {
+ byte[] zipBytes;
+ try {
+ zipBytes = Base64.getDecoder().decode(base64PolicyStore);
+ } catch (IllegalArgumentException e) {
+ log.error(ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription(), e);
+ throw new ApplicationException(
+ Response.Status.BAD_REQUEST.getStatusCode(),
+ ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription()
+ );
+ }
+
+ String mimeType = tika.detect(zipBytes);
+ if (Strings.isNullOrEmpty(mimeType) || !mimeType.equalsIgnoreCase("application/zip")) {
log.error(ErrorResponse.UNSUPPORTED_POLICY_STORE_EXTENSION.getDescription());
throw new ApplicationException(
Response.Status.BAD_REQUEST.getStatusCode(),
ErrorResponse.UNSUPPORTED_POLICY_STORE_EXTENSION.getDescription()
);
}
- }
- private void validateInputStream(InputStream inputStream) throws ApplicationException {
- try {
- if (inputStream == null || inputStream.available() <= 0) {
- log.error(ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription());
- throw new ApplicationException(
- Response.Status.BAD_REQUEST.getStatusCode(),
- ErrorResponse.BAD_REQUEST_IN_POLICY_STORE_UPLOAD.getDescription()
- );
- }
- } catch (IOException e) {
+ try (InputStream cjarStream = new ByteArrayInputStream(zipBytes)) {
+ AUIConfiguration auiConfiguration = auiConfigurationService.getAUIConfiguration();
+ validatePolicyStoreDomain(cjarStream, auiConfiguration.getAuiWebServerHost());
+ } catch (ApplicationException e) {
+ throw e;
+ } catch (URISyntaxException | IOException e) {
throw new ApplicationException(
Response.Status.BAD_REQUEST.getStatusCode(),
- "Error in reading policy-store. Invalid input stream"
+ e.getMessage()
);
}
}
- private String resolvePolicyStorePath(AUIConfiguration config) {
- return Optional.ofNullable(config.getAuiCedarlingDefaultPolicyStorePath())
- .filter(path -> !Strings.isNullOrEmpty(path))
- .orElse(AppConstants.DEFAULT_POLICY_STORE_FILE_PATH);
- }
-
+ /**
+ * Verifies that the trusted-issuer domain inside the policy-store archive matches the
+ * host of the configured Admin UI web-server URL.
+ *
+ * @param cjarStream the policy-store archive stream to inspect
+ * @param webServerHost the configured Admin UI web-server URL whose host must match
+ * @throws ApplicationException with HTTP 400 if the domains do not match
+ * @throws URISyntaxException if {@code webServerHost} is not a valid URI
+ */
private void validatePolicyStoreDomain(InputStream cjarStream, String webServerHost)
throws ApplicationException, URISyntaxException {
@@ -259,32 +392,13 @@ private void validatePolicyStoreDomain(InputStream cjarStream, String webServerH
}
}
- private void backupExistingPolicyStore(Path path) throws IOException {
- if (Files.exists(path)) {
- Path backupPath = Paths.get(path.toString() + ".bak");
- Files.move(path, backupPath, StandardCopyOption.REPLACE_EXISTING);
- }
- }
-
- private void restoreFromBackup(Path path) {
- try {
- Path backupPath = Paths.get(path.toString() + ".bak");
- if (Files.exists(backupPath)) {
- Files.move(backupPath, path, StandardCopyOption.REPLACE_EXISTING);
- log.info("Restored policy store from backup");
- }
- } catch (IOException e) {
- log.error("Failed to restore policy store from backup", e);
- }
- }
-
/**
- * Synchronizes Admin UI role-to-scope mappings using the currently configured Cedar policy store.
+ * Synchronizes Admin UI role-to-scope mappings using the active Cedar policy store stored in the database.
*
* The synchronization process includes:
*
* - Retrieving resource-to-scope mappings from persistence
- * - Parsing the Cedar policy store archive (.cjar)
+ * - Fetching the active policy store from the database and parsing its Cedar archive (.cjar)
* - Deriving principal-to-scope mappings from policies
* - Generating Admin UI roles from the principals
* - Generating role-permission mappings
@@ -315,19 +429,16 @@ public GenericResponse syncRoleScopeMapping() throws ApplicationException {
throw new ApplicationException(Response.Status.BAD_REQUEST.getStatusCode(),
"Invalid input data for synchronization");
}
- AUIConfiguration auiConfiguration = auiConfigurationService.getAUIConfiguration();
- String policyStorePath = Optional.ofNullable(auiConfiguration.getAuiCedarlingDefaultPolicyStorePath())
- .filter(path -> !Strings.isNullOrEmpty(path))
- .orElse(AppConstants.DEFAULT_POLICY_STORE_FILE_PATH);
- Map> principalsToScopesMap;
- try (ZipFile zipFile = new ZipFile(policyStorePath)) {
- principalsToScopesMap = policyToScopeMapper.processZipFile(zipFile, resourceScopesJson);
- } catch (Exception e) {
- log.error(ErrorResponse.ERROR_IN_POLICY_STORE.getDescription(), e);
- throw new ApplicationException(Response.Status.BAD_REQUEST.getStatusCode(),
- ErrorResponse.ERROR_IN_POLICY_STORE.getDescription());
+ // Fetch the active policy-store from the database (its document is stored as base64).
+ AdminUIPolicyStore activePolicyStore = getActivePolicyStore();
+ if (activePolicyStore == null || Strings.isNullOrEmpty(activePolicyStore.getPolicyStore())) {
+ log.error(ErrorResponse.NO_ACTIVE_POLICY_STORE_FOUND.getDescription());
+ throw new ApplicationException(Response.Status.NOT_FOUND.getStatusCode(),
+ ErrorResponse.NO_ACTIVE_POLICY_STORE_FOUND.getDescription());
}
+ Map> principalsToScopesMap = mapPrincipalsToScopes(activePolicyStore, resourceScopesJson);
+
// Validate mapping results
if (principalsToScopesMap.isEmpty()) {
log.warn("No role-to-scope mappings found during synchronization");
@@ -355,6 +466,60 @@ public GenericResponse syncRoleScopeMapping() throws ApplicationException {
}
}
+ /**
+ * Retrieves the single active policy-store from persistence.
+ *
+ * @return the active {@link AdminUIPolicyStore}, or {@code null} if none is active
+ */
+ private AdminUIPolicyStore getActivePolicyStore() {
+ Filter activeFilter = Filter.createEqualityFilter(AppConstants.STATUS, AppConstants.STATUS_ACTIVE);
+ List activeStores =
+ entryManager.findEntries(POLICY_STORE_DN, AdminUIPolicyStore.class, activeFilter);
+
+ if (CollectionUtils.isEmpty(activeStores)) {
+ return null;
+ }
+ if (activeStores.size() > 1) {
+ log.warn("Found {} active policy-stores; using the first one.", activeStores.size());
+ }
+ return activeStores.get(0);
+ }
+
+ /**
+ * Decodes the base64 policy-store document into a temporary .cjar archive and derives
+ * principal-to-scope mappings from it. The temporary file is always removed afterwards.
+ *
+ * @param policyStore the active policy-store whose document should be parsed
+ * @param resourceScopesJson the resource-to-scope mappings used during parsing
+ * @return principal-to-scope mappings derived from the policy-store
+ * @throws ApplicationException if the document cannot be decoded or parsed
+ */
+ private Map> mapPrincipalsToScopes(AdminUIPolicyStore policyStore, JsonNode resourceScopesJson)
+ throws ApplicationException {
+ Path tempFile = null;
+ try {
+ byte[] zipBytes = Base64.getDecoder().decode(policyStore.getPolicyStore());
+ tempFile = Files.createTempFile("adminui-policy-store-", ".cjar");
+ Files.write(tempFile, zipBytes);
+
+ try (ZipFile zipFile = new ZipFile(tempFile.toFile())) {
+ return policyToScopeMapper.processZipFile(zipFile, resourceScopesJson);
+ }
+ } catch (Exception e) {
+ log.error(ErrorResponse.ERROR_IN_POLICY_STORE.getDescription(), e);
+ throw new ApplicationException(Response.Status.BAD_REQUEST.getStatusCode(),
+ ErrorResponse.ERROR_IN_POLICY_STORE.getDescription());
+ } finally {
+ if (tempFile != null) {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException e) {
+ log.warn("Failed to delete temporary policy-store file: {}", tempFile, e);
+ }
+ }
+ }
+ }
+
/**
* Extracted method for creating AdminRole objects
*/
@@ -402,6 +567,16 @@ private List removeDuplicatePermissions(List 0
+ ? configurationFactory.getApiAppConfiguration().getMaxCount()
+ : ApiConstants.DEFAULT_MAX_COUNT);
+ }
}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/AppConstants.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/AppConstants.java
index 20b453de5c5..67d6121cc88 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/AppConstants.java
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/AppConstants.java
@@ -4,6 +4,7 @@ public interface AppConstants {
public static final String ADMIN_UI_CONFIG_DN = "ou=admin-ui,ou=configuration,o=jans";
public static final String ADMIN_UI_RESOURCE_SCOPES_MAPPING_DN = "ou=adminUIResourceScopesMapping,ou=admin-ui,o=jans";
public static final String WEBHOOK_DN = "ou=auiWebhooks,ou=admin-ui,o=jans";
+ public static final String POLICY_STORE_DN = "ou=adminUIPolicyStore,ou=admin-ui,o=jans";
public static final String ADMIN_UI_FEATURES_DN = "ou=auiFeatures,ou=admin-ui,o=jans";
public static final String ADS_CONFIG_DN = "ou=agama-developer-studio,ou=configuration,o=jans";
public static final String DEFAULT_POLICY_STORE_FILE_PATH = "./custom/config/adminUI/policy-store.cjar";
@@ -21,6 +22,9 @@ public interface AppConstants {
//fields name
public static final String WEBHOOK_ID = "webhookId";
public static final String INUM = "inum";
+ public static final String STATUS = "jansStatus";
+ public static final String STATUS_ACTIVE = "active";
+ public static final String STATUS_INACTIVE = "inactive";
public static final String ADMIN_UI_FEATURE_ID = "featureId";
public static final String AUTHORIZATION = "Authorization";
public static final String CONTENT_TYPE = "Content-Type";
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/ErrorResponse.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/ErrorResponse.java
index ab69c1d63ff..16289051f58 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/ErrorResponse.java
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/utils/ErrorResponse.java
@@ -76,10 +76,15 @@ public enum ErrorResponse {
ADMINUI_SESSION_REMOVE_ERROR("Error in removing Admin UI session"),
ORG_ID_CLAIM_NOT_FOUND("org_id claim not found in jwt"),
POLICY_STORE_UPLOAD_ERROR("Error in uploading policy-store"),
+ POLICY_STORE_UPDATE_ERROR("Error in updating policy-store"),
+ POLICY_STORE_DELETE_ERROR("Error in deleting policy-store"),
+ POLICY_STORE_GET_ERROR("Error in fetching policy-store"),
+ POLICY_STORE_NOT_EXIST("Policy store does not exist"),
BAD_REQUEST_IN_POLICY_STORE_UPLOAD("Bad Request: No Policy Store file provided"),
- UNSUPPORTED_POLICY_STORE_EXTENSION("Bad Request: The Policy Store extension is not .cjar and is not supported"),
+ UNSUPPORTED_POLICY_STORE_EXTENSION("Bad Request: The Policy Store extension is not .cjar or of application/zip mime and is not supported"),
POLICY_STORE_DOMAIN_NOT_MATCHING("The hostname of the configuration_endpoint in policy-store does not matches with hostname of OpenID Provider."),
- ERROR_IN_POLICY_STORE("Bad Request: Error in reading Policy Store file")
+ ERROR_IN_POLICY_STORE("Bad Request: Error in reading Policy Store file"),
+ NO_ACTIVE_POLICY_STORE_FOUND("No active policy-store found. Activate a policy-store before syncing role-to-scope mappings.")
;
private final String description;
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/create-policy-store-request.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/create-policy-store-request.json
new file mode 100644
index 00000000000..498d4030e84
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/create-policy-store-request.json
@@ -0,0 +1,6 @@
+{
+ "displayname": "Admin UI Cedarling Policy Store",
+ "description": "Cedarling policy store used to derive Admin UI role-to-scope mappings",
+ "policyStore": "UEsDBBQACAgIAABb...==",
+ "jansStatus": "active"
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/create-policy-store-response.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/create-policy-store-response.json
new file mode 100644
index 00000000000..97ee11923e7
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/create-policy-store-response.json
@@ -0,0 +1,5 @@
+{
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Policy store saved successfully."
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/delete-policy-store-response.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/delete-policy-store-response.json
new file mode 100644
index 00000000000..4445f855319
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/delete-policy-store-response.json
@@ -0,0 +1,5 @@
+{
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Policy store deleted successfully."
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/edit-policy-store-request.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/edit-policy-store-request.json
new file mode 100644
index 00000000000..b636b569d2a
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/edit-policy-store-request.json
@@ -0,0 +1,5 @@
+{
+ "displayname": "Admin UI Cedarling Policy Store (updated)",
+ "description": "Updated description for the Admin UI policy store",
+ "jansStatus": "inactive"
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/edit-policy-store-response.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/edit-policy-store-response.json
new file mode 100644
index 00000000000..4bc24d4a351
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/edit-policy-store-response.json
@@ -0,0 +1,5 @@
+{
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Policy store updated successfully."
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/get-all-policy-store-response.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/get-all-policy-store-response.json
new file mode 100644
index 00000000000..6ddebfe05ab
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/get-all-policy-store-response.json
@@ -0,0 +1,18 @@
+{
+ "start": 0,
+ "totalEntriesCount": 1,
+ "entriesCount": 1,
+ "entries": [
+ {
+ "dn": "inum=e1a2b3c4-1234-5678-9abc-def012345678,ou=policy-stores,ou=admin-ui,o=jans",
+ "inum": "e1a2b3c4-1234-5678-9abc-def012345678",
+ "displayname": "Admin UI Cedarling Policy Store",
+ "description": "Cedarling policy store used to derive Admin UI role-to-scope mappings",
+ "policyStore": "UEsDBBQACAgIAABb...==",
+ "jansUsrDN": "inum=abc123,ou=people,o=jans",
+ "jansStatus": "active",
+ "creationDate": "2026-07-14T10:15:30.000Z",
+ "jansLastUpd": "2026-07-14T11:20:45.000Z"
+ }
+ ]
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/sync-role-scopes-mapping-response.json b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/sync-role-scopes-mapping-response.json
new file mode 100644
index 00000000000..6243aeed2b4
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/main/resources/example/adminui/security/sync-role-scopes-mapping-response.json
@@ -0,0 +1,5 @@
+{
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Sync of role-to-scope mappings from the policy store completed successfully."
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/AdminUISecurityServiceTest.java b/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/AdminUISecurityServiceTest.java
new file mode 100644
index 00000000000..41e8a6f00b5
--- /dev/null
+++ b/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/AdminUISecurityServiceTest.java
@@ -0,0 +1,403 @@
+package io.jans.ca.plugin.adminui.test.services;
+
+import io.jans.ca.plugin.adminui.model.adminui.AdminUIPolicyStore;
+import io.jans.ca.plugin.adminui.model.auth.GenericResponse;
+import io.jans.ca.plugin.adminui.model.exception.ApplicationException;
+import io.jans.ca.plugin.adminui.service.adminui.AdminUISecurityService;
+import io.jans.ca.plugin.adminui.service.config.AUIConfigurationService;
+import io.jans.ca.plugin.adminui.utils.AppConstants;
+import io.jans.ca.plugin.adminui.utils.ErrorResponse;
+import io.jans.configapi.configuration.ConfigurationFactory;
+import io.jans.configapi.model.configuration.ApiAppConfiguration;
+import io.jans.model.SearchRequest;
+import io.jans.orm.PersistenceEntryManager;
+import io.jans.orm.model.PagedResult;
+import io.jans.orm.model.SortOrder;
+import io.jans.orm.search.filter.Filter;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.testng.MockitoTestNGListener;
+import org.slf4j.Logger;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+import jakarta.ws.rs.core.Response;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+/**
+ * Unit tests for {@link AdminUISecurityService} covering the methods invoked
+ * from {@code AdminUISecurityResource}: searchPolicyStores, uploadPolicyStore,
+ * editPolicyStore, deletePolicyStore, syncRoleScopeMapping and getRecordMaxCount.
+ */
+@Listeners(MockitoTestNGListener.class)
+public class AdminUISecurityServiceTest {
+
+ private static final String INUM = "1234-5678";
+
+ @Mock
+ private Logger log;
+ @Mock
+ private PersistenceEntryManager entryManager;
+ @Mock
+ private AUIConfigurationService auiConfigurationService;
+ @Mock
+ private ConfigurationFactory configurationFactory;
+ @Mock
+ private io.jans.ca.plugin.adminui.service.adminui.AdminUIService adminUIService;
+ @Mock
+ private io.jans.ca.plugin.adminui.utils.security.PolicyToScopeMapper policyToScopeMapper;
+
+ @InjectMocks
+ private AdminUISecurityService adminUISecurityService;
+
+ private static final String WEB_SERVER_HOST = "https://test-jans.io";
+ private static final String POLICY_STORE_FIXTURE = "/json/adminui/test-policy-store.cjar";
+
+ @BeforeMethod
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ /**
+ * Loads the packaged {@code .cjar} test fixture and returns it base64-encoded,
+ * as stored in the {@code policyStore} document field.
+ */
+ private String loadPolicyStoreBase64() throws java.io.IOException {
+ try (java.io.InputStream is = getClass().getResourceAsStream(POLICY_STORE_FIXTURE)) {
+ assertNotNull(is, "Missing test fixture: " + POLICY_STORE_FIXTURE);
+ return java.util.Base64.getEncoder().encodeToString(is.readAllBytes());
+ }
+ }
+
+ // ---------------------------------------------------------------------
+ // searchPolicyStores
+ // ---------------------------------------------------------------------
+
+ @Test
+ public void testSearchPolicyStores_success() throws ApplicationException {
+ SearchRequest searchRequest = new SearchRequest();
+ searchRequest.setSortBy(AppConstants.INUM);
+ searchRequest.setSortOrder(SortOrder.ASCENDING.getValue());
+ searchRequest.setStartIndex(0);
+ searchRequest.setCount(10);
+ searchRequest.setMaxCount(100);
+
+ PagedResult expected = new PagedResult<>();
+ when(entryManager.findPagedEntries(anyString(), eq(AdminUIPolicyStore.class), any(),
+ isNull(), anyString(), any(SortOrder.class), anyInt(), anyInt(), anyInt()))
+ .thenReturn(expected);
+
+ PagedResult result = adminUISecurityService.searchPolicyStores(searchRequest);
+
+ assertSame(result, expected);
+ }
+
+ @Test
+ public void testSearchPolicyStores_persistenceFailureWrapped() {
+ SearchRequest searchRequest = new SearchRequest();
+ searchRequest.setSortBy(AppConstants.INUM);
+ searchRequest.setSortOrder(SortOrder.ASCENDING.getValue());
+ lenient().when(entryManager.findPagedEntries(anyString(), any(), any(), any(), any(),
+ any(), anyInt(), anyInt(), anyInt()))
+ .thenThrow(new RuntimeException("db down"));
+
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.searchPolicyStores(searchRequest));
+ assertEquals(ex.getErrorCode(), Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
+ }
+
+ // ---------------------------------------------------------------------
+ // uploadPolicyStore
+ // ---------------------------------------------------------------------
+
+ @Test
+ public void testUploadPolicyStore_nullRequestThrowsBadRequest() {
+ // validateRequest fails -> wrapped as INTERNAL_SERVER_ERROR by uploadPolicyStore
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.uploadPolicyStore(null));
+ assertEquals(ex.getErrorCode(), Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
+ verify(entryManager, never()).persist(any());
+ }
+
+ @Test
+ public void testUploadPolicyStore_missingPolicyStoreDocument() {
+ AdminUIPolicyStore store = new AdminUIPolicyStore();
+ store.setDisplayname("test");
+ // policyStore document is null -> validation fails
+
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.uploadPolicyStore(store));
+ assertEquals(ex.getErrorCode(), Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
+ verify(entryManager, never()).persist(any());
+ }
+
+ @Test
+ public void testUploadPolicyStore_success() throws Exception {
+ io.jans.configapi.core.model.adminui.AUIConfiguration auiConfiguration =
+ mock(io.jans.configapi.core.model.adminui.AUIConfiguration.class);
+ when(auiConfigurationService.getAUIConfiguration()).thenReturn(auiConfiguration);
+ when(auiConfiguration.getAuiWebServerHost()).thenReturn(WEB_SERVER_HOST);
+
+ AdminUIPolicyStore store = new AdminUIPolicyStore();
+ store.setDisplayname("test-store");
+ store.setPolicyStore(loadPolicyStoreBase64());
+
+ GenericResponse response = adminUISecurityService.uploadPolicyStore(store);
+
+ assertTrue(response.isSuccess());
+ assertEquals(response.getResponseCode(), 200);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(AdminUIPolicyStore.class);
+ verify(entryManager).persist(captor.capture());
+ AdminUIPolicyStore persisted = captor.getValue();
+ assertNotNull(persisted.getInum());
+ assertEquals(persisted.getDn(), adminUISecurityService.getDnForPolicyStore(persisted.getInum()));
+ assertEquals(persisted.getJansStatus(), AppConstants.STATUS_INACTIVE);
+ assertNotNull(persisted.getCreationDate());
+ }
+
+ @Test
+ public void testUploadPolicyStore_domainMismatchRejected() throws Exception {
+ io.jans.configapi.core.model.adminui.AUIConfiguration auiConfiguration =
+ mock(io.jans.configapi.core.model.adminui.AUIConfiguration.class);
+ when(auiConfigurationService.getAUIConfiguration()).thenReturn(auiConfiguration);
+ when(auiConfiguration.getAuiWebServerHost()).thenReturn("https://someone-else.example.com");
+
+ AdminUIPolicyStore store = new AdminUIPolicyStore();
+ store.setDisplayname("test-store");
+ store.setPolicyStore(loadPolicyStoreBase64());
+
+ // domain does not match the fixture's trusted-issuer host -> wrapped as 500 by uploadPolicyStore
+ expectThrows(ApplicationException.class, () -> adminUISecurityService.uploadPolicyStore(store));
+ verify(entryManager, never()).persist(any());
+ }
+
+ // ---------------------------------------------------------------------
+ // editPolicyStore
+ // ---------------------------------------------------------------------
+
+ @Test
+ public void testEditPolicyStore_nullRequestReturnsBadRequest() {
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.editPolicyStore(INUM, null));
+ assertEquals(ex.getErrorCode(), Response.Status.BAD_REQUEST.getStatusCode());
+ }
+
+ @Test
+ public void testEditPolicyStore_blankInumReturnsBadRequest() {
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.editPolicyStore("", new AdminUIPolicyStore()));
+ assertEquals(ex.getErrorCode(), Response.Status.BAD_REQUEST.getStatusCode());
+ }
+
+ @Test
+ public void testEditPolicyStore_notFound() {
+ when(entryManager.find(eq(AdminUIPolicyStore.class), anyString())).thenReturn(null);
+
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.editPolicyStore(INUM, new AdminUIPolicyStore()));
+ assertEquals(ex.getErrorCode(), Response.Status.NOT_FOUND.getStatusCode());
+ verify(entryManager, never()).merge(any());
+ }
+
+ @Test
+ public void testEditPolicyStore_success() throws ApplicationException {
+ AdminUIPolicyStore existing = new AdminUIPolicyStore();
+ existing.setInum(INUM);
+ existing.setDisplayname("old");
+ when(entryManager.find(eq(AdminUIPolicyStore.class), anyString())).thenReturn(existing);
+
+ AdminUIPolicyStore update = new AdminUIPolicyStore();
+ update.setDisplayname("new-name");
+ update.setDescription("new-desc");
+ update.setJansStatus(AppConstants.STATUS_ACTIVE);
+
+ GenericResponse response = adminUISecurityService.editPolicyStore(INUM, update);
+
+ assertTrue(response.isSuccess());
+ assertEquals(response.getResponseCode(), 200);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(AdminUIPolicyStore.class);
+ verify(entryManager).merge(captor.capture());
+ AdminUIPolicyStore merged = captor.getValue();
+ assertEquals(merged.getDisplayname(), "new-name");
+ assertEquals(merged.getDescription(), "new-desc");
+ assertEquals(merged.getJansStatus(), AppConstants.STATUS_ACTIVE);
+ // read-only field preserved
+ assertEquals(merged.getInum(), INUM);
+ }
+
+ @Test
+ public void testEditPolicyStore_activatingDemotesOtherActiveStore() throws ApplicationException {
+ AdminUIPolicyStore existing = new AdminUIPolicyStore();
+ existing.setInum(INUM);
+ existing.setJansStatus(AppConstants.STATUS_INACTIVE);
+ when(entryManager.find(eq(AdminUIPolicyStore.class), anyString())).thenReturn(existing);
+
+ // Another store is currently active and must be demoted.
+ AdminUIPolicyStore otherActive = new AdminUIPolicyStore();
+ otherActive.setInum("other-inum");
+ otherActive.setJansStatus(AppConstants.STATUS_ACTIVE);
+ when(entryManager.findEntries(anyString(), eq(AdminUIPolicyStore.class), any(Filter.class)))
+ .thenReturn(java.util.List.of(otherActive));
+
+ AdminUIPolicyStore update = new AdminUIPolicyStore();
+ update.setJansStatus(AppConstants.STATUS_ACTIVE);
+
+ GenericResponse response = adminUISecurityService.editPolicyStore(INUM, update);
+
+ assertTrue(response.isSuccess());
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(AdminUIPolicyStore.class);
+ verify(entryManager, times(2)).merge(captor.capture());
+ java.util.List merged = captor.getAllValues();
+ // the previously-active store was demoted...
+ AdminUIPolicyStore demoted = merged.stream()
+ .filter(s -> "other-inum".equals(s.getInum())).findFirst().orElseThrow();
+ assertEquals(demoted.getJansStatus(), AppConstants.STATUS_INACTIVE);
+ // ...and the edited store is now active
+ AdminUIPolicyStore activated = merged.stream()
+ .filter(s -> INUM.equals(s.getInum())).findFirst().orElseThrow();
+ assertEquals(activated.getJansStatus(), AppConstants.STATUS_ACTIVE);
+ }
+
+ @Test
+ public void testEditPolicyStore_activatingSelfDoesNotDemoteItself() throws ApplicationException {
+ AdminUIPolicyStore existing = new AdminUIPolicyStore();
+ existing.setInum(INUM);
+ existing.setJansStatus(AppConstants.STATUS_ACTIVE);
+ when(entryManager.find(eq(AdminUIPolicyStore.class), anyString())).thenReturn(existing);
+
+ // The only active store is the one being edited; it must not be demoted.
+ AdminUIPolicyStore self = new AdminUIPolicyStore();
+ self.setInum(INUM);
+ self.setJansStatus(AppConstants.STATUS_ACTIVE);
+ when(entryManager.findEntries(anyString(), eq(AdminUIPolicyStore.class), any(Filter.class)))
+ .thenReturn(java.util.List.of(self));
+
+ AdminUIPolicyStore update = new AdminUIPolicyStore();
+ update.setJansStatus(AppConstants.STATUS_ACTIVE);
+
+ GenericResponse response = adminUISecurityService.editPolicyStore(INUM, update);
+
+ assertTrue(response.isSuccess());
+ // only the edited store is merged (once); nothing gets demoted
+ ArgumentCaptor captor = ArgumentCaptor.forClass(AdminUIPolicyStore.class);
+ verify(entryManager, times(1)).merge(captor.capture());
+ assertEquals(captor.getValue().getJansStatus(), AppConstants.STATUS_ACTIVE);
+ }
+
+ // ---------------------------------------------------------------------
+ // deletePolicyStore
+ // ---------------------------------------------------------------------
+
+ @Test
+ public void testDeletePolicyStore_blankInumReturnsBadRequest() {
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.deletePolicyStore(""));
+ assertEquals(ex.getErrorCode(), Response.Status.BAD_REQUEST.getStatusCode());
+ verify(entryManager, never()).remove(any());
+ }
+
+ @Test
+ public void testDeletePolicyStore_notFound() {
+ when(entryManager.find(eq(AdminUIPolicyStore.class), anyString())).thenReturn(null);
+
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.deletePolicyStore(INUM));
+ assertEquals(ex.getErrorCode(), Response.Status.NOT_FOUND.getStatusCode());
+ verify(entryManager, never()).remove(any());
+ }
+
+ @Test
+ public void testDeletePolicyStore_success() throws ApplicationException {
+ AdminUIPolicyStore existing = new AdminUIPolicyStore();
+ existing.setInum(INUM);
+ when(entryManager.find(eq(AdminUIPolicyStore.class), anyString())).thenReturn(existing);
+
+ GenericResponse response = adminUISecurityService.deletePolicyStore(INUM);
+
+ assertTrue(response.isSuccess());
+ assertEquals(response.getResponseCode(), 200);
+ verify(entryManager).remove(existing);
+ }
+
+ // ---------------------------------------------------------------------
+ // syncRoleScopeMapping
+ // ---------------------------------------------------------------------
+
+ @Test
+ public void testSyncRoleScopeMapping_noActivePolicyStore() {
+ // resource-scope mappings lookup returns empty list (non-null -> JSON node is created)
+ when(entryManager.findEntries(anyString(), any(), any(Filter.class)))
+ .thenReturn(java.util.Collections.emptyList());
+
+ ApplicationException ex = expectThrows(ApplicationException.class,
+ () -> adminUISecurityService.syncRoleScopeMapping());
+ assertEquals(ex.getErrorCode(), Response.Status.NOT_FOUND.getStatusCode());
+ assertEquals(ex.getMessage(), ErrorResponse.NO_ACTIVE_POLICY_STORE_FOUND.getDescription());
+ }
+
+ @Test
+ public void testSyncRoleScopeMapping_success() throws Exception {
+ // resource-scope mappings lookup (presence filter)
+ when(entryManager.findEntries(anyString(),
+ eq(io.jans.ca.plugin.adminui.model.adminui.AdminUIResourceScopesMapping.class),
+ any(Filter.class)))
+ .thenReturn(java.util.Collections.emptyList());
+
+ // active policy-store lookup (equality filter) -> one active store with the fixture document
+ AdminUIPolicyStore activeStore = new AdminUIPolicyStore();
+ activeStore.setInum(INUM);
+ activeStore.setJansStatus(AppConstants.STATUS_ACTIVE);
+ activeStore.setPolicyStore(loadPolicyStoreBase64());
+ when(entryManager.findEntries(anyString(), eq(AdminUIPolicyStore.class), any(Filter.class)))
+ .thenReturn(java.util.List.of(activeStore));
+
+ // policy-store parsing is delegated to the mapper (mocked): return a role with a duplicate scope
+ java.util.Map> mapped = new java.util.HashMap<>();
+ mapped.put("adminRole", new java.util.LinkedHashSet<>(java.util.List.of("scope-read", "scope-write")));
+ when(policyToScopeMapper.processZipFile(any(), any())).thenReturn(mapped);
+
+ GenericResponse response = adminUISecurityService.syncRoleScopeMapping();
+
+ assertTrue(response.isSuccess());
+ assertEquals(response.getResponseCode(), 200);
+ verify(policyToScopeMapper).processZipFile(any(), any());
+ verify(adminUIService).resetRoles(any());
+ verify(adminUIService).resetPermissionsToRole(any());
+ }
+
+ // ---------------------------------------------------------------------
+ // getRecordMaxCount
+ // ---------------------------------------------------------------------
+
+ @Test
+ public void testGetRecordMaxCount_usesConfiguredValue() {
+ ApiAppConfiguration apiAppConfiguration = mock(ApiAppConfiguration.class);
+ when(configurationFactory.getApiAppConfiguration()).thenReturn(apiAppConfiguration);
+ when(apiAppConfiguration.getMaxCount()).thenReturn(250);
+
+ assertEquals(adminUISecurityService.getRecordMaxCount(), 250);
+ }
+
+ @Test
+ public void testGetRecordMaxCount_fallsBackToDefaultWhenNotPositive() {
+ ApiAppConfiguration apiAppConfiguration = mock(ApiAppConfiguration.class);
+ when(configurationFactory.getApiAppConfiguration()).thenReturn(apiAppConfiguration);
+ when(apiAppConfiguration.getMaxCount()).thenReturn(0);
+
+ assertEquals(adminUISecurityService.getRecordMaxCount(),
+ io.jans.configapi.util.ApiConstants.DEFAULT_MAX_COUNT);
+ }
+}
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/LicenseServiceTest.java b/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/LicenseServiceTest.java
index 76eda592d9f..4b9879cf650 100644
--- a/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/LicenseServiceTest.java
+++ b/jans-config-api/plugins/admin-ui-plugin/src/test/java/io/jans/ca/plugin/adminui/test/services/LicenseServiceTest.java
@@ -55,6 +55,10 @@ public void setUp() {
lenient().when(adminConf.getMainSettings()).thenReturn(mock(MainSettings.class));
lenient().when(adminConf.getMainSettings().getLicenseConfig()).thenReturn(licenseConfig);
+ // The service also reads the in-memory license configuration; stub that path so
+ // auiConfiguration / licenseConfiguration are never null during validation.
+ lenient().when(auiConfigurationService.getAUIConfiguration()).thenReturn(auiConfiguration);
+ lenient().when(auiConfiguration.getLicenseConfiguration()).thenReturn(licenseConfiguration);
}
@Test
@@ -98,6 +102,10 @@ void testValidateLicenseConfiguration_ValidConfig() {
when(licenseConfig.getOidcClient()).thenReturn(new OIDCClientSettings("test-host", "test-client-id", "test-client-secret"));
when(licenseConfig.getIntervalForSyncLicenseDetailsInDays()).thenReturn(30L);
+ // In-memory license details: not expired and recently synced, so no re-sync is triggered.
+ lenient().when(licenseConfiguration.getLicenseValidUpto()).thenReturn(LocalDate.now().plusDays(30).toString());
+ lenient().when(licenseConfiguration.getLicenseDetailsLastUpdatedOn()).thenReturn(LocalDate.now().minusDays(5).toString());
+
GenericResponse response = licenseDetailsService.validateLicenseConfiguration();
assertTrue(response.isSuccess());
assertEquals(200, response.getResponseCode());
diff --git a/jans-config-api/plugins/admin-ui-plugin/src/test/resources/json/adminui/test-policy-store.cjar b/jans-config-api/plugins/admin-ui-plugin/src/test/resources/json/adminui/test-policy-store.cjar
new file mode 100644
index 00000000000..a344dc5001c
Binary files /dev/null and b/jans-config-api/plugins/admin-ui-plugin/src/test/resources/json/adminui/test-policy-store.cjar differ
diff --git a/jans-config-api/plugins/docs/jans-admin-ui-plugin-swagger.yaml b/jans-config-api/plugins/docs/jans-admin-ui-plugin-swagger.yaml
index da05bc82c7c..953fbd186aa 100644
--- a/jans-config-api/plugins/docs/jans-admin-ui-plugin-swagger.yaml
+++ b/jans-config-api/plugins/docs/jans-admin-ui-plugin-swagger.yaml
@@ -731,6 +731,50 @@ paths:
summary: Get Admin UI policy store
description: Get Admin UI policy store
operationId: get-adminui-policy-store
+ parameters:
+ - name: limit
+ in: query
+ description: Search size - max size of the results to return
+ schema:
+ type: integer
+ format: int32
+ default: 50
+ - name: pattern
+ in: query
+ description: Search pattern
+ schema:
+ type: string
+ default: ""
+ - name: startIndex
+ in: query
+ description: The 1-based index of the first query result
+ schema:
+ type: integer
+ format: int32
+ default: 0
+ - name: sortBy
+ in: query
+ description: Attribute whose value will be used to order the returned response
+ schema:
+ type: string
+ default: inum
+ - name: sortOrder
+ in: query
+ description: Order in which the sortBy param is applied. Allowed values are
+ "ascending" and "descending"
+ schema:
+ type: string
+ default: ascending
+ - name: fieldValuePair
+ in: query
+ description: Field and value pair for seraching
+ schema:
+ type: string
+ default: ""
+ examples:
+ Field value example:
+ description: Field value example
+ value: "scopeType=spontaneous,defaultScope=true"
responses:
"200":
description: Ok
@@ -739,7 +783,29 @@ paths:
schema:
type: array
items:
- $ref: "#/components/schemas/GenericResponse"
+ $ref: "#/components/schemas/AdminUIPolicyStore"
+ examples:
+ Response json example:
+ description: Response json example
+ value: |
+ {
+ "start": 0,
+ "totalEntriesCount": 1,
+ "entriesCount": 1,
+ "entries": [
+ {
+ "dn": "inum=e1a2b3c4-1234-5678-9abc-def012345678,ou=policy-stores,ou=admin-ui,o=jans",
+ "inum": "e1a2b3c4-1234-5678-9abc-def012345678",
+ "displayname": "Admin UI Cedarling Policy Store",
+ "description": "Cedarling policy store used to derive Admin UI role-to-scope mappings",
+ "policyStore": "UEsDBBQACAgIAABb...==",
+ "jansUsrDN": "inum=abc123,ou=people,o=jans",
+ "jansStatus": "active",
+ "creationDate": "2026-07-14T10:15:30.000Z",
+ "jansLastUpd": "2026-07-14T11:20:45.000Z"
+ }
+ ]
+ }
"400":
description: Bad Request
content:
@@ -757,18 +823,146 @@ paths:
security:
- oauth2:
- https://jans.io/oauth/jans-auth-server/config/adminui/security.readonly
+ post:
+ tags:
+ - Admin UI - Cedarling
+ summary: Create Admin UI Policy Store
+ description: Create Admin UI Policy Store
+ operationId: create-adminui-policy-store
+ requestBody:
+ description: Policy Store Object
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AdminUIPolicyStore"
+ examples:
+ Request json example:
+ description: Request json example
+ value: |
+ {
+ "displayname": "Admin UI Cedarling Policy Store",
+ "description": "Cedarling policy store used to derive Admin UI role-to-scope mappings",
+ "policyStore": "UEsDBBQACAgIAABb...==",
+ "jansStatus": "active"
+ }
+ responses:
+ "200":
+ description: Ok
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/GenericResponse"
+ examples:
+ Response json example:
+ description: Response json example
+ value: |
+ {
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Policy store saved successfully."
+ }
+ "400":
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GenericResponse"
+ "401":
+ description: Unauthorized
+ "500":
+ description: InternalServerError
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GenericResponse"
+ security:
+ - oauth2:
+ - https://jans.io/oauth/jans-auth-server/config/adminui/security.write
+ /admin-ui/security/policyStore/{INUM}:
put:
tags:
- Admin UI - Cedarling
- summary: Upload Admin UI Policy Store
- description: Upload Admin UI Policy Store
- operationId: upload-adminui-policy-store
+ summary: Edit Admin UI Policy Store
+ description: Edit Admin UI Policy Store
+ operationId: edit-adminui-policy-store
+ parameters:
+ - name: INUM
+ in: path
+ description: Policy store inum
+ required: true
+ schema:
+ type: string
requestBody:
- description: String multipart form.
+ description: Policy Store Object
content:
- multipart/form-data:
+ application/json:
schema:
$ref: "#/components/schemas/AdminUIPolicyStore"
+ examples:
+ Request json example:
+ description: Request json example
+ value: |
+ {
+ "displayname": "Admin UI Cedarling Policy Store (updated)",
+ "description": "Updated description for the Admin UI policy store",
+ "jansStatus": "inactive"
+ }
+ responses:
+ "200":
+ description: Ok
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/GenericResponse"
+ examples:
+ Response json example:
+ description: Response json example
+ value: |
+ {
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Policy store updated successfully."
+ }
+ "400":
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GenericResponse"
+ "401":
+ description: Unauthorized
+ "404":
+ description: Not Found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GenericResponse"
+ "500":
+ description: InternalServerError
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GenericResponse"
+ security:
+ - oauth2:
+ - https://jans.io/oauth/jans-auth-server/config/adminui/security.write
+ delete:
+ tags:
+ - Admin UI - Cedarling
+ summary: Delete Admin UI Policy Store
+ description: Delete Admin UI Policy Store
+ operationId: delete-adminui-policy-store
+ parameters:
+ - name: INUM
+ in: path
+ description: Policy store inum
+ required: true
+ schema:
+ type: string
responses:
"200":
description: Ok
@@ -778,6 +972,15 @@ paths:
type: array
items:
$ref: "#/components/schemas/GenericResponse"
+ examples:
+ Response json example:
+ description: Response json example
+ value: |
+ {
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Policy store deleted successfully."
+ }
"400":
description: Bad Request
content:
@@ -786,6 +989,12 @@ paths:
$ref: "#/components/schemas/GenericResponse"
"401":
description: Unauthorized
+ "404":
+ description: Not Found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GenericResponse"
"500":
description: InternalServerError
content:
@@ -814,6 +1023,15 @@ paths:
type: array
items:
$ref: "#/components/schemas/GenericResponse"
+ examples:
+ Response json example:
+ description: Response json example
+ value: |
+ {
+ "success": true,
+ "responseCode": 200,
+ "responseMessage": "Sync of role-to-scope mappings from the policy store completed successfully."
+ }
"400":
description: Bad Request
content:
@@ -1863,46 +2081,60 @@ components:
value:
type: string
AdminUIPolicyStore:
- required:
- - document
- - policyStore
- type: object
- properties:
- document:
- $ref: "#/components/schemas/Document"
- policyStore:
- type: string
- format: binary
- Document:
type: object
properties:
dn:
type: string
+ description: Distinguished Name (DN) of the policy store entry. Server-generated
+ on create; read-only and ignored on edit.
+ readOnly: true
+ example: "inum=e1a2b3c4-1234-5678-9abc-def012345678,ou=policy-stores,ou=admin-ui,o=jans"
inum:
type: string
- fileName:
- type: string
- filePath:
+ description: Unique identifier (inum) of the policy store. Server-generated
+ on create; read-only and ignored on edit.
+ readOnly: true
+ example: e1a2b3c4-1234-5678-9abc-def012345678
+ displayname:
type: string
+ description: Human-readable display name of the policy store.
+ example: Admin UI Cedarling Policy Store
description:
type: string
- document:
+ description: Free-text description of the policy store and its purpose.
+ example: Cedarling policy store used to derive Admin UI role-to-scope mappings
+ policyStore:
+ type: string
+ description: Base64-encoded Cedar policy store archive (.cjar zip). Required
+ on create; immutable and ignored on edit.
+ example: UEsDBBQACAgIAABb...==
+ jansUsrDN:
type: string
+ description: DN of the user who owns the policy store. Set at create time;
+ cannot be reassigned via edit.
+ example: "inum=abc123,ou=people,o=jans"
+ jansStatus:
+ type: string
+ description: Status of the policy store. Only one store should be 'active'
+ at a time; newly created stores default to 'inactive'.
+ example: active
+ enum:
+ - active
+ - inactive
creationDate:
type: string
+ description: Timestamp when the policy store was created. Server-managed
+ and fixed at create time.
format: date-time
- service:
- type: string
- level:
- type: integer
- format: int32
- revision:
- type: integer
- format: int32
- enabled:
- type: boolean
- baseDn:
+ readOnly: true
+ example: 2026-07-14T10:15:30Z
+ jansLastUpd:
type: string
+ description: Timestamp of the last update. Server-managed and overwritten
+ on every create/edit; any client-supplied value is ignored.
+ format: date-time
+ readOnly: true
+ example: 2026-07-14T11:20:45Z
LicenseRequest:
type: object
properties: