-
Notifications
You must be signed in to change notification settings - Fork 19
Cedarling Integration Plan
We will be using TBAC authorization control in Admin UI. The application will include the bootstrap.json and policy-store.json (without policies) files.
bootstrap.json
{
"CEDARLING_APPLICATION_NAME": "Gluu Flex Admin UI",
"CEDARLING_AUDIT_HEALTH_INTERVAL": 0,
"CEDARLING_AUDIT_TELEMETRY_INTERVAL": 0,
"CEDARLING_DYNAMIC_CONFIGURATION": "disabled",
"CEDARLING_ID_TOKEN_TRUST_MODE": "strict",
"CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED": [
"HS256",
"RS256"
],
"CEDARLING_JWT_SIG_VALIDATION": "disabled",
"CEDARLING_JWT_STATUS_VALIDATION": "disabled",
"CEDARLING_LISTEN_SSE": "disabled",
"CEDARLING_LOCAL_JWKS": null,
"CEDARLING_LOCK": "disabled",
"CEDARLING_LOCK_MASTER_CONFIGURATION_URI": null,
"CEDARLING_LOCK_SSA_JWT": null,
"CEDARLING_LOG_LEVEL": "DEBUG",
"CEDARLING_LOG_TTL": 120,
"CEDARLING_LOG_TYPE": "memory",
"CEDARLING_POLICY_STORE_ID": "1d927bd9e20810be41fbac38529efaede03287207442",
"CEDARLING_POLICY_STORE_LOCAL": "<the policy-store string will be added here on loading Admin UI>",
"CEDARLING_PRINCIPAL_BOOLEAN_OPERATION": {
"===": [
{
"var": "Jans::User"
},
"ALLOW"
]
},
"CEDARLING_USER_AUTHZ": "enabled",
"CEDARLING_WORKLOAD_AUTHZ": "disabled"
}
policy-store.json
{
"cedar_version": "4.4.0",
"policy_stores": {
"1d927bd9e20810be41fbac38529efaede03287207442": {
"name": "adminui_tbac_store",
"description": "Admin UI TBAC store",
"policies": {
"27c7009394b34dff13314bd1e5d833be795a74b14c78": {
"description": "",
"creation_date": "2025-06-24T19:03:54.710311",
"policy_content": ""
}
},
"trusted_issuers": {
"516ccb6d665d2ab37655a3a86d2d496495496c47015a": {
"name": "AdminUITrustedIssuer",
"description": "Admin UI Trusted Issuer",
"openid_configuration_endpoint": "https://admin-ui-test.gluu.org/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"trusted": true,
"entity_type_name": "Jans::Access_token",
"user_id": "sub",
"token_id": "jti",
"workload_id": "rp_id",
"claim_mapping": {},
"required_claims": [
"jti",
"iss",
"aud",
"sub",
"exp",
"nbf"
],
"principal_mapping": [
"Jans::Workload"
]
},
"id_token": {
"trusted": true,
"entity_type_name": "Jans::id_token",
"user_id": "sub",
"token_id": "jti",
"claim_mapping": {},
"principal_mapping": [
"Jans::User"
]
},
"userinfo_token": {
"trusted": true,
"entity_type_name": "Jans::Userinfo_token",
"user_id": "sub",
"token_id": "jti",
"role_mapping": "jansAdminUIRole",
"claim_mapping": {},
"principal_mapping": [
"Jans::User"
]
}
}
}
},
"schema": "eyJKYW5zIjp7ImNvbW1vblR5cGVzIjp7IkNvbnRleHQiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiY3VycmVudF90aW1lIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IkxvbmcifSwiZGV2aWNlX2hlYWx0aCI6eyJ0eXBlIjoiU2V0IiwicmVxdWlyZWQiOmZhbHNlLCJlbGVtZW50Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX0sImZyYXVkX2luZGljYXRvcnMiOnsidHlwZSI6IlNldCIsInJlcXVpcmVkIjpmYWxzZSwiZWxlbWVudCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19LCJnZW9sb2NhdGlvbiI6eyJ0eXBlIjoiU2V0IiwicmVxdWlyZWQiOmZhbHNlLCJlbGVtZW50Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX0sIm5ldHdvcmsiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiU3RyaW5nIn0sIm5ldHdvcmtfdHlwZSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifSwib3BlcmF0aW5nX3N5c3RlbSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifSwidXNlcl9hZ2VudCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifX19LCJlbWFpbF9hZGRyZXNzIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImRvbWFpbiI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInVpZCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19fSwiVXJsIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7Imhvc3QiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJwYXRoIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwicHJvdG9jb2wiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJlbnRpdHlUeXBlcyI6eyJBY2Nlc3NfdG9rZW4iOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiYXVkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiZXhwIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlhdCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiTG9uZyJ9LCJpc3MiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlRydXN0ZWRJc3N1ZXIifSwianRpIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9LCJuYmYiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiTG9uZyJ9LCJzY29wZSI6eyJ0eXBlIjoiU2V0IiwicmVxdWlyZWQiOmZhbHNlLCJlbGVtZW50Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fX0sIkZlYXR1cmUiOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnt9fX0sIkhUVFBfUmVxdWVzdCI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJoZWFkZXIiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiQWNjZXB0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9fX0sInVybCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVXJsIn19fX0sImlkX3Rva2VuIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImFjciI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifSwiYW1yIjp7InR5cGUiOiJTZXQiLCJyZXF1aXJlZCI6ZmFsc2UsImVsZW1lbnQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fSwiYXVkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiYXpwIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9LCJiaXJ0aGRhdGUiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiU3RyaW5nIn0sImVtYWlsIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6ImVtYWlsX2FkZHJlc3MifSwiZXhwIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlhdCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiTG9uZyJ9LCJpc3MiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlRydXN0ZWRJc3N1ZXIifSwianRpIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9LCJuYW1lIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9LCJwaG9uZV9udW1iZXIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiU3RyaW5nIn0sInJvbGUiOnsidHlwZSI6IlNldCIsInJlcXVpcmVkIjpmYWxzZSwiZWxlbWVudCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJSb2xlIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7fX19LCJUcnVzdGVkSXNzdWVyIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7Imlzc3Vlcl9lbnRpdHlfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlVybCJ9fX19LCJVc2VyIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImVtYWlsIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6ImVtYWlsX2FkZHJlc3MifSwiaWRfdG9rZW4iOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiaWRfdG9rZW4ifSwicGhvbmVfbnVtYmVyIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJ1c2VyaW5mb190b2tlbiI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJVc2VyaW5mb190b2tlbiJ9LCJ1c2VybmFtZSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifX19LCJtZW1iZXJPZlR5cGVzIjpbIlJvbGUiLCJGZWF0dXJlIl19LCJVc2VyaW5mb190b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJiaXJ0aGRhdGUiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiU3RyaW5nIn0sImVtYWlsIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6ImVtYWlsX2FkZHJlc3MifSwiZXhwIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IkxvbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sImp0aSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifSwibmFtZSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifSwicGhvbmVfbnVtYmVyIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IlN0cmluZyJ9LCJqYW5zQWRtaW5VSVJvbGUiOnsidHlwZSI6IlNldCIsImVsZW1lbnQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fSwic3ViIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiV29ya2xvYWQiOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiYWNjZXNzX3Rva2VuIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsInJlcXVpcmVkIjpmYWxzZSwibmFtZSI6IkFjY2Vzc190b2tlbiJ9LCJjbGllbnRfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJpc3MiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlRydXN0ZWRJc3N1ZXIifSwibmFtZSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifSwicnBfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwicmVxdWlyZWQiOmZhbHNlLCJuYW1lIjoiU3RyaW5nIn0sInNwaWZmZV9pZCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJyZXF1aXJlZCI6ZmFsc2UsIm5hbWUiOiJTdHJpbmcifX19fX0sImFjdGlvbnMiOnsiRGVsZXRlIjp7ImFwcGxpZXNUbyI6eyJwcmluY2lwYWxUeXBlcyI6WyJVc2VyIl0sInJlc291cmNlVHlwZXMiOlsiRmVhdHVyZSJdLCJjb250ZXh0Ijp7InR5cGUiOiJDb250ZXh0In19fSwiRXhlY3V0ZSI6eyJhcHBsaWVzVG8iOnsicHJpbmNpcGFsVHlwZXMiOlsiVXNlciJdLCJyZXNvdXJjZVR5cGVzIjpbIkZlYXR1cmUiXSwiY29udGV4dCI6eyJ0eXBlIjoiQ29udGV4dCJ9fX0sIlJlYWQiOnsiYXBwbGllc1RvIjp7InByaW5jaXBhbFR5cGVzIjpbIlVzZXIiXSwicmVzb3VyY2VUeXBlcyI6WyJGZWF0dXJlIl0sImNvbnRleHQiOnsidHlwZSI6IkNvbnRleHQifX19LCJXcml0ZSI6eyJhcHBsaWVzVG8iOnsicHJpbmNpcGFsVHlwZXMiOlsiVXNlciJdLCJyZXNvdXJjZVR5cGVzIjpbIkZlYXR1cmUiXSwiY29udGV4dCI6eyJ0eXBlIjoiQ29udGV4dCJ9fX19fX0="
}
}
}
The administrator can map the Admin UI Role with one or more Permission(s) using the Role-Permission Mapping page. The Role mapped with Permissions can be then assigned to the user to allow access to the corresponding operations of the GUI.

