mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-06-03 23:09:09 -07:00
Keycloak: add clientscope management (#2905)
* Add new keycloak_clienscope module * Add description and protocol parameter + Indentation Fix * Add protocolMappers parameter * Add documentation and Fix updatating of protocolMappers * Update plugins/modules/identity/keycloak/keycloak_clientscope.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/identity/keycloak/keycloak_clientscope.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/identity/keycloak/keycloak_clientscope.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/identity/keycloak/keycloak_clientscope.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/identity/keycloak/keycloak_clientscope.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/identity/keycloak/keycloak_clientscope.py Co-authored-by: Felix Fontein <felix@fontein.de> * Add sanitize_cr(clientscoperep) function to sanitize the clientscope representation * Add unit tests for clientscope Keycloak module * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
d7c6ba89f8
commit
4a392372a8
4 changed files with 1345 additions and 0 deletions
|
@ -57,6 +57,11 @@ URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates"
|
|||
URL_GROUPS = "{url}/admin/realms/{realm}/groups"
|
||||
URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}"
|
||||
|
||||
URL_CLIENTSCOPES = "{url}/admin/realms/{realm}/client-scopes"
|
||||
URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}"
|
||||
URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models"
|
||||
URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}"
|
||||
|
||||
URL_AUTHENTICATION_FLOWS = "{url}/admin/realms/{realm}/authentication/flows"
|
||||
URL_AUTHENTICATION_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{id}"
|
||||
URL_AUTHENTICATION_FLOW_COPY = "{url}/admin/realms/{realm}/authentication/flows/{copyfrom}/copy"
|
||||
|
@ -511,6 +516,239 @@ class KeycloakAPI(object):
|
|||
self.module.fail_json(msg='Could not delete client template %s in realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def get_clientscopes(self, realm="master"):
|
||||
""" Fetch the name and ID of all clientscopes on the Keycloak server.
|
||||
|
||||
To fetch the full data of the group, make a subsequent call to
|
||||
get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return.
|
||||
|
||||
:param realm: Realm in which the clientscope resides; default 'master'.
|
||||
:return The clientscopes of this realm (default "master")
|
||||
"""
|
||||
clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(clientscopes_url, method="GET", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch list of clientscopes in realm %s: %s"
|
||||
% (realm, str(e)))
|
||||
|
||||
def get_clientscope_by_clientscopeid(self, cid, realm="master"):
|
||||
""" Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID.
|
||||
|
||||
If the clientscope does not exist, None is returned.
|
||||
|
||||
gid is a UUID provided by the Keycloak API
|
||||
:param cid: UUID of the clientscope to be returned
|
||||
:param realm: Realm in which the clientscope resides; default 'master'.
|
||||
"""
|
||||
clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(clientscope_url, method="GET", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
else:
|
||||
self.module.fail_json(msg="Could not fetch clientscope %s in realm %s: %s"
|
||||
% (cid, realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not clientscope group %s in realm %s: %s"
|
||||
% (cid, realm, str(e)))
|
||||
|
||||
def get_clientscope_by_name(self, name, realm="master"):
|
||||
""" Fetch a keycloak clientscope within a realm based on its name.
|
||||
|
||||
The Keycloak API does not allow filtering of the clientscopes resource by name.
|
||||
As a result, this method first retrieves the entire list of clientscopes - name and ID -
|
||||
then performs a second query to fetch the group.
|
||||
|
||||
If the clientscope does not exist, None is returned.
|
||||
:param name: Name of the clientscope to fetch.
|
||||
:param realm: Realm in which the clientscope resides; default 'master'
|
||||
"""
|
||||
try:
|
||||
all_clientscopes = self.get_clientscopes(realm=realm)
|
||||
|
||||
for clientscope in all_clientscopes:
|
||||
if clientscope['name'] == name:
|
||||
return self.get_clientscope_by_clientscopeid(clientscope['id'], realm=realm)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch clientscope %s in realm %s: %s"
|
||||
% (name, realm, str(e)))
|
||||
|
||||
def create_clientscope(self, clientscoperep, realm="master"):
|
||||
""" Create a Keycloak clientscope.
|
||||
|
||||
:param clientscoperep: a ClientScopeRepresentation of the clientscope to be created. Must contain at minimum the field name.
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return open_url(clientscopes_url, method='POST', headers=self.restheaders,
|
||||
data=json.dumps(clientscoperep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not create clientscope %s in realm %s: %s"
|
||||
% (clientscoperep['name'], realm, str(e)))
|
||||
|
||||
def update_clientscope(self, clientscoperep, realm="master"):
|
||||
""" Update an existing clientscope.
|
||||
|
||||
:param grouprep: A GroupRepresentation of the updated group.
|
||||
:return HTTPResponse object on success
|
||||
"""
|
||||
clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=clientscoperep['id'])
|
||||
|
||||
try:
|
||||
return open_url(clientscope_url, method='PUT', headers=self.restheaders,
|
||||
data=json.dumps(clientscoperep), validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update clientscope %s in realm %s: %s'
|
||||
% (clientscoperep['name'], realm, str(e)))
|
||||
|
||||
def delete_clientscope(self, name=None, cid=None, realm="master"):
|
||||
""" Delete a clientscope. One of name or cid must be provided.
|
||||
|
||||
Providing the clientscope ID is preferred as it avoids a second lookup to
|
||||
convert a clientscope name to an ID.
|
||||
|
||||
:param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID.
|
||||
:param cid: The ID of the clientscope (preferred to name).
|
||||
:param realm: The realm in which this group resides, default "master".
|
||||
"""
|
||||
|
||||
if cid is None and name is None:
|
||||
# prefer an exception since this is almost certainly a programming error in the module itself.
|
||||
raise Exception("Unable to delete group - one of group ID or name must be provided.")
|
||||
|
||||
# only lookup the name if cid isn't provided.
|
||||
# in the case that both are provided, prefer the ID, since it's one
|
||||
# less lookup.
|
||||
if cid is None and name is not None:
|
||||
for clientscope in self.get_clientscopes(realm=realm):
|
||||
if clientscope['name'] == name:
|
||||
cid = clientscope['id']
|
||||
break
|
||||
|
||||
# if the group doesn't exist - no problem, nothing to delete.
|
||||
if cid is None:
|
||||
return None
|
||||
|
||||
# should have a good cid by here.
|
||||
clientscope_url = URL_CLIENTSCOPE.format(realm=realm, id=cid, url=self.baseurl)
|
||||
try:
|
||||
return open_url(clientscope_url, method='DELETE', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to delete clientscope %s: %s" % (cid, str(e)))
|
||||
|
||||
def get_clientscope_protocolmappers(self, cid, realm="master"):
|
||||
""" Fetch the name and ID of all clientscopes on the Keycloak server.
|
||||
|
||||
To fetch the full data of the group, make a subsequent call to
|
||||
get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return.
|
||||
|
||||
:param cid: id of clientscope (not name).
|
||||
:param realm: Realm in which the clientscope resides; default 'master'.
|
||||
:return The protocolmappers of this realm (default "master")
|
||||
"""
|
||||
protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(id=cid, url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(protocolmappers_url, method="GET", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch list of protocolmappers in realm %s: %s"
|
||||
% (realm, str(e)))
|
||||
|
||||
def get_clientscope_protocolmapper_by_protocolmapperid(self, pid, cid, realm="master"):
|
||||
""" Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID.
|
||||
|
||||
If the clientscope does not exist, None is returned.
|
||||
|
||||
gid is a UUID provided by the Keycloak API
|
||||
|
||||
:param cid: UUID of the protocolmapper to be returned
|
||||
:param cid: UUID of the clientscope to be returned
|
||||
:param realm: Realm in which the clientscope resides; default 'master'.
|
||||
"""
|
||||
protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=pid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(protocolmapper_url, method="GET", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
else:
|
||||
self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s"
|
||||
% (pid, realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s"
|
||||
% (cid, realm, str(e)))
|
||||
|
||||
def get_clientscope_protocolmapper_by_name(self, cid, name, realm="master"):
|
||||
""" Fetch a keycloak clientscope within a realm based on its name.
|
||||
|
||||
The Keycloak API does not allow filtering of the clientscopes resource by name.
|
||||
As a result, this method first retrieves the entire list of clientscopes - name and ID -
|
||||
then performs a second query to fetch the group.
|
||||
|
||||
If the clientscope does not exist, None is returned.
|
||||
:param cid: Id of the clientscope (not name).
|
||||
:param name: Name of the protocolmapper to fetch.
|
||||
:param realm: Realm in which the clientscope resides; default 'master'
|
||||
"""
|
||||
try:
|
||||
all_protocolmappers = self.get_clientscope_protocolmappers(cid, realm=realm)
|
||||
|
||||
for protocolmapper in all_protocolmappers:
|
||||
if protocolmapper['name'] == name:
|
||||
return self.get_clientscope_protocolmapper_by_protocolmapperid(protocolmapper['id'], cid, realm=realm)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s"
|
||||
% (name, realm, str(e)))
|
||||
|
||||
def create_clientscope_protocolmapper(self, cid, mapper_rep, realm="master"):
|
||||
""" Create a Keycloak clientscope protocolmapper.
|
||||
|
||||
:param cid: Id of the clientscope.
|
||||
:param mapper_rep: a ProtocolMapperRepresentation of the protocolmapper to be created. Must contain at minimum the field name.
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(url=self.baseurl, id=cid, realm=realm)
|
||||
try:
|
||||
return open_url(protocolmappers_url, method='POST', headers=self.restheaders,
|
||||
data=json.dumps(mapper_rep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not create protocolmapper %s in realm %s: %s"
|
||||
% (mapper_rep['name'], realm, str(e)))
|
||||
|
||||
def update_clientscope_protocolmappers(self, cid, mapper_rep, realm="master"):
|
||||
""" Update an existing clientscope.
|
||||
|
||||
:param cid: Id of the clientscope.
|
||||
:param mapper_rep: A ProtocolMapperRepresentation of the updated protocolmapper.
|
||||
:return HTTPResponse object on success
|
||||
"""
|
||||
protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=mapper_rep['id'])
|
||||
|
||||
try:
|
||||
return open_url(protocolmapper_url, method='PUT', headers=self.restheaders,
|
||||
data=json.dumps(mapper_rep), validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update protocolmappers for clientscope %s in realm %s: %s'
|
||||
% (mapper_rep, realm, str(e)))
|
||||
|
||||
def get_groups(self, realm="master"):
|
||||
""" Fetch the name and ID of all groups on the Keycloak server.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue