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:
Gaetan2907 2021-07-19 22:39:02 +01:00 committed by GitHub
parent d7c6ba89f8
commit 4a392372a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1345 additions and 0 deletions

View file

@ -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.