Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# AGENTS.md

Guidance for coding agents working in `mosip/keycloak`.

## Keycloak init mappers

`keycloak-init/keycloak_init.py` creates protocol mappers. Each client `mappers` entry is passed to `create_mapper`, which calls `mapper_payload`. Add a new audience through that list. Do not add another mapper implementation.

- Leave `protocol_mapper` unset, or set `oidc-usermodel-attribute-mapper`, for a user-attribute mapper. Those entries use `mapper_name`, `mapper_user_attribute`, and `token_claim_name`.
- Set `protocol_mapper: oidc-audience-mapper` to add one value to the access token `aud`. Set exactly one of `included_client_audience` (a client id in the same realm) or `included_custom_audience` (a literal audience). The script sets `access.token.claim` to `true`. An entry with both fields is rejected, because Keycloak keeps `included.client.audience` and ignores `included.custom.audience`.
- Any other `protocol_mapper` is rejected. `validate_configured_mappers` checks every entry before a realm or client is created or updated. One bad entry stops the run before Keycloak is changed.
- A re-run skips a mapper name that already exists (HTTP 409). The stored mapper is left as it is. To change an audience, delete that mapper in Keycloak and run init again. Any other Keycloak error is raised.
- Chart mapper lists live in `helm/keycloak-init/values.yaml`. Add an audience entry only on the client that needs it. Existing user-attribute mappers stay unchanged.
8 changes: 8 additions & 0 deletions helm/keycloak-init/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -224,44 +224,52 @@
- name: add_oidc_client
description: Scope required to create OIDC client
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 227 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

227:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "false", include.in.token.scope: "true"}
- name: update_oidc_client
description: ''
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 232 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

232:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "false", include.in.token.scope: "true"}
- name: get_certificate
description: Scope required to create OIDC client
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 237 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

237:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "false", include.in.token.scope: "true"}
- name: upload_certificate
description: ''
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 242 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

242:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "false", include.in.token.scope: "true"}
- name: individual_id
description: Scope required to create resident client
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 247 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

247:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "true", include.in.token.scope: "true"}
- name: ida_token
description: ''
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 252 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

252:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "true", include.in.token.scope: "true"}
- name: send_binding_otp
description: Scope required to create mpartner-default-mobile client
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 257 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

257:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "false", include.in.token.scope: "true"}
- name: wallet_binding
description: Scope required to create mpartner-default-mobile client
protocol: openid-connect
"Include In Token Scope": on

Check warning on line 262 in helm/keycloak-init/values.yaml

View workflow job for this annotation

GitHub Actions / chart-lint-publish / chart-lint-publish

262:37 [truthy] truthy value should be one of [false, true]
attributes: {display.on.consent.screen: "false", include.in.token.scope: "true"}
clients:
# A mapper without protocol_mapper stays oidc-usermodel-attribute-mapper
# (mapper_name, mapper_user_attribute, token_claim_name).
# Add an audience on the client that needs it:
# - mapper_name: esignet-audience
# protocol_mapper: oidc-audience-mapper
# included_client_audience: target-client-id
# Use included_custom_audience instead when the audience is not a client id.
# Set only one of those fields.
- name: mosip-abis-client
mappers: []
saroles: []
Expand Down
28 changes: 28 additions & 0 deletions keycloak-init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,34 @@ Keycloak will feature the Mosip theme as the default theme. To apply a customize

Note: We are adding theme as a part of keycloak_init

# Protocol mappers

A client `mappers` entry creates a protocol mapper. `protocol_mapper` selects the type. When it is omitted, or set to `oidc-usermodel-attribute-mapper`, the mapper stays a user-attribute mapper and uses `mapper_user_attribute` plus `token_claim_name`.

An `oidc-audience-mapper` entry adds one audience to the access token `aud` claim. Set either `included_client_audience` (another client's client id in the same realm) or `included_custom_audience` (a literal audience), not both. Keycloak keeps `included.client.audience` and ignores `included.custom.audience` when both are present, so the script rejects that combination. The script sets `access.token.claim=true`.

`protocol_mapper` must be omitted, `oidc-usermodel-attribute-mapper`, or `oidc-audience-mapper`. Any other value stops init before Keycloak is changed.

Every mapper entry is checked before a realm or client is created or updated. One bad entry stops that run without applying it.

```yaml
clients:
- name: mosip-pms-client
mappers:
- mapper_name: partnerType
mapper_user_attribute: partnerType
token_claim_name: partnerType
- mapper_name: esignet-audience
protocol_mapper: oidc-audience-mapper
included_client_audience: mosip-esignet-client
- mapper_name: custom-audience
protocol_mapper: oidc-audience-mapper
included_custom_audience: https://api.example.org
saroles: []
```

A re-run does not change a mapper whose name already exists. Keycloak returns 409 and the script skips it. To change `included_client_audience` or `included_custom_audience` on a mapper that is already in the realm, delete that mapper in Keycloak and run init again.

# Keycloak Roles Assignment to Client
This "assign_client_roles" method within the script is used to assign roles to clients within a specified realm in Keycloak using keycloak admin API. This method handles retrieving the roles from the values.yaml file, fetching the client ID, and assigning the specified roles within the values.yaml file to the client directly.

Expand Down
92 changes: 71 additions & 21 deletions keycloak-init/keycloak_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,70 @@
from keycloak.exceptions import raise_error_from_response, KeycloakError, KeycloakGetError
from keycloak.urls_patterns import URL_ADMIN_USER_REALM_ROLES

USER_ATTRIBUTE_MAPPER = "oidc-usermodel-attribute-mapper"
AUDIENCE_MAPPER = "oidc-audience-mapper"

def mapper_payload(mapper):
"""Build the Keycloak protocol-mapper body for one realm YAML mapper entry.

Entries without protocol_mapper stay user-attribute mappers. An
oidc-audience-mapper entry sets exactly one of included_client_audience
or included_custom_audience on the access token aud.
"""
protocol_mapper = mapper.get("protocol_mapper") or USER_ATTRIBUTE_MAPPER
if protocol_mapper not in (USER_ATTRIBUTE_MAPPER, AUDIENCE_MAPPER):
raise ValueError(
'mapper "%s" has unknown protocol_mapper "%s"'
% (mapper.get("mapper_name"), protocol_mapper)
)
if protocol_mapper == AUDIENCE_MAPPER:
client_audience = str(mapper.get("included_client_audience") or "").strip()
custom_audience = str(mapper.get("included_custom_audience") or "").strip()
if client_audience and custom_audience:
raise ValueError(
'audience mapper "%s" must set only one of included_client_audience or included_custom_audience'
% mapper.get("mapper_name")
)
config = {"access.token.claim": "true"}
if client_audience:
config["included.client.audience"] = client_audience
elif custom_audience:
config["included.custom.audience"] = custom_audience
else:
raise ValueError(
'audience mapper "%s" needs included_client_audience or included_custom_audience'
% mapper.get("mapper_name")
)
else:
config = {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"multivalued": "",
"aggregate.attrs": "",
"user.attribute": mapper["mapper_user_attribute"],
"claim.name": mapper["token_claim_name"],
"jsonType.label": "String",
}
return {
"protocol": "openid-connect",
"config": config,
"name": mapper["mapper_name"],
"protocolMapper": protocol_mapper,
}

def validate_configured_mappers(values):
"""Fail before any realm or client is created when a mapper entry is invalid."""
for realm, body in values.items():
if realm == "del_realms" or not isinstance(body, dict):
continue
for client in body.get("clients") or []:
if not isinstance(client, dict):
continue
for mapper in client.get("mappers") or []:
if mapper:
mapper_payload(mapper)

class KeycloakSession:
def __init__(self, realm, server_url, user, pwd, ssl_verify):
self.keycloak_admin = KeycloakAdmin(server_url=server_url,
Expand Down Expand Up @@ -425,35 +489,20 @@ def create_client(self, realm, client, secret, sa_roles=None):

def create_mapper(self, realm, client, mapper, skip_exists=False):
self.keycloak_admin.realm_name = realm # work around because otherwise client was getting created in master
client_id = self.keycloak_admin.get_client_id(client)
mapper_url = 'admin/realms/'+realm+'/clients/'+client_id+'/protocol-mappers/models'
payload = {
"protocol":"openid-connect",
"config": {
"id.token.claim":"true",
"access.token.claim":"true",
"userinfo.token.claim":"true",
"multivalued":"",
"aggregate.attrs":"",
"user.attribute":mapper['mapper_user_attribute'],
"claim.name":mapper['token_claim_name'],
"jsonType.label":"String"
},
"name":mapper['mapper_name'],
"protocolMapper":"oidc-usermodel-attribute-mapper"
}
try:
client_id = self.keycloak_admin.get_client_id(client)
mapper_url = 'admin/realms/'+realm+'/clients/'+client_id+'/protocol-mappers/models'
payload = mapper_payload(mapper)
print('\t\tCreating Mapper %s' % mapper['mapper_name'])
data_raw = self.keycloak_admin.connection.raw_post(mapper_url, data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, skip_exists=skip_exists)

except KeycloakError as e:
if e.response_code == 409:
print('\t\tMapper %s Exists already exists; SKIPPING;' % mapper['mapper_name'])

except:
self.keycloak_admin.realm_name = 'master' # restore
return
raise
finally:
self.keycloak_admin.realm_name = 'master' # restore

def create_user(self, realm, uname, email, fname, lname, password, temp_flag, attributes={}):
self.keycloak_admin.realm_name = realm
Expand Down Expand Up @@ -656,6 +705,7 @@ def main():

fp = open(input_yaml, 'rt')
values = yaml.load(fp, Loader=yaml.FullLoader)
validate_configured_mappers(values)

server_url = server_url # Full url to access api

Expand Down
Loading