Based on role-pemission mapping data Admin UI will create policies and add those to policy store using below javascript function. It takes policy-store json (without policies) and role-permission mapping as input and returns policy-store with policies.
function mapRolePermissions(allPermissions, rolePermissionList) {
// Create a map from permission string to its tag
const permissionTagMap = new Map();
for (const perm of allPermissions) {
permissionTagMap.set(perm.permission, perm.tag);
}
// Process each role
const result = rolePermissionList.map(roleObj => {
const mappedPermissions = roleObj.permissions
.map(perm => {
const tag = permissionTagMap.get(perm);
if (tag) {
return { name: perm, tag };
} else {
return null; // Ignore unknown permissions
}
})
.filter(p => p !== null); // Remove nulls
return {
role: roleObj.role,
permissions: mappedPermissions
};
});
return result;
}
function generateCedarPolicies(policyStoreJson, rolePermissionMapping) {
const determineAction = (permission) => {
if (permission.endsWith('.readonly') || permission.endsWith('.read')) {
return 'Read';
} else if (permission.endsWith('.write')) {
return 'Write';
} else if (permission.endsWith('.delete')) {
return 'Delete';
} else {
return 'Execute';
}
};
const formatPolicy = (role, action, resource) => {
return `permit (
principal in Jans::Role::"${role}",
action in Jans::Action::"${action}",
resource == Jans::Feature::"${resource.tag}"
);`;
};
const storeId = Object.keys(policyStoreJson.policy_stores)[0];
const policyStore = policyStoreJson.policy_stores[storeId];
if (!policyStore.policies) {
policyStore.policies = {};
}
// Remove policies with empty policy_content
for (const [key, value] of Object.entries(policyStore.policies)) {
if (!value.policy_content || value.policy_content.trim() === "") {
delete policyStore.policies[key];
}
}
rolePermissionMapping.forEach(entry => {
const { role, permissions } = entry;
permissions.forEach(permission => {
const action = determineAction(permission.name);
const policy = formatPolicy(role, action, permission);
const encoded = btoa(policy); // base64 encode
const policyId = crypto.randomUUID(); // Generate random ID
policyStore.policies[policyId] = {
description: `Policy for ${role} to ${action} ${permission}`,
creation_date: new Date().toISOString(),
policy_content: encoded
};
});
});
return policyStoreJson;
}
The policy-store json will be converted into string and will be set to CEDARLING_POLICY_STORE_LOCAL
field of bootstrap json. This bootstrap json will be used to initialise cedarling on loading Admin UI on browser.
Let's see policies created for OIDC Clients feature by the javascript function. We have following mapping done in Admin UI.
[
{
"role": "api-viewer",
"permissions": ["https://jans.io/oauth/config/openid/clients.readonly"]
},
{
"role": "api-editor",
"permissions": ["https://jans.io/oauth/config/openid/clients.readonly","https://jans.io/oauth/config/openid/clients.write"]
},
{
"role": "api-manager",
"permissions": ["https://jans.io/oauth/config/openid/clients.readonly","https://jans.io/oauth/config/openid/clients.write"]
},
{
"role": "api-admin",
"permissions": ["https://jans.io/oauth/config/openid/clients.readonly","https://jans.io/oauth/config/openid/clients.write","https://jans.io/oauth/config/openid/clients.delete"]
},
...
]
So the generated policies will be like:
// api-viewer policies
----------------------
permit (
principal in Jans::Role::"api-viewer",
action in Jans::Action::"Read",
resource == Jans::Feature::"clients"
);
// api-editor policies
----------------------
permit (
principal in Jans::Role::"api-editor",
action in Jans::Action::"Read",
resource == Jans::Feature::"clients"
);
permit (
principal in Jans::Role::"api-editor",
action in Jans::Action::"Write",
resource == Jans::Feature::"clients"
);
//api-manager policies
----------------------
permit (
principal in Jans::Role::"api-manager",
action in Jans::Action::"Read",
resource == Jans::Feature::"clients"
);
permit (
principal in Jans::Role::"api-manager",
action in Jans::Action::"Write",
resource == Jans::Feature::"clients"
);
//api-admin policies
--------------------
permit (
principal in Jans::Role::"api-admin",
action in Jans::Action::"Read",
resource == Jans::Feature::"clients"
);
permit (
principal in Jans::Role::"api-admin",
action in Jans::Action::"Write",
resource == Jans::Feature::"clients"
);
permit (
principal in Jans::Role::"api-admin",
action in Jans::Action::"Delete",
resource == Jans::Feature::"clients"
);
Now testing with a sample Authz input
The user-info token has Admin UI roles in jansAdminUIRole
claim.

Action: Jans::Action::"Read"
Resource:
{
"app_id": "admin_ui_id",
"id": "clients",
"type": "Jans::Feature"
}
Result:

const generateCedarPolicies = (
rolePermissionMapping: RolePermissionMapping,
): RuntimePolicyStoreConfig => {
// Validate input
if (!Array.isArray(rolePermissionMapping)) {
throw new Error('Invalid rolePermissionMapping: must be an array');
}
// Load existing policy store config or create a default one
let policyStoreJson: RuntimePolicyStoreConfig;
try {
const configSource = process.env.POLICY_STORE_CONFIG;
const initialPolicyJson =
typeof configSource === 'string'
? JSON.parse(configSource)
: typeof configSource === 'object' && configSource !== null
? configSource
: { policy_stores: {} };
policyStoreJson = updateOpenIdConfigurationEndpoint(initialPolicyJson as ExtendedPolicyStoreConfig)
as unknown as RuntimePolicyStoreConfig;
} catch (error) {
console.warn('Failed to parse POLICY_STORE_CONFIG, using default structure:', error);
policyStoreJson = { policy_stores: {} };
}
// Ensure store structure
if (typeof policyStoreJson.policy_stores !== 'object' || !policyStoreJson.policy_stores) {
policyStoreJson.policy_stores = {};
}
let storeId = Object.keys(policyStoreJson.policy_stores)[0];
if (!storeId) {
storeId = 'default-store';
policyStoreJson.policy_stores[storeId] = { policies: {} };
}
const policyStore = policyStoreJson.policy_stores[storeId];
if (typeof policyStore.policies !== 'object' || !policyStore.policies) {
policyStore.policies = {};
}
// Remove empty policies
for (const [key, value] of Object.entries(policyStore.policies)) {
if (!value?.policy_content?.trim()) {
delete policyStore.policies[key];
}
}
// Action determination logic
const determineAction = (permission: string): string => {
if (/(readonly|read|read-all|search)$/.test(permission)) return 'Read';
if (permission.endsWith('write')) return 'Write';
if (permission.endsWith('delete')) return 'Delete';
return 'Execute';
};
// Group by role + action
const grouped = new Map<string, { role: string; action: string; resources: Set<string> }>();
for (const entry of rolePermissionMapping) {
if (!entry?.role || !Array.isArray(entry.permissions)) {
console.warn('Skipping invalid role permission entry:', entry);
continue;
}
for (const permission of entry.permissions) {
if (!permission?.name || !permission?.tag) {
console.warn('Skipping invalid permission:', permission);
continue;
}
const action = determineAction(permission.name);
const key = `${entry.role}::${action}`;
if (!grouped.has(key)) {
grouped.set(key, { role: entry.role, action, resources: new Set() });
}
grouped.get(key)!.resources.add(permission.tag);
}
}
// Create one policy per group
for (const { role, action, resources } of grouped.values()) {
const resourceList = Array.from(resources)
.map(tag => `Jans::Feature::"${tag}"`)
.join(",\n ");
const policy = `permit (
principal in Jans::Role::"${role}",
action in Jans::Action::"${action}",
resource in [
${resourceList}
]
);`;
try {
const encoded = btoa(policy);
const policyId = crypto.randomUUID();
policyStore.policies[policyId] = {
description: `Policy for ${role} to ${action} multiple resources`,
creation_date: new Date().toISOString(),
policy_content: encoded,
};
} catch (error) {
console.error(`Failed to generate policy for role ${role}, action ${action}:`, error);
}
}
return policyStoreJson;
};