diff --git a/README.md b/README.md index 428c73f..3248d3b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ and managing nextcloud installations server instance. Can install, remove, enable/disable and update apps. - [`ldap_user_backend`](roles/ldap_user_backend/README.md): Manages LDAP authentication sources in installed nextcloud instances. +- [`oidc_user_backend`](roles/oidc_user_backend/README.md): + Manage OIDC authentication sources in installed nextcloud instances. - [`nginx_fpm_proxy`](roles/nginx_fpm_proxy/README.md): Reverse proxy role which connects to nextcloud using FPM and serves static content. diff --git a/roles/oidc_user_backend/README.md b/roles/oidc_user_backend/README.md new file mode 100644 index 0000000..f4500d4 --- /dev/null +++ b/roles/oidc_user_backend/README.md @@ -0,0 +1,29 @@ +# `finallycoffee.nextcloud.oidc_user_backend` ansible role + +Configure OIDC user backends in nextcloud using this ansible role. +This role can be run multiple times with different arguments in order to +configure multiple oidc-based user backends. + +> [!WARNING] +> This role is not production ready or finished + +## Configuration + +Set `oidc_user_backend_provider_identifier` to a unique identifier. +Populate your provider information in the `oidc_user_backend_config_provider_provider_(settings_)` +like this: + +```yaml +oidc_user_backend_config_provider_identifier: my_provider +oidc_user_backend_config_provider_discovery_endpoint: https://idp.example.com/ +oidc_user_backend_config_provider_client_id: my-client-id +oidc_user_backend_config_provider_client_secret: my-client-secret + +# All options to the occ command are avaible in the +# `oidc_user_backend_config_provider_settings_` namespace +oidc_user_backend_config_provider_settings_unique_id: true +oidc_user_backend_config_provider_settings_send_id_token_hint: true +oidc_user_backend_config_provider_settings_mapping_display_name: name +oidc_user_backend_config_provider_settings_mapping_uid: preferred_username +oidc_user_backend_config_provider_settings_mapping_email: email +``` diff --git a/roles/oidc_user_backend/defaults/main/config.yml b/roles/oidc_user_backend/defaults/main/config.yml new file mode 100644 index 0000000..145a9ab --- /dev/null +++ b/roles/oidc_user_backend/defaults/main/config.yml @@ -0,0 +1,40 @@ +--- +oidc_user_backend_config_provider_identifier: ~ +oidc_user_backend_config_provider_client_id: ~ +oidc_user_backend_config_provider_client_secret: ~ +oidc_user_backend_config_provider_discovery_endpoint: ~ +oidc_user_backend_config_provider_end_session_endpoint: ~ +oidc_user_backend_config_provider_scopes: + - openid + - email + - profile + +oidc_user_backend_config_provider_settings_unique_id: true +oidc_user_backend_config_provider_settings_check_bearer: true +oidc_user_backend_config_provider_settings_send_id_token_hint: true +oidc_user_backend_config_provider_settings_bearer_provisioning: false +oidc_user_backend_config_provider_settings_extra_claims: [] +oidc_user_backend_config_provider_settings_provider_based_id: false +oidc_user_backend_config_provider_settings_group_provisioning: false + +oidc_user_backend_config_provider_settings_mapping_display_name: name +oidc_user_backend_config_provider_settings_mapping_email: email +oidc_user_backend_config_provider_settings_mapping_quota: ~ +oidc_user_backend_config_provider_settings_mapping_uid: sub +oidc_user_backend_config_provider_settings_mapping_groups: ~ +oidc_user_backend_config_provider_settings_mapping_address: ~ +oidc_user_backend_config_provider_settings_mapping_street_address: ~ +oidc_user_backend_config_provider_settings_mapping_postal_code: ~ +oidc_user_backend_config_provider_settings_mapping_locality: ~ +oidc_user_backend_config_provider_settings_mapping_region: ~ +oidc_user_backend_config_provider_settings_mapping_country: ~ +oidc_user_backend_config_provider_settings_mapping_website: ~ +oidc_user_backend_config_provider_settings_mapping_avatar: ~ +oidc_user_backend_config_provider_settings_mapping_twitter: ~ +oidc_user_backend_config_provider_settings_mapping_fediverse: ~ +oidc_user_backend_config_provider_settings_mapping_organisation: ~ +oidc_user_backend_config_provider_settings_mapping_role: ~ +oidc_user_backend_config_provider_settings_mapping_headline: ~ +oidc_user_backend_config_provider_settings_mapping_biography: ~ +oidc_user_backend_config_provider_settings_mapping_phone: ~ +oidc_user_backend_config_provider_settings_mapping_gender: ~ diff --git a/roles/oidc_user_backend/defaults/main/main.yml b/roles/oidc_user_backend/defaults/main/main.yml new file mode 100644 index 0000000..1b35e54 --- /dev/null +++ b/roles/oidc_user_backend/defaults/main/main.yml @@ -0,0 +1,5 @@ +--- +oidc_user_backend_deployment_method: host +oidc_user_backend_deployment_become_user: ~ +oidc_user_backend_deployment_method_docker_container_name: nextcloud +oidc_user_backend_deployment_method_podman_container_name: nextcloud diff --git a/roles/oidc_user_backend/defaults/main/occ.yml b/roles/oidc_user_backend/defaults/main/occ.yml new file mode 100644 index 0000000..6e679f3 --- /dev/null +++ b/roles/oidc_user_backend/defaults/main/occ.yml @@ -0,0 +1,15 @@ +--- +oidc_user_backend_occ_command: "php occ" +oidc_user_backend_occ_user_oidc_provider_identifier: >-2 + {{ oidc_user_backend_config_provider_identifier }} +oidc_user_backend_force_update: false + +oidc_user_backend_occ_user_oidc_provider_set_command: >-2 + {{ oidc_user_backend_occ_command }} user_oidc:provider + {{ oidc_user_backend_occ_user_oidc_provider_options }} + {{ oidc_user_backend_occ_user_oidc_provider_identifier }} +oidc_user_backend_occ_user_oidc_provider_list_command: >-2 + {{ oidc_user_backend_occ_command }} user_oidc:provider --output=json +oidc_user_backend_occ_user_oidc_provider_get_command: >-2 + {{ oidc_user_backend_occ_command }} user_oidc:provider --output=json + {{ oidc_user_backend_occ_user_oidc_provider_identifier }} diff --git a/roles/oidc_user_backend/tasks/execute-occ.yml b/roles/oidc_user_backend/tasks/execute-occ.yml new file mode 100644 index 0000000..2ca325b --- /dev/null +++ b/roles/oidc_user_backend/tasks/execute-occ.yml @@ -0,0 +1,40 @@ +--- +- name: Execute OCC command (host) + ansible.builtin.command: + cmd: "{{ oidc_user_backend_occ_command_to_exec }}" + become_user: "{{ oidc_user_backend_occ_user_to_become }}" + register: oidc_user_backend_occ_command_result_host + when: oidc_user_backend_deployment_method == 'host' + +- name: Execute OCC command (docker) + community.docker.docker_container_exec: + container: >-2 + {{ oidc_user_backend_deployment_method_docker_container_name }} + command: "{{ oidc_user_backend_occ_command_to_exec }}" + user: "{{ oidc_user_backend_occ_user_to_become | default(omit, true) }}" + register: oidc_user_backend_occ_command_result_docker + when: oidc_user_backend_deployment_method == 'docker' + +- name: Execute OCC command (podman) + containers.podman.podman_container_exec: + name: >-2 + {{ oidc_user_backend_deployment_method_podman_container_name }} + command: "{{ oidc_user_backend_occ_command_to_exec }}" + user: "{{ oidc_user_backend_occ_user_to_become | default(omit, true) }}" + register: oidc_user_backend_occ_command_result_podman + when: oidc_user_backend_deployment_method == 'podman' + +- name: Register result into variable + ansible.builtin.set_fact: { + "{{ oidc_user_backend_occ_command_result_var }}" : "{{ + oidc_user_backend_occ_command_result.stdout | string | from_json + }}" + } + vars: + oidc_user_backend_occ_result_map: + host: "{{ oidc_user_backend_occ_command_result_host }}" + docker: "{{ oidc_user_backend_occ_command_result_docker }}" + podman: "{{ oidc_user_backend_occ_command_result_podman }}" + oidc_user_backend_occ_command_result: >-2 + {{ oidc_user_backend_occ_result_map[oidc_user_backend_deployment_method] + | default(false, true) }} diff --git a/roles/oidc_user_backend/tasks/main.yml b/roles/oidc_user_backend/tasks/main.yml new file mode 100644 index 0000000..9297fe8 --- /dev/null +++ b/roles/oidc_user_backend/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Check if deployment method is supported + ansible.builtin.fail: + msg: >-2 + Deployment method '{{ oidc_user_backend_deployment_method }}' is not supported! + Supported are: {{ oidc_user_backend_deployment_methods | join(', ') }} + when: oidc_user_backend_deployment_method not in oidc_user_backend_deployment_methods + +- name: Lookup become user info + ansible.builtin.user: + name: "{{ oidc_user_backend_deployment_become_user }}" + state: present + check_mode: true + register: oidc_user_backend_deployment_become_user_info + when: oidc_user_backend_deployment_become_user | default(false, true) + +- name: Retrieve configured providers + ansible.builtin.include_tasks: + file: execute-occ.yml + vars: + oidc_user_backend_occ_command_to_exec: >- + {{ oidc_user_backend_occ_user_oidc_provider_get_command }} + oidc_user_backend_occ_user_to_become: "{{ oidc_user_backend_deployment_become_user_info.uid }}" + oidc_user_backend_occ_command_result_var: "oidc_user_backend_occ_user_oidc_provider" + +- name: Check if provider information should be updated + set_fact: + oidc_user_backend_backend_force_update: true + loop: "{{ lookup('ansible.utils.to_paths', oidc_user_backend_occ_user_oidc_provider) | dict2items }}" + loop_control: + label: "{{ item.key }}" + vars: + target_config: >-2 + {{ lookup('ansible.utils.to_paths', oidc_user_backend_occ_user_oidc_config_provider_dict) }} + when: + - item.key not in oidc_user_backend_occ_user_oidc_provider_ignored_settings + - (item.value != None) and (target_config[item.key] != None) + - >-2 + (target_config[item.key] != None) | ternary( + (item.value != target_config[item.key]), + (item.value | string | length > 0) + ) + +- name: Update configuration for provider '{{ oidc_user_backend_config_provider_identifier }}' + ansible.builtin.include_tasks: + file: execute-occ.yml + vars: + occ_command: >- + {{ oidc_user_backend_occ_user_oidc_provider_set_command }} + occ_become_user: "{{ oidc_user_backend_deployment_become_user_info.uid }}" + occ_command_result_var: "oidc_user_backend_occ_user_oidc_provider_update_info" + when: oidc_user_backend_backend_force_update | default(false, true) diff --git a/roles/oidc_user_backend/vars/main/main.yml b/roles/oidc_user_backend/vars/main/main.yml new file mode 100644 index 0000000..cdf31d0 --- /dev/null +++ b/roles/oidc_user_backend/vars/main/main.yml @@ -0,0 +1,9 @@ +--- +oidc_user_backend_deployment_methods: + - host + - docker + - podman + +oidc_user_backend_occ_user_oidc_provider_ignored_settings: + - 'id' + - 'settings.bearerProvisioning' diff --git a/roles/oidc_user_backend/vars/main/provider-options-json.yml b/roles/oidc_user_backend/vars/main/provider-options-json.yml new file mode 100644 index 0000000..156f99e --- /dev/null +++ b/roles/oidc_user_backend/vars/main/provider-options-json.yml @@ -0,0 +1,43 @@ +--- +# JSON structure of the `occ` response with `--output=json` +oidc_user_backend_occ_user_oidc_config_provider_dict: + identifier: "{{ oidc_user_backend_config_provider_identifier }}" + clientId: "{{ oidc_user_backend_config_provider_client_id }}" + clientSecret: "{{ oidc_user_backend_config_provider_client_secret }}" + discoveryEndpoint: "{{ oidc_user_backend_config_provider_discovery_endpoint }}" + endSessionEndpoint: "{{ oidc_user_backend_config_provider_end_session_endpoint }}" + scope: "{{ oidc_user_backend_config_provider_scopes | default([], true) | join(' ') }}" + settings: + uniqueUid: >-2 + {{ oidc_user_backend_config_provider_settings_unique_id | bool }} + providerBasedId: >-2 + {{ oidc_user_backend_config_provider_settings_provider_based_id | bool }} + checkBearer: >-2 + {{ oidc_user_backend_config_provider_settings_check_bearer | bool }} + sendIdTokenHint: >-2 + {{ oidc_user_backend_config_provider_settings_send_id_token_hint | bool }} + groupProvisioning: >-2 + {{ oidc_user_backend_config_provider_settings_group_provisioning | bool }} + extraClaims: >-2 + {{ oidc_user_backend_config_provider_settings_extra_claims | default([]) | join(' ') }} + mappingDisplayName: "{{ oidc_user_backend_config_provider_settings_mapping_display_name }}" + mappingEmail: "{{ oidc_user_backend_config_provider_settings_mapping_email }}" + mappingQuota: "{{ oidc_user_backend_config_provider_settings_mapping_quota }}" + mappingUid: "{{ oidc_user_backend_config_provider_settings_mapping_uid }}" + mappingGroups: "{{ oidc_user_backend_config_provider_settings_mapping_groups }}" + mappingWebsite: "{{ oidc_user_backend_config_provider_settings_mapping_website }}" + mappingAvatar: "{{ oidc_user_backend_config_provider_settings_mapping_avatar }}" + mappingTwitter: "{{ oidc_user_backend_config_provider_settings_mapping_twitter }}" + mappingFediverse: "{{ oidc_user_backend_config_provider_settings_mapping_fediverse }}" + mappingOrganisation: "{{ oidc_user_backend_config_provider_settings_mapping_organisation }}" + mappingRole: "{{ oidc_user_backend_config_provider_settings_mapping_role }}" + mappingHeadline: "{{ oidc_user_backend_config_provider_settings_mapping_headline }}" + mappingBiography: "{{ oidc_user_backend_config_provider_settings_mapping_biography }}" + mappingPhonenumber: "{{ oidc_user_backend_config_provider_settings_mapping_phone }}" + mappingGender: "{{ oidc_user_backend_config_provider_settings_mapping_gender }}" + mappingAddress: "{{ oidc_user_backend_config_provider_settings_mapping_address }}" + mappingStreetaddress: "{{ oidc_user_backend_config_provider_settings_mapping_street_address }}" + mappingPostalcode: "{{ oidc_user_backend_config_provider_settings_mapping_postal_code }}" + mappingLocality: "{{ oidc_user_backend_config_provider_settings_mapping_locality }}" + mappingRegion: "{{ oidc_user_backend_config_provider_settings_mapping_region }}" + mappingCountry: "{{ oidc_user_backend_config_provider_settings_mapping_country }}" diff --git a/roles/oidc_user_backend/vars/main/provider-options.yml b/roles/oidc_user_backend/vars/main/provider-options.yml new file mode 100644 index 0000000..1a9e58b --- /dev/null +++ b/roles/oidc_user_backend/vars/main/provider-options.yml @@ -0,0 +1,54 @@ +--- +# Structure how `occ user_oidc:provider [options] []` expects them +oidc_user_backend_occ_user_oidc_provider_options_dict: + clientid: "{{ oidc_user_backend_config_provider_client_id }}" + clientsecret: "{{ oidc_user_backend_config_provider_client_secret }}" + discoveryuri: "{{ oidc_user_backend_config_provider_discovery_endpoint }}" + endsessionendpointuri: "{{ oidc_user_backend_config_provider_end_session_endpoint }}" + scope: "'{{ oidc_user_backend_config_provider_scopes | default([], true) | join(' ') }}'" + "unique-uid": >-2 + {{ oidc_user_backend_config_provider_settings_unique_id | bool | ternary(1, 0) }} + "check-bearer": >-2 + {{ oidc_user_backend_config_provider_settings_check_bearer | bool | ternary(1, 0) }} + "send-id-token-hint": >-2 + {{ oidc_user_backend_config_provider_settings_send_id_token_hint | bool | ternary(1, 0) }} + "group-provisioning": >-2 + {{ oidc_user_backend_config_provider_settings_group_provisioning | bool | ternary(1, 0) }} + "extra-claims": >-2 + {{ (oidc_user_backend_config_provider_settings_extra_claims | default([]) | length > 0) + | ternary(oidc_user_backend_config_provider_settings_extra_claims | join(' ') | quote, '') }} + +oidc_user_backend_occ_user_oidc_provider_mapping_options_dict: + "display-name": "{{ oidc_user_backend_config_provider_settings_mapping_display_name }}" + email: "{{ oidc_user_backend_config_provider_settings_mapping_email }}" + quota: "{{ oidc_user_backend_config_provider_settings_mapping_quota }}" + uid: "{{ oidc_user_backend_config_provider_settings_mapping_uid }}" + groups: "{{ oidc_user_backend_config_provider_settings_mapping_groups }}" + website: "{{ oidc_user_backend_config_provider_settings_mapping_website }}" + avatar: "{{ oidc_user_backend_config_provider_settings_mapping_avatar }}" + twitter: "{{ oidc_user_backend_config_provider_settings_mapping_twitter }}" + fediverse: "{{ oidc_user_backend_config_provider_settings_mapping_fediverse }}" + organisation: "{{ oidc_user_backend_config_provider_settings_mapping_organisation }}" + role: "{{ oidc_user_backend_config_provider_settings_mapping_role }}" + headline: "{{ oidc_user_backend_config_provider_settings_mapping_headline }}" + biography: "{{ oidc_user_backend_config_provider_settings_mapping_biography }}" + phone: "{{ oidc_user_backend_config_provider_settings_mapping_phone }}" + gender: "{{ oidc_user_backend_config_provider_settings_mapping_gender }}" + address: "{{ oidc_user_backend_config_provider_settings_mapping_address }}" + street_address: "{{ oidc_user_backend_config_provider_settings_mapping_street_address }}" + postal_code: "{{ oidc_user_backend_config_provider_settings_mapping_postal_code }}" + locality: "{{ oidc_user_backend_config_provider_settings_mapping_locality }}" + region: "{{ oidc_user_backend_config_provider_settings_mapping_region }}" + country: "{{ oidc_user_backend_config_provider_settings_mapping_country }}" + +oidc_user_backend_occ_user_oidc_provider_options: >-2 + {% for tuple in oidc_user_backend_occ_user_oidc_provider_options_dict | dict2items %} + {% if tuple.value | default(false, true) %} + --{{ tuple.key }}={{ tuple.value }} + {% endif %} + {% endfor %} + {% for tuple in oidc_user_backend_occ_user_oidc_provider_mapping_options_dict | dict2items %} + {% if tuple.value | default(false, true) %} + --mapping-{{ tuple.key }}={{ tuple.value }} + {% endif %} + {% endfor %}