Worteks / ansible-lemonldapng

LemonLDAP-NG Ansible role
MIT License
6 stars 4 forks source link

use lemonldap api to edit the configuration #8

Open l00ptr opened 2 years ago

l00ptr commented 2 years ago

Hello,

It could be nice to be able to adapt the config of a running LemonLDAP through the API within ansible. I know there is an API for the manager, but don't know if we have enough features there to create an Ansible module for this purpose. What do you think ? It could be fun to explore and create a first PoC with some very simple object (menucat or menuapp). If you think that's good idea, i will spend some time the few next weeks exploring this.

Best regards, l.

coudot commented 2 years ago

Hello, it is a good idea. We already tried this for W'Sweet, but code is not yet published.

l00ptr commented 2 years ago

ok, could you share this code ? I would like those features (it could be easier than writing them from scratch)

coudot commented 2 years ago

Here is an ansible module written by @maxbes to create OIDC RP through API:

#!/usr/bin/python

# Copyright: (c) 2018, Terry Jones <terry.jones@example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}

DOCUMENTATION = '''
---
module: lemonldap_oidc_rp

short_description: This module creates a new OIDC Relying Party in LemonLDAP

version_added: "2.8"

description:
    - "This module uses the Manager API to create a new OIDC Relying Party"

options:
    name:
        description:
            - The arbitrary, internal name of this party in LemonLDAP::NG
        required: true
    state:
        description:
            - Whether the party should exist or not, taking action if the state is different from what is stated
        type: str
        choices: [ absent, present ]
        default: present
    options:
        description:
            - Dictionnary of OIDC Relying Party options
        type: dict
        clientId=dict(type='str', required=False),
    clientId:
        description:
            - The OpenID Connect Client ID of this party
    redirectUris:
        description:
            - The list of allowed redirect URIs for this party
        type: list
    exported_variables:
        description:
            - Dictionnary of LemonLDAP::NG session attributes to send to this party
            - The key of each dictionnary is the name of the OIDC claim
            - The value is the name of LemonLDAP session attribute to use for this claim
        type: dict
    extra_claims:
        description:
            - Dictionary of additional scopes you want this party to support
            - The key of each dictionnary is the name of a scope
            - The value is the space-separated list of OIDC claims released by this scope
        type: dict
    api_url:
        description:
            - URL of the LemonLDAP::NG Manager API
        type: str
        required: true
    url_username:
        description:
            - Username to login on the Manager API as
        type: str
    url_password:
        description:
            - Password for the Manager API
        type: str
    timeout:
        description:
            - Request timeout for the Manager API
        type: int
        default: 60

author:
    - Your Name (@yourhandle)
'''

EXAMPLE = '''
# Create a new OIDC Relying party
- name: create
lemonldap_oidc_rp:
    api_url: https://manager-api.dev.wsweet.cloud/
    url_username: manager
    url_password: password
    name: myopenidapp
    state: present
    clientId: myclientid
    options:
        clientSecret: myclientsecret
    redirectUris:
      - https://myapp.dev.wsweet.cloud/oauth2/callback
    exported_variables:
        email: mail
        family_name: sn
        given_name: givenName
        preferred_username: uid
'''

RETURN = '''
original_message:
    description: The original name param that was passed in
    type: str
    returned: always
message:
    description: The output message that the test module generates
    type: str
    returned: always
'''

import json

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.lemonldap_api import LemonApi,lemon_base_spec

class OidcRpApi(LemonApi):

    def get_object(self):
        name = self.module.params['name']
        url = "/providers/oidc/rp/{0}".format(name)

        return self.api_request(url)

    def differs(self, current_object):
        if self.module.params['clientId'] != current_object['clientId']:
            return True
        if self._differs_hash(current_object, 'options', 'options'):
            return True
        if self._differs_hash(current_object, 'exported_variables', 'exportedVars'):
            return True
        if self._differs_hash(current_object, 'extra_claims', 'extraClaims'):
            return True
        return False

    def _differs_hash(self, current_object, option_name, payload_key):
        specification = self.module.params[option_name]
        state = current_object[payload_key]
        if specification is None:
            return False
        for keyname in specification.keys():
            if not keyname in state or specification[keyname] != state[keyname]:
                return True

    def build_payload(self,confKey = None):
        data = {}
        if not confKey is None:
            data['confKey'] = confKey
        if 'clientId' in self.module.params:
            data['clientId'] = self.module.params['clientId']
        if 'redirectUris' in self.module.params:
            data['redirectUris'] = self.module.params['redirectUris']
        if 'options' in self.module.params:
            data['options'] = self.module.params['options']
        if 'exported_variables' in self.module.params:
            data['exportedVars'] = self.module.params['exported_variables']
        if 'extra_claims' in self.module.params:
            data['extraClaims'] = self.module.params['extra_claims']
        return data

    def create(self):
        url = "/providers/oidc/rp"
        data = self.build_payload(self.module.params['name'])
        self.api_request(url, method='POST', data=data, expect=201)
        self.module.exit_json(changed=True)

    def update(self):
        url = "/providers/oidc/rp/{0}".format(self.module.params['name'])
        data = self.build_payload()
        self.api_request(url, method='PATCH', data=data, expect=204)
        self.module.exit_json(changed=True)

    def delete(self):
        url = "/providers/oidc/rp/{0}".format(self.module.params['name'])
        self.api_request(url, method='DELETE', expect=204)
        self.module.exit_json(changed=True)

def run_module():
    # define available arguments/parameters a user can pass to the module
    module_args = lemon_base_spec(dict(
        name=dict(type='str', required=True),
        clientId=dict(type='str', required=False),
        redirectUris=dict(type='list', elements='str', required=False,),
        options=dict(type='dict', required=False,),
        exported_variables=dict(type='dict', required=False,),
        extra_claims=dict(type='dict', required=False,),
        state=dict(type='str', default="present", choices=["present", "absent"]),
    ))

    # seed the result dict in the object
    # we primarily care about changed and state
    # change is if this module effectively modified the target
    # state will include any data that you want your module to pass back
    # for consumption, for example, in a subsequent task
    result = dict(
        changed=False,
    )

    # the AnsibleModule object will be our abstraction working with Ansible
    # this includes instantiation, a couple of common attr would be the
    # args/params passed to the execution, as well as if the module
    # supports check mode
    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    api = OidcRpApi(module)

    api.run()

def main():
    run_module()

if __name__ == '__main__':
    main()
l00ptr commented 2 years ago

great thx for sharing this piece of code