Skip to content

Cedarling Integration Plan

Arnab Dutta edited this page Aug 11, 2025 · 20 revisions

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.

image

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.

Sample policies created for Admin UI

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"
);

Cedarling authorization

Now testing with a sample Authz input

The user-info token has Admin UI roles in jansAdminUIRole claim.

image

Action: Jans::Action::"Read"

Resource:

{
  "app_id": "admin_ui_id",
  "id": "clients",
  "type": "Jans::Feature"
}

Result:

image
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;
};

Clone this wiki locally