diff --git a/.github/workflows/molecule-loadbalancer.yml b/.github/workflows/molecule-loadbalancer.yml index ca03e28f3..fa1afc9a3 100644 --- a/.github/workflows/molecule-loadbalancer.yml +++ b/.github/workflows/molecule-loadbalancer.yml @@ -24,7 +24,7 @@ jobs: build: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python 3.8 uses: actions/setup-python@v6 diff --git a/.github/workflows/molecule-mongo.yml b/.github/workflows/molecule-mongo.yml index f0b52d67d..f944dab16 100644 --- a/.github/workflows/molecule-mongo.yml +++ b/.github/workflows/molecule-mongo.yml @@ -18,7 +18,7 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python 3.8 uses: actions/setup-python@v6 with: diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index 8295b151f..521fb2c73 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -19,7 +19,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python 3.8 uses: actions/setup-python@v6 diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 000000000..8824391dc --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,27 @@ +--- +extends: "default" + +rules: + # 80 chars should be enough, but don't fail if a line is longer + line-length: + max: 160 + level: "warning" + + quoted-strings: + quote-type: "any" + required: true + allow-quoted-quotes: false + check-keys: false + +# ansible-lint compatibility: + comments: + min-spaces-from-content: 1 + + comments-indentation: false + + braces: + max-spaces-inside: 1 + + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true diff --git a/README.md b/README.md index a6623af49..88a3628c1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ Every application has a seperate role to install it. The following roles can be | myconext | eduID | | profile | Profile page | | manage | Entity registration | -| teams | Group membership app | | mujina | Mujina IdP | | voot | Voot membership API | | pdp | Policy Decicions API | diff --git a/environments/template/group_vars/all.yml b/environments/template/group_vars/all.yml index 1d8bd6f84..ded950834 100644 --- a/environments/template/group_vars/all.yml +++ b/environments/template/group_vars/all.yml @@ -30,6 +30,8 @@ admin_email: "openconext-admin@example.edu" environment_shortname: "" environment_ribbon_colour: "" +current_release_appdir: /opt/openconext + httpd_csp: lenient: "default-src 'self'; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" lenient_with_static_img: "default-src 'self'; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https://{{ static_vhost }} http://localhost:* data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" @@ -52,7 +54,6 @@ engine_attribute_aggregation_password: "{{ aa.eb_password }}" # Some deprovision variables are shared between applications authz_server_api_lifecycle_username: authz_server_api_lifecycle_user -teams_api_lifecycle_username: teams_api_lifecycle_user attribute_aggregator_api_lifecycle_username: attribute_aggregator_api_lifecycle_user engine_api_deprovision_user: lifecycle lifecycle_api_username: lifecycle diff --git a/environments/template/group_vars/mongo_servers.yml b/environments/template/group_vars/mongo_servers.yml index 70bb40871..d9f3e10cc 100644 --- a/environments/template/group_vars/mongo_servers.yml +++ b/environments/template/group_vars/mongo_servers.yml @@ -1,5 +1,5 @@ --- -replica_set_name: my_mongo_cluster +mongo_replica_set_name: my_mongo_cluster mongo_cluster_members: - host: "mongo3.example.com:{{ mongo_port }}" # arbiter first or change mongo_arbiter_index diff --git a/environments/template/group_vars/template.yml b/environments/template/group_vars/template.yml index dc2642d3b..a1538a319 100644 --- a/environments/template/group_vars/template.yml +++ b/environments/template/group_vars/template.yml @@ -38,8 +38,6 @@ mujina_version: "8.0.2" oidcng_version: "6.1.6" pdp_version: "7.3.0" profile_version: "3.1.4" -teams_gui_version: "9.1.3" -teams_server_version: "9.1.3" voot_version: "6.2.0" myconext_version: "8.1.12-1" dashboard_version: "13.0.11" @@ -53,14 +51,12 @@ statistics_version: "1.1.7" databases: names: - - teams - "{{ engine_database_name }}" - pdp-server - aaserver - shibboleth - eb_logins users: - - { name: teamsrw, db_name: teams, password: "{{ mysql_passwords.teams }}" } - { name: "{{ engine_database_user }}", db_name: "{{ engine_database_name }}", password: "{{ mysql_passwords.eb }}" } - { name: pdp-serverrw, db_name: pdp-server, password: "{{ mysql_passwords.pdp_server }}" } - { name: aa-serverrw, db_name: aaserver, password: "{{ mysql_passwords.aa_server }}" } @@ -100,32 +96,10 @@ engine_trusted_proxy_ips: - 192.168.1.1 - 10.0.0.1 # -engine_keys: - default: - privateFile: /etc/openconext/engineblock.pem - publicKey: engineblock.crt - publicFile: /etc/openconext/engineblock.crt - profile_apache_symfony_environment: prod # Engine's assertion signing certificate: engine_profile_idp_certificate: /etc/openconext/engineblock.crt -teams: - db_name: "teams" - db_user: "teamsrw" - db_password: "{{ mysql_passwords.teams }}" - db_host: "{{ mariadb_host }}" - group_name_context: "urn:collab:group:{{ base_domain }}:" - voot_api_user: "voot" - spdashboard_api_user: "spdashboard" - spdashboard_person_urn: "urn:collab:person:surfnet.nl:sp-dashboard-C133A36F-CFCA-4F3D-87CE-7ECE29773FE0" - product_name: "OpenConext Teams" - default_stem_name: "demo:openconext:org" - feature_invite_migration_on: False - super_admins_team_urns: - - "nl:surfnet:diensten:teams_super_users" - - "nl:surfnet:diensten:teams_super_admin_users" - engineblock: idp_url: https://engine.{{ base_domain }}/authentication/idp/single-sign-on idp_entity_id: https://engine.{{ base_domain }}/authentication/idp/metadata @@ -402,9 +376,6 @@ loadbalancing: metadata: port: 409 - teams: - port: 601 - oidc_playground: port: 619 @@ -483,13 +454,6 @@ haproxy_applications: servers: "{{docker_servers}}" restricted: yes - - name: teams - vhost_name: teams.{{ base_domain }} - ha_method: "GET" - ha_url: "/api/teams/health" - port: "{{ loadbalancing.teams.port }}" - servers: "{{docker_servers}}" - - name: oidc_playground vhost_name: "oidc-playground.{{ base_domain }}" ha_method: "GET" diff --git a/environments/template/inventory b/environments/template/inventory index f1b3dabed..b6e736941 100644 --- a/environments/template/inventory +++ b/environments/template/inventory @@ -84,9 +84,6 @@ docker2.example.com [docker_invite:children] docker_apps1 -[docker_teams:children] -docker_apps1 - [docker_pdp:children] docker_apps1 diff --git a/environments/template/secrets/secret_example.yml b/environments/template/secrets/secret_example.yml index 6dc112821..a6c052658 100644 --- a/environments/template/secrets/secret_example.yml +++ b/environments/template/secrets/secret_example.yml @@ -1,7 +1,6 @@ mysql_root_password: secret mysql_passwords: - teams: secret eb: secret pdp_server: secret aa_server: secret @@ -13,7 +12,7 @@ mongo_passwords: oidcng: secret myconext: secret -mongo_admin_password: secret +mongo_admin_password: secret # this works for first time install, if you change it later you will have to do it manually mongo_ca_passphrase: secret engine_api_metadata_push_password: secret @@ -36,7 +35,6 @@ engine_parameters_secret: secretsecretsecretsecretsecretsecret # need 32 chars profile_secret: secret -teams_authz_client_secret: secret teams_migration_secret_key: secret voot_resource_checking_secret: secret @@ -45,7 +43,6 @@ voot_oidcng_checkToken_secret: secret external_group_provider_secrets: teams: secret -teams_api_lifecycle_password: secret teams_api_spdashboard_password: secret attribute_aggregator_api_lifecycle_password: secret @@ -144,7 +141,7 @@ invite_lifecycle_secret: "secret" invite_internal_secret: "secret" invite_profile_secret: "secret" invite_sp_dashboard_secret: "secret" -invite_access_secret: "secret" +invite_access_dashboard_secret: "secret" invite_private_key_pkcs8: | -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCfpYYMgKYDICkp diff --git a/provision.yml b/provision.yml index 3b26963a5..815d62ee2 100644 --- a/provision.yml +++ b/provision.yml @@ -283,13 +283,6 @@ - role: stepupwebauthn tags: ['stepupwebauthn', 'stepup'] -- name: Deploy teams app - hosts: docker_teams - become: true - roles: - - teams - tags: ['teams'] - - name: Deploy voot app hosts: docker_voot become: true diff --git a/roles/dashboard/templates/serverapplication.yml.j2 b/roles/dashboard/templates/serverapplication.yml.j2 index 45109b554..88a96ff8d 100644 --- a/roles/dashboard/templates/serverapplication.yml.j2 +++ b/roles/dashboard/templates/serverapplication.yml.j2 @@ -27,8 +27,8 @@ spDashboard.password={{ dashboard_sp_dashboard_password }} # SAB connection details sab-rest.endpoint={{ dashboard.sab_rest_endpoint }} -sab-rest.username=cdk -sab-rest.password={{ dashboard_sab_rest_password }} +sab-rest.username={{ dashboard.sab_rest_username }} +sab-rest.password={{ dashboard.sab_rest_password }} # SAB roles admin.surfconext.idp.sabRole=SURFconextverantwoordelijke diff --git a/roles/engine/defaults/main.yml b/roles/engine/defaults/main.yml index cdc888cd5..af113fd22 100644 --- a/roles/engine/defaults/main.yml +++ b/roles/engine/defaults/main.yml @@ -2,21 +2,22 @@ engine_version: "" # Be aware that if you enable this option that NPM, Node.js and Composer are installed # Feature toggles -engine_feature_encrypted_assertions: 1 -engine_feature_encrypted_assertions_require_outer_signature: 1 -engine_feature_run_all_manipulations_prior_to_consent: 0 -engine_feature_block_user_on_violation: 0 -engine_feature_enable_sso_notification: 0 -engine_feature_enable_sso_session_cookie: 0 -engine_feature_enable_consent: 1 -engine_feature_stepup_override_entityid: 0 -engine_feature_idp_initiated_flow: 1 -engine_api_feature_metadata_push: 1 -engine_api_feature_consent_listing: 1 -engine_api_feature_consent_remove: 0 -engine_api_feature_metadata_api: 1 -engine_api_feature_deprovision: 1 -engine_feature_send_user_attributes: 0 +engine_feature_encrypted_assertions: true +engine_feature_encrypted_assertions_require_outer_signature: true +engine_feature_run_all_manipulations_prior_to_consent: false +engine_feature_block_user_on_violation: false +engine_feature_enable_sso_notification: false +engine_feature_enable_sso_session_cookie: false +engine_feature_enable_consent: true +engine_feature_stepup_override_entityid: false +engine_feature_idp_initiated_flow: true +engine_feature_send_user_attributes: false +engine_feature_enable_sbs_interrupt: false +engine_api_feature_metadata_push: true +engine_api_feature_consent_listing: true +engine_api_feature_consent_remove: false +engine_api_feature_metadata_api: true +engine_api_feature_deprovision: true # Cutoff point for showing unfiltered IdPs on the WAYF engine_wayf_cutoff_point_for_showing_unfiltered_idps: 50 @@ -76,6 +77,14 @@ engine_stepup_gateway_sfo_entity_id: "https://{{ engine_stepup_gateway_domain }} # The single sign-on endpoint used for Stepup Gateway SFO callouts engine_stepup_gateway_sfo_sso_location: "https://{{ engine_stepup_gateway_domain }}/second-factor-only/single-sign-on" +# SBS interrupt settings +engine_sbs_base_url: "sbs.{{ base_domain }}" +engine_sbs_attributes_allowed: + - 'urn:mace:dir:attribute-def:eduPersonEntitlement' + - 'urn:mace:dir:attribute-def:uid' + - 'urn:mace:dir:attribute-def:eduPersonPrincipalName' + - 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13' + ## The minimum priority of messages that will be logged engine_logging_passthru_level: NOTICE diff --git a/roles/engine/tasks/main.yml b/roles/engine/tasks/main.yml index c75ece132..f14b05dc1 100644 --- a/roles/engine/tasks/main.yml +++ b/roles/engine/tasks/main.yml @@ -1,24 +1,24 @@ --- -- name: Add group engine +- name: "Add group engine" ansible.builtin.group: name: "engine" - state: present - register: engine_guid + state: "present" + register: "engine_guid" -- name: Add user engine +- name: "Add user engine" ansible.builtin.user: name: "engine" group: "engine" - createhome: false - state: present - register: engine_uid + create_home: false + state: "present" + register: "engine_uid" -- name: Create some dirs +- name: "Create some dirs" ansible.builtin.file: - state: directory - dest: "{{ item }}" - owner: root - group: root + path: "{{ item }}" + state: "directory" + owner: "root" + group: "root" mode: "0755" with_items: - "{{ _engine_config_dir }}" @@ -27,7 +27,7 @@ - "{{ _engine_config_dir }}/images" - "{{ _engine_config_dir }}/languages" -- name: Place parameters.yml +- name: "Place parameters.yml" ansible.builtin.template: src: "{{ item }}.j2" dest: "{{ _engine_config_dir }}/configs/{{ item }}" @@ -39,157 +39,157 @@ - "monolog.yml" notify: "Restart engine" -- name: Check presence of environment specific attributes.json +- name: "Check presence of environment specific attributes.json" ansible.builtin.stat: path: "{{ inventory_dir }}/files/eb/attributes.json" - register: engine_attributes_json_present + register: "engine_attributes_json_present" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Copy environment specific attributes.json +- name: "Copy environment specific attributes.json" ansible.builtin.copy: src: "{{ inventory_dir }}/files/eb/attributes.json" dest: "{{ _engine_config_dir }}/configs/" mode: "0644" - owner: root - group: engine - when: engine_attributes_json_present.stat.exists + owner: "root" + group: "engine" + when: "engine_attributes_json_present.stat.exists" -- name: Check presence of language specific overrides +- name: "Check presence of language specific overrides" ansible.builtin.stat: path: "{{ inventory_dir }}/files/eb/languages/" - register: engine_overrides_present + register: "engine_overrides_present" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Copy language specific overrides +- name: "Copy language specific overrides" ansible.builtin.template: src: "{{ item }}" dest: "{{ _engine_config_dir }}/languages/" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0644" - when: engine_overrides_present.stat.exists + when: "engine_overrides_present.stat.exists" with_fileglob: - "{{ inventory_dir }}/files/eb/languages/*" notify: "Restart engine" -- name: Check if we have a custom logo +- name: "Check if we have a custom logo" ansible.builtin.stat: path: "{{ inventory_dir }}/files/logo.png" - register: engine_customlogo + register: "engine_customlogo" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Install environment specific logo +- name: "Install environment specific logo" ansible.builtin.copy: src: "{{ inventory_dir }}/files/logo.png" dest: "{{ _engine_config_dir }}/images/" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0644" - when: engine_customlogo.stat.exists + when: "engine_customlogo.stat.exists" -- name: Check if we have a custom favicon +- name: "Check if we have a custom favicon" ansible.builtin.stat: path: "{{ inventory_dir }}/files/favicon.ico" - register: engine_customfavicon + register: "engine_customfavicon" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Install environment specific favicon +- name: "Install environment specific favicon" ansible.builtin.copy: src: "{{ inventory_dir }}/files/favicon.ico" dest: "/opt/openconext/common/" - owner: root - group: root + owner: "root" + group: "root" mode: "0644" - when: engine_customfavicon.stat.exists + when: "engine_customfavicon.stat.exists" -- name: Check if we have a custom background back image for the feedback page +- name: "Check if we have a custom background back image for the feedback page" ansible.builtin.stat: path: "{{ inventory_dir }}/files/eb/background-back.svg" - register: engine_customfeedbackbackground + register: "engine_customfeedbackbackground" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Install environment specific background back image +- name: "Install environment specific background back image" ansible.builtin.copy: src: "{{ inventory_dir }}/files/eb/background-back.svg" dest: "{{ _engine_config_dir }}/images/" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0644" - when: engine_customfeedbackbackground.stat.exists + when: "engine_customfeedbackbackground.stat.exists" -- name: Check if we have a custom background front image for the feedback page +- name: "Check if we have a custom background front image for the feedback page" ansible.builtin.stat: path: "{{ inventory_dir }}/files/eb/background-front.svg" - register: engine_customfeedbackforeground + register: "engine_customfeedbackforeground" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Install environment specific background front image +- name: "Install environment specific background front image" ansible.builtin.copy: src: "{{ inventory_dir }}/files/eb/background-front.svg" dest: "{{ _engine_config_dir }}/images/" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0644" - when: engine_customfeedbackforeground.stat.exists + when: "engine_customfeedbackforeground.stat.exists" -- name: Check if we have a Stepup GW certificate +- name: "Check if we have a Stepup GW certificate" ansible.builtin.stat: path: "{{ inventory_dir }}/files/certs/stepup_gateway.pem" - register: engine_stepupgwcert + register: "engine_stepupgwcert" become: false - delegate_to: localhost + delegate_to: "localhost" -- name: Install Stepup GW certificate +- name: "Install Stepup GW certificate" ansible.builtin.copy: src: "{{ inventory_dir }}/files/certs/stepup_gateway.pem" dest: "{{ _engine_config_dir }}/certs/" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0644" - when: engine_stepupgwcert.stat.exists + when: "engine_stepupgwcert.stat.exists" -- name: Copy over the engineblock keys +- name: "Copy over the engineblock keys" ansible.builtin.copy: content: "{{ item.private_key }}" dest: "{{ _engine_config_dir }}/certs/{{ item.name }}.key" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0440" no_log: true loop: "{{ engine_key_and_certs }}" -- name: Copy engineblock certificates to correct location +- name: "Copy engineblock certificates to correct location" ansible.builtin.copy: src: "{{ inventory_dir }}/files/certs/{{ item.crt_name }}" dest: "{{ _engine_config_dir }}/certs/{{ item.name }}.crt" - owner: root - group: engine + owner: "root" + group: "engine" mode: "0644" no_log: true loop: "{{ engine_key_and_certs }}" -- name: Create Docker volume to contain the sessions +- name: "Create Docker volume to contain the sessions" community.docker.docker_volume: - name: engineblock_sessions - state: present + volume_name: "engineblock_sessions" + state: "present" -- name: Add the MariaDB docker network to the list of networks when MariaDB runs in Docker +- name: "Add the MariaDB docker network to the list of networks when MariaDB runs in Docker" ansible.builtin.set_fact: engine_docker_networks: - - name: loadbalancer - - name: openconext_mariadb - when: mariadb_in_docker | default(false) | bool + - name: "loadbalancer" + - name: "openconext_mariadb" + when: "mariadb_in_docker | default(false) | bool" -- name: Create the container +- name: "Create the container" community.docker.docker_container: name: "engineblock" - image: ghcr.io/openconext/openconext-engineblock/openconext-engineblock:{{ engine_version }} + image: "ghcr.io/openconext/openconext-engineblock/openconext-engineblock:{{ engine_version }}" pull: true restart_policy: "always" networks: "{{ engine_docker_networks }}" @@ -208,53 +208,53 @@ PHP_MEMORY_LIMIT: "{{ engine_php_memory }}" APP_ENV: "prod" APP_SECRET: "{{ engine_parameters_secret }}" - APP_DEBUG: "{{ engine_debug | bool | int }}" + APP_DEBUG: "{{ engine_debug | bool | int | string }}" etc_hosts: - host.docker.internal: host-gateway + host.docker.internal: "host-gateway" mounts: - source: "{{ _engine_config_dir }}/configs/" target: "{{ _engine_container_config_dir }}" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/languages/overrides.en.php" target: "/var/www/html/languages/overrides.en.php" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/languages/overrides.nl.php" target: "/var/www/html/languages/overrides.nl.php" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/configs/attributes.json" target: "{{ _engine_container_config_dir }}/attributes.json" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/images/background-back.svg" target: "/var/www/html/public/images/background-back.svg" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/images/background-front.svg" target: "/var/www/html/public/images/background-front.svg" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/images/logo.png" target: "/var/www/html/public/images/logo.png" - type: bind + type: "bind" read_only: true - source: "{{ _engine_config_dir }}/certs/" target: "/var/www/html/certs/" - type: bind + type: "bind" read_only: true - source: "/opt/openconext/common/favicon.ico" target: "/var/www/html/public/favicon.ico" - type: bind + type: "bind" read_only: true - source: "engineblock_sessions" target: "/tmp/" - type: volume + type: "volume" healthcheck: test: ["CMD-SHELL", "curl --fail -s http://localhost/internal/health | grep -q '\"status\":\"UP\"'"] - start_period: 60s - interval: 10s - timeout: 1s - retries: 20 + start_period: "60s" + interval: "10s" + timeout: "1s" + retries: "20" register: "engine_container" diff --git a/roles/engine/templates/parameters.yml.j2 b/roles/engine/templates/parameters.yml.j2 index 77903de1e..526104405 100644 --- a/roles/engine/templates/parameters.yml.j2 +++ b/roles/engine/templates/parameters.yml.j2 @@ -228,6 +228,7 @@ parameters: feature_stepup_sfo_override_engine_entityid: {{ engine_feature_stepup_override_entityid | bool | to_json }} feature_enable_idp_initiated_flow: {{ engine_feature_idp_initiated_flow | bool | to_json }} feature_stepup_send_user_attributes: {{ engine_feature_send_user_attributes | bool | to_json }} + feature_enable_sram_interrupt: {{ engine_feature_enable_sbs_interrupt | bool | to_json }} ########################################################################################## ## PROFILE SETTINGS ########################################################################################## @@ -310,3 +311,15 @@ parameters: # used in the authentication log record. The attributeName will be searched in the response attributes and if present # the log data will be enriched. The values of the response attributes are the final values after ARP and Attribute Manipulation. auth.log.attributes: {{ engine_log_attributes }} + + + ########################################################################################## + ## SBS external authorization/attribute enrichtment + ########################################################################################## + sram.api_token: "{{ engine_sbs_api_token | default('') }}" + sram.base_url: "https://{{ engine_sbs_base_url }}/api/users/" + sram.authz_location: "authz_eb" + sram.attributes_location: "attributes_eb" + sram.interrupt_location: "interrupt" + sram.verify_peer: true + sram.allowed_attributes: {{ engine_sbs_attributes_allowed }} diff --git a/roles/haproxy/defaults/main.yml b/roles/haproxy/defaults/main.yml index 9833699c4..90c1ea74d 100644 --- a/roles/haproxy/defaults/main.yml +++ b/roles/haproxy/defaults/main.yml @@ -45,3 +45,9 @@ haproxy_acmedns: password: "password" subdomain: "a_subdomain" fulldomain: "a_subdomain.acme-dns.example.org" + +# on which weekday (cron, 0==sunday) to run the renewal script +haproxy_acme_cronjob_dow: 1 + +# optional monitoring url for acme cron +haproxy_acme_cronjob_monitor_url: diff --git a/roles/haproxy/tasks/acme.yml b/roles/haproxy/tasks/acme.yml index 1ee9ab315..ed6ffed89 100644 --- a/roles/haproxy/tasks/acme.yml +++ b/roles/haproxy/tasks/acme.yml @@ -54,3 +54,21 @@ register: "acme_account" become: true become_user: "acme" + +- name: Remove default cronjob for renewal + ansible.builtin.file: + path: "/var/spool/cron/crontabs/acme" + state: "absent" + +- name: Install cronjob for renewal + ansible.builtin.copy: + dest: "/etc/cron.d/acme-renew" + owner: "root" + group: "root" + mode: "0644" + content: | + MAILTO=surfconext-beheer@surf.nl + 30 07 * * {{ haproxy_acme_cronjob_dow }} acme /home/acme/.acme.sh/acme.sh --cron --home /home/acme/.acme.sh > /dev/null + {%- if haproxy_acme_cronjob_monitor_url | default('') -%} + {# #} && curl -fsS --retry 3 -o /dev/null {{ haproxy_acme_cronjob_monitor_url }} + {%- endif %} diff --git a/roles/haproxy/tasks/get_acme_certs.yml b/roles/haproxy/tasks/get_acme_certs.yml index 7ef17361c..18e87bc49 100644 --- a/roles/haproxy/tasks/get_acme_certs.yml +++ b/roles/haproxy/tasks/get_acme_certs.yml @@ -6,7 +6,8 @@ ( [base_domain] + (haproxy_applications | map(attribute='vhost_name') | list) - + (haproxy_redirects | map(attribute='hostname') | list) + + (haproxy_ldap_servers | map(attribute='vhost_name') | list) + + (haproxy_redirects | map(attribute='hostname') | list) ) | unique | sort }} diff --git a/roles/haproxy/templates/haproxy_backend.cfg.j2 b/roles/haproxy/templates/haproxy_backend.cfg.j2 index d2387c033..8ef005da4 100644 --- a/roles/haproxy/templates/haproxy_backend.cfg.j2 +++ b/roles/haproxy/templates/haproxy_backend.cfg.j2 @@ -67,3 +67,18 @@ {% endfor %} {% endif %} {% endfor %} + +{% if haproxy_ldap_servers is defined %} +#--------------------------------------------------------------------- +# ldap backend +#--------------------------------------------------------------------- +backend ldap_servers + mode tcp + option tcpka + + option ldap-check + + {% for server in haproxy_ldap_servers -%} + server {{server.label}} {{server.ip}}:{{server.port}} ssl verify none check weight 10 {% if loop.index==1 %}on-marked-up shutdown-backup-sessions{% else %}backup{% endif %} + {% endfor %} +{% endif %} diff --git a/roles/haproxy/templates/haproxy_frontend.cfg.j2 b/roles/haproxy/templates/haproxy_frontend.cfg.j2 index 6082e9c03..4909a0074 100644 --- a/roles/haproxy/templates/haproxy_frontend.cfg.j2 +++ b/roles/haproxy/templates/haproxy_frontend.cfg.j2 @@ -12,8 +12,8 @@ frontend stats # ------------------------------------------------------------------- frontend internet_ip - bind {{ haproxy_sni_ip.ipv4 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent - bind {{ haproxy_sni_ip.ipv6 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent + bind {{ haproxy_sni_ip.ipv4 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent + bind {{ haproxy_sni_ip.ipv6 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent bind {{ haproxy_sni_ip.ipv4 }}:80 transparent bind {{ haproxy_sni_ip.ipv6 }}:80 transparent # Logging is done in the local_ip backend, otherwise all requests are logged twice @@ -30,7 +30,7 @@ frontend internet_ip http-request redirect scheme https code 301 if !{ ssl_fc } # Log the user agent in the httplogs capture request header User-agent len 256 - # Put the useragent header in a variable, shared between request and response. + # Put the useragent header in a variable, shared between request and response. http-request set-var(txn.useragent) req.fhdr(User-Agent) # The ACL below makes sure only supported http methods are allowed acl valid_method method {{ haproxy_supported_http_methods }} @@ -51,7 +51,7 @@ frontend internet_ip http-response replace-header Set-Cookie (?i)(^(?!.*samesite).*$) \1;\ SameSite=None if !no_same_site_uas # Remove an already present SameSite cookie attribute for unsupported browsers http-response replace-value Set-Cookie (^.*)(?i);\ *SameSite=(Lax|Strict|None)(.*$) \1\3 if no_same_site_uas - # Log whether the no_same_site_uas ACL has been hit + # Log whether the no_same_site_uas ACL has been hit http-request set-header samesitesupport samesite_notsupported if no_same_site_uas http-request set-header samesitesupport samesite_supported if !no_same_site_uas # We need a dummy backend in order to be able to rewrite the loadbalancer cookies @@ -66,7 +66,7 @@ frontend local_ip acl valid_vhost hdr(host) -f /etc/haproxy/acls/validvhostsunrestricted.acl acl staging req.cook(staging) -m str true acl staging src -f /etc/haproxy/acls/stagingips.acl - acl stagingvhost hdr(host) -i -M -f /etc/haproxy/maps/backendsstaging.map + acl stagingvhost hdr(host) -i -M -f /etc/haproxy/maps/backendsstaging.map use_backend %[req.hdr(host),lower,map(/etc/haproxy/maps/backendsstaging.map)] if stagingvhost staging use_backend %[req.hdr(host),lower,map(/etc/haproxy/maps/backends.map)] option httplog @@ -82,7 +82,7 @@ frontend local_ip http-request capture sc_http_req_rate(0) len 4 # Create an ACL when the request rate exceeds {{ haproxy_max_request_rate }} per 10s acl exceeds_max_request_rate_per_ip sc_http_req_rate(0) gt {{ haproxy_max_request_rate }} - # Measure and log the request rate per path and ip + # Measure and log the request rate per path and ip http-request track-sc1 base32+src table st_httpreqs_per_ip_and_path http-request capture sc_http_req_rate(1) len 4 # Some paths allow for a higher ratelimit. These are in a seperate mapfile @@ -96,7 +96,7 @@ frontend local_ip http-request deny if ! valid_vhost # Deny the request when the request rate exceeds {{ haproxy_max_request_rate }} per 10s http-request deny deny_status 429 if exceeds_max_request_rate_per_ip !allowlist - # Deny the request when the request rate per host header url path and src ip exceeds {{ haproxy_max_request_rate_ip_path }} per 1 m + # Deny the request when the request rate per host header url path and src ip exceeds {{ haproxy_max_request_rate_ip_path }} per 1 m http-request deny deny_status 429 if exceeds_max_request_rate_per_ip_and_path !allowlist # Create some http redirects {% if haproxy_securitytxt_target_url is defined %} @@ -111,8 +111,8 @@ frontend local_ip ## ------------------------------------------------------------------- frontend internet_restricted_ip - bind {{ haproxy_sni_ip_restricted.ipv4 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent - bind {{ haproxy_sni_ip_restricted.ipv6 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent + bind {{ haproxy_sni_ip_restricted.ipv4 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent + bind {{ haproxy_sni_ip_restricted.ipv6 }}:443 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 alpn h2,http/1.1 transparent bind {{ haproxy_sni_ip_restricted.ipv4 }}:80 transparent bind {{ haproxy_sni_ip_restricted.ipv6 }}:80 transparent # Logging is done in the local_ip_restriced backend, otherwise all requests are logged twice @@ -128,8 +128,8 @@ frontend internet_restricted_ip # We redirect all port 80 to port 443 http-request redirect scheme https code 301 if !{ ssl_fc } # Log the user agent in the httplogs - capture request header User-agent len 256 - # Put the useragent header in a variable, shared between request and response. + capture request header User-agent len 256 + # Put the useragent header in a variable, shared between request and response. http-request set-var(txn.useragent) req.fhdr(User-Agent) # The ACL below makes sure only supported http methods are allowed acl valid_method method {{ haproxy_supported_http_methods }} @@ -155,12 +155,12 @@ frontend internet_restricted_ip # frontend restricted ip addresses localhost # traffic coming back from the dummy backend ends up here # ------------------------------------------------------------------- -frontend localhost_restricted +frontend localhost_restricted bind 127.0.0.1:82 accept-proxy acl valid_vhost hdr(host) -f /etc/haproxy/acls/validvhostsrestricted.acl acl staging req.cook(staging) -m str true acl staging src -f /etc/haproxy/acls/stagingips.acl - acl stagingvhost hdr(host) -i -M -f /etc/haproxy/maps/backendsstaging.map + acl stagingvhost hdr(host) -i -M -f /etc/haproxy/maps/backendsstaging.map use_backend %[req.hdr(host),lower,map(/etc/haproxy/maps/backendsstaging.map)] if stagingvhost staging use_backend %[req.hdr(host),lower,map(/etc/haproxy/maps/backends.map)] option httplog @@ -177,7 +177,7 @@ frontend localhost_restricted # Create an ACL when the request rate exceeds {{ haproxy_max_request_rate }} per 10s acl exceeds_max_request_rate_per_ip sc_http_req_rate(0) gt {{ haproxy_max_request_rate }} http-request deny deny_status 429 if exceeds_max_request_rate_per_ip !allowlist - # Measure and log the request rate per path and ip + # Measure and log the request rate per path and ip http-request track-sc1 base32+src table st_httpreqs_per_ip_and_path http-request capture sc_http_req_rate(1) len 4 # Some paths allow for a higher ratelimit. These are in a seperate mapfile @@ -191,7 +191,7 @@ frontend localhost_restricted http-request deny if ! valid_vhost # Deny the request when the request rate exceeds {{ haproxy_max_request_rate }} per 10s http-request deny deny_status 429 if exceeds_max_request_rate_per_ip !allowlist - # Deny the request when the request rate per host header url path and src ip exceeds {{ haproxy_max_request_rate_ip_path }} per 1 m + # Deny the request when the request rate per host header url path and src ip exceeds {{ haproxy_max_request_rate_ip_path }} per 1 m http-request deny deny_status 429 if exceeds_max_request_rate_per_ip_and_path !allowlist # Create some http redirects {% if haproxy_securitytxt_target_url is defined %} @@ -201,3 +201,19 @@ frontend localhost_restricted http-request redirect location %[base,map_reg(/etc/haproxy/maps/redirects.map)] if { base,map_reg(/etc/haproxy/maps/redirects.map) -m found } {% endif %} + +{% if haproxy_ldap_servers is defined %} +#-------------------------------------------------------------------- +# frontend public ips ldap +# ------------------------------------------------------------------- +listen ldap + mode tcp + no option dontlognull + option tcplog + option logasap + timeout client 900s + timeout server 901s + bind {{ haproxy_sni_ip.ipv4 }}:636 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 transparent + bind {{ haproxy_sni_ip.ipv6 }}:636 ssl crt-list /etc/haproxy/maps/certlist.lst ssl crt /etc/haproxy/certs/ no-sslv3 no-tlsv10 no-tlsv11 transparent + use_backend ldap_servers +{% endif %} diff --git a/roles/hosts/tasks/main.yml b/roles/hosts/tasks/main.yml index 14e36b308..3c8ce29c7 100644 --- a/roles/hosts/tasks/main.yml +++ b/roles/hosts/tasks/main.yml @@ -20,7 +20,6 @@ - "aa.vm.openconext.org" - "link.vm.openconext.org" - "connect.vm.openconext.org" - - "teams.vm.openconext.org" - "manage.vm.openconext.org" - name: Set logstash in hostsfile diff --git a/roles/invite/templates/logback.xml.j2 b/roles/invite/templates/logback.xml.j2 index 95ef4fe23..f8ccf8009 100644 --- a/roles/invite/templates/logback.xml.j2 +++ b/roles/invite/templates/logback.xml.j2 @@ -2,41 +2,41 @@ - - - %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n - - + + + %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n + + - - host.docker.internal:514 + + host.docker.internal:514 - {"app":"invite"} - true - - [ignore] - [ignore] - [ignore] - - - - invitejson: - - - + {"app":"invite"} + true + + [ignore] + [ignore] + [ignore] + + + + invitejson: + + + - - - - - + + + + + - - - {% if invite_logback_json | bool %} - - {%endif%} - + + + {% if invite_logback_json | bool %} + + {% endif %} + diff --git a/roles/invite/templates/serverapplication.yml.j2 b/roles/invite/templates/serverapplication.yml.j2 index 81e7dae5d..6c943cf5c 100644 --- a/roles/invite/templates/serverapplication.yml.j2 +++ b/roles/invite/templates/serverapplication.yml.j2 @@ -75,13 +75,13 @@ crypto: private-key-location: file:///private_key_pkcs8.pem cron: + delay_min: 200000 + delay_max: 1200001 user-cleaner-cron: "PT30M" - user-cleaner-cron-initial-delay: "PT10M" user-cleaner-lock-at-least-for: "PT5M" user-cleaner-lock-at-most-for: "PT28M" last-activity-duration-days: 1000 role-expiration-notifier-cron: "PT30M" - role-expiration-notifier-cron-initial-delay: "PT15M" # Set to -1 to suppress role expiry notifications role-expiration-notifier-duration-days: 5 role-expiration-notifier-lock-at-least-for: "PT5M" @@ -149,46 +149,7 @@ feature: # We don't encode in-memory passwords, but they are reused so do NOT prefix them with {noop} external-api-configuration: remote-users: - - username: {{ invite.vootuser }} - password: "{{ invite.vootsecret }}" - scopes: - - voot - - username: {{ invite.teamsuser}} - password: "{{ invite.teamssecret }}" - scopes: - - teams - - username: {{ aa.invite_username }} - password: "{{ invite_attribute_aggregation_secret }}" - scopes: - - attribute_aggregation - - username: {{ invite.lifecycle_user }} - password: "{{ invite.lifecycle_secret }}" - scopes: - - lifecycle - - username: internal - password: "{{ invite.internal_secret }}" - scopes: - - actuator - - username: {{ invite.profile_user }} - password: "{{ invite.profile_secret }}" - scopes: - - profile - - username: {{ invite.sp_dashboard_user }} - password: "{{ invite.sp_dashboard_secret }}" - organizationGUIDFallback: {{ invite.surf_idp_organization_guid }} - scopes: - - sp_dashboard - applications: - - manageId: {{ invite.sp_dashboard_manage_id }} - manageType: SAML20_SP - - username: {{ invite.access_user }} - password: "{{ invite.access_secret }}" - organizationGUIDFallback: {{ invite.surf_idp_organization_guid }} - scopes: - - access - applications: - - manageId: {{ invite.access_manage_id }} - manageType: OIDC10_RP + {{ invite.api_users | to_nice_yaml(indent=2) | indent(4, first=false) }} voot: group_urn_domain: "{{ invite.group_urn_domain }}" diff --git a/roles/manage/defaults/main.yml b/roles/manage/defaults/main.yml index 41c6f34d4..1f616ebe9 100644 --- a/roles/manage/defaults/main.yml +++ b/roles/manage/defaults/main.yml @@ -32,7 +32,13 @@ manage_tabs_enabled: - provisioning - sram - organisation + - sfo + - institution manage_docker_networks: - name: loadbalancer manage_server_restart_policy: always manage_server_restart_retries: 0 +manage_logback_json: false + +manage_stepup_raas: + - "urn:collab:person:example.com:admin" diff --git a/roles/manage/files/metadata_templates/institution.template.json b/roles/manage/files/metadata_templates/institution.template.json new file mode 100644 index 000000000..7fc560d0e --- /dev/null +++ b/roles/manage/files/metadata_templates/institution.template.json @@ -0,0 +1,19 @@ +{ + "entityid": "", + "metaDataFields": {}, + "identifier": "", + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [ + "tiqr" + ], + "number_of_tokens_per_identity": 3, + "use_ra": [], + "use_raa": [], + "select_raa": [], + "self_vet": true, + "allow_self_asserted_tokens": false, + "sso_on_2fa": false, + "stepup-client": "full" +} diff --git a/roles/manage/files/metadata_templates/sfo.template.json b/roles/manage/files/metadata_templates/sfo.template.json new file mode 100644 index 000000000..82fb90649 --- /dev/null +++ b/roles/manage/files/metadata_templates/sfo.template.json @@ -0,0 +1,14 @@ +{ + "name": "", + "entityid": "", + "metaDataFields": {}, + "public_key": "", + "acs": [], + "loa": "{{ stepup_loa_values_supported[0] }}", + "assertion_encryption_enabled": false, + "second_factor_only": true, + "second_factor_only_nameid_patterns": [], + "blacklisted_encryption_algorithms": [], + "allow_sso_on_2fa": true, + "set_sso_cookie_on_2fa": true +} diff --git a/roles/manage/files/policies/allowed_attributes.json b/roles/manage/files/policies/allowed_attributes.json index beb5c8363..3905646f7 100644 --- a/roles/manage/files/policies/allowed_attributes.json +++ b/roles/manage/files/policies/allowed_attributes.json @@ -15,7 +15,8 @@ "value": "urn:mace:dir:attribute-def:eduPersonAffiliation", "validationRegex": "^(student|staff|faculty|employee|member)$", "allowedInDenyRule": true, - "label": "Edu person affiliation" + "label": "Edu person affiliation", + "enum": true }, { "value": "urn:mace:dir:attribute-def:eduPersonScopedAffiliation", @@ -45,7 +46,8 @@ "value": "urn:collab:sab:surfnet.nl", "validationRegex": "^(Superuser|Instellingsbevoegde|OperationeelBeheerder|SURFconextbeheerder|DNS-Beheerder)$", "allowedInDenyRule": false, - "label": "SAB role" + "label": "SAB role", + "enum": true }, { "value": "urn:mace:dir:attribute-def:mail", diff --git a/roles/manage/tasks/main.yml b/roles/manage/tasks/main.yml index 9df3ecb97..50cae20fb 100644 --- a/roles/manage/tasks/main.yml +++ b/roles/manage/tasks/main.yml @@ -11,6 +11,15 @@ - "/opt/openconext/manage/metadata_templates" - "/opt/openconext/manage/policies" +- name: Copy Stepup stepup_config.json from inventory + ansible.builtin.template: + src: "stepup_config.json.j2" + dest: "/opt/openconext/manage/stepup_config.json" + owner: "root" + group: "root" + mode: "0644" + notify: restart manageserver + - name: Import the mongo CA file ansible.builtin.copy: src: "{{ inventory_dir }}/secrets/mongo/mongoca.pem" @@ -31,14 +40,14 @@ - name: Place the serverapplication configfiles ansible.builtin.template: src: "{{ item }}.j2" - dest: /opt/openconext/manage/{{ item }} - owner: root - group: root + dest: "/opt/openconext/manage/{{ item }}" + owner: "root" + group: "root" mode: "0644" with_items: - - application.yml - - logback.xml - - manage-api-users.yml + - "application.yml" + - "logback.xml" + - "manage-api-users.yml" notify: restart manageserver - name: Place old __cacert_entrypoint.sh script @@ -53,8 +62,8 @@ ansible.builtin.template: src: "metadata_configuration/{{ item }}.schema.json.j2" dest: "/opt/openconext/manage/metadata_configuration/{{ item }}.schema.json" - owner: root - group: root + owner: "root" + group: "root" mode: "0640" with_items: - "{{ manage_tabs_enabled }}" @@ -81,18 +90,17 @@ group: root mode: "0640" with_items: - - allowed_attributes.json - - extra_saml_attributes.json + - "allowed_attributes.json" + - "extra_saml_attributes.json" notify: - "restart manageserver" - name: Add the mongodb and mariadb docker network to the list of networks when MongoDB runs in Docker ansible.builtin.set_fact: manage_docker_networks: - - name: loadbalancer - - name: openconext_mongodb - - name: openconext_mariadb - when: mongodb_in_docker | default(false) | bool + - name: "loadbalancer" + - name: "openconext_mariadb" + when: mariadb_in_docker | default(false) | bool - name: Create and start the server container community.docker.docker_container: @@ -105,15 +113,22 @@ state: started networks: "{{ manage_docker_networks }}" mounts: - - source: /opt/openconext/manage/ - target: /config/ - type: bind - - source: /opt/openconext/manage/mongoca.pem - target: /certificates/mongoca.crt - type: bind - - source: /opt/openconext/manage/__cacert_entrypoint.sh - target: /__cacert_entrypoint.sh - type: bind + - source: "/opt/openconext/manage/" + target: "/config/" + type: "bind" + read_only: true + - source: "/opt/openconext/manage/mongoca.pem" + target: "/certificates/mongoca.crt" + type: "bind" + read_only: true + - source: "/opt/openconext/manage/__cacert_entrypoint.sh" + target: "/__cacert_entrypoint.sh" + type: "bind" + read_only: true + - source: "/opt/openconext/manage/stepup_config.json" + target: "/stepup_config.json" + type: "bind" + read_only: true command: "java -jar /app.jar -Xmx512m --spring.config.location=./config/" etc_hosts: host.docker.internal: host-gateway @@ -170,6 +185,8 @@ - source: /etc/localtime target: /etc/localtime type: bind + read_only: true - source: /opt/openconext/common/favicon.ico target: /var/www/favicon.ico type: bind + read_only: true diff --git a/roles/manage/templates/application.yml.j2 b/roles/manage/templates/application.yml.j2 index aec21cfdf..31085a6d7 100644 --- a/roles/manage/templates/application.yml.j2 +++ b/roles/manage/templates/application.yml.j2 @@ -53,11 +53,20 @@ push: user: {{ pdp.username }} password: "{{ pdp.password }}" enabled: {{ manage.pdp_push_enabled }} + stepup: + url: https://middleware.{{ base_domain }} + user: {{ manage.middleware_user }} + configuration_file: "file:///stepup_config.json" + password: {{ manage_middleware_password }} + enabled: {{ manage.stepup_push_enabled }} + product: name: Manage organization: {{ instance_name }} service_provider_feed_url: {{ manage_service_provider_feed_url }} + jira_base_url: https://servicedesk.surf.nl/jira/browse/ + jira_ticket_prefixes: CXT,SD supported_languages: {{ supported_language_codes }} show_oidc_rp: {{ manage_show_oidc_rp_tab }} diff --git a/roles/manage/templates/logback.xml.j2 b/roles/manage/templates/logback.xml.j2 index d1df41a7a..5a567f793 100644 --- a/roles/manage/templates/logback.xml.j2 +++ b/roles/manage/templates/logback.xml.j2 @@ -1,32 +1,49 @@ -#jinja2:lstrip_blocks: True - - - %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n - - + + + %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n + + - - {{ smtp_server }} - {{ noreply_email }} - {{ error_mail_to }} - {{ error_subject_prefix }}Unexpected error manage - + + {{ smtp_server }} + {{ noreply_email }} + {{ error_mail_to }} + {{ error_subject_prefix }}Unexpected error manage + - org.everit.json.schema.ValidationException - ERROR - - + org.everit.json.schema.ValidationException + ERROR + + - - +{% if manage_logback_json | bool -%} + + host.docker.internal:514 + + {"app":"manage"} + true + + [ignore] + [ignore] + [ignore] + + + +{%- endif %} - - - - + + + + + + + {% if manage_logback_json | bool -%} + + {%- endif %} + diff --git a/roles/manage/templates/metadata_configuration/institution.schema.json.j2 b/roles/manage/templates/metadata_configuration/institution.schema.json.j2 new file mode 100644 index 000000000..ac8f66e83 --- /dev/null +++ b/roles/manage/templates/metadata_configuration/institution.schema.json.j2 @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "institution", + "order": 11, + "type": "object", + "properties": { + "eid": { + "type": "number" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "entityid": { + "type": "string", + "minLength": 1 + }, + "identifier": { + "type": "string", + "info": "The unique identifier of the institution." + }, + "use_ra_locations": { + "type": "boolean" + }, + "show_raa_contact_information": { + "type": "boolean" + }, + "verify_email": { + "type": "boolean" + }, + "allowed_second_factors": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "yubikey", + "tiqr", + "webauthn", + "sms" + ] + } + }, + "number_of_tokens_per_identity": { + "type": "number" + }, + "use_ra": { + "type": "array", + "items": { + "type": "string" + } + }, + "use_raa": { + "type": "array", + "items": { + "type": "string" + } + }, + "select_raa": { + "type": "array", + "items": { + "type": "string" + } + }, + "self_vet": { + "type": "boolean" + }, + "allow_self_asserted_tokens": { + "type": "boolean" + }, + "sso_on_2fa": { + "type": "boolean" + }, + "stepup-client": { + "type": "string", + "enum": [ + "freerider", + "full" + ], + "default": "freerider" + }, + + "revisionid": { + "type": "number" + }, + "created": { + "type": [ + "string", + "null" + ] + }, + "revisionnote": { + "type": "string" + }, + "notes": { + "type": [ + "string", + "null" + ] + }, + "metaDataFields": { + "type": "object", + "properties": {}, + "patternProperties": {}, + "required": [], + "additionalProperties": false + } + }, + "required": [ + "name", + "entityid", + "identifier", + "stepup-client" + ], + "additionalProperties": false, + "indexes": [] +} diff --git a/roles/manage/templates/metadata_configuration/oidc10_rp.schema.json.j2 b/roles/manage/templates/metadata_configuration/oidc10_rp.schema.json.j2 index 31f775b8f..da12914c5 100644 --- a/roles/manage/templates/metadata_configuration/oidc10_rp.schema.json.j2 +++ b/roles/manage/templates/metadata_configuration/oidc10_rp.schema.json.j2 @@ -394,11 +394,16 @@ "type": "boolean", "info": "Set to make invisible in the dashboard for identity providers." }, - "coin:application_url": { + "coin:login_url": { "type": "string", "format": "url", "info": "The URL of the service used to log on." }, + "coin:application_url": { + "type": "string", + "format": "url", + "info": "The URL of the service with general information." + }, "coin:application_name": { "type": "string", "info": "The name of the service / application in related applications." diff --git a/roles/manage/templates/metadata_configuration/saml20_sp.schema.json.j2 b/roles/manage/templates/metadata_configuration/saml20_sp.schema.json.j2 index b55972aee..9914fbef1 100644 --- a/roles/manage/templates/metadata_configuration/saml20_sp.schema.json.j2 +++ b/roles/manage/templates/metadata_configuration/saml20_sp.schema.json.j2 @@ -486,11 +486,16 @@ "type": "boolean", "info": "Set to make invisible in the dashboard for identity providers." }, - "coin:application_url": { + "coin:login_url": { "type": "string", "format": "url", "info": "The URL of the service used to log on." }, + "coin:application_url": { + "type": "string", + "format": "url", + "info": "The URL of the service with general information." + }, "coin:application_name": { "type": "string", "info": "The name of the service / application in related applications." diff --git a/roles/manage/templates/metadata_configuration/sfo.schema.json.j2 b/roles/manage/templates/metadata_configuration/sfo.schema.json.j2 new file mode 100644 index 000000000..a96c321e9 --- /dev/null +++ b/roles/manage/templates/metadata_configuration/sfo.schema.json.j2 @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "sfo", + "order": 10, + "type": "object", + "properties": { + "eid": { + "type": "number" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "entityid": { + "type": "string", + "minLength": 1 + }, + "public_key": { + "type": "string", + "format": "certificate", + "info": "The supplied public certificate of the institution. This must be a PEM encoded certificate. DER, CRT or CER are not supported." + }, + "acs": { + "type": "array", + "items": { + "type": "string", + "format": "url" + }, + "info": "The ACS locations of this institution." + }, + "loa": { + "type": "string", + "enum": [ + {% for loa in [stepup_intrinsic_loa] + stepup_loa_values_supported %} + "{{ loa }}"{{ "," if not loop.last else ""}} + {% endfor %} + ], + "default": "{{ stepup_loa_values_supported[0] }}", + }, + "assertion_encryption_enabled": { + "type": "boolean" + }, + "second_factor_only": { + "type": "boolean" + }, + "second_factor_only_nameid_patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "blacklisted_encryption_algorithms": { + "type": "array", + "items": { + "type": "string" + } + }, + "allow_sso_on_2fa": { + "type": "boolean" + }, + "set_sso_cookie_on_2fa": { + "type": "boolean" + }, + "revisionid": { + "type": "number" + }, + "created": { + "type": [ + "string", + "null" + ] + }, + "revisionnote": { + "type": "string" + }, + "notes": { + "type": [ + "string", + "null" + ] + }, + "metaDataFields": { + "type": "object", + "properties": {}, + "patternProperties": {}, + "required": [], + "additionalProperties": false + } + }, + "required": [ + "name", + "entityid", + "public_key", + "acs", + "loa" + ], + "additionalProperties": false, + "indexes": [] +} diff --git a/roles/manage/templates/metadata_configuration/single_tenant_template.schema.json.j2 b/roles/manage/templates/metadata_configuration/single_tenant_template.schema.json.j2 index 3733b6a47..344edf725 100644 --- a/roles/manage/templates/metadata_configuration/single_tenant_template.schema.json.j2 +++ b/roles/manage/templates/metadata_configuration/single_tenant_template.schema.json.j2 @@ -379,11 +379,16 @@ "type": "boolean", "info": "Set to indicate the service in the dashboard for institutions is marked as to have disagreed to sign the aansluitovereenkomst." }, - "coin:application_url": { + "coin:login_url": { "type": "string", "format": "url", "info": "The URL of the service used to log on." }, + "coin:application_url": { + "type": "string", + "format": "url", + "info": "The URL of the service with general information." + }, "coin:application_name": { "type": "string", "info": "The name of the service / application in related applications." diff --git a/roles/manage/templates/metadata_configuration/sram.schema.json.j2 b/roles/manage/templates/metadata_configuration/sram.schema.json.j2 index c4a7dc31b..8a84ca871 100644 --- a/roles/manage/templates/metadata_configuration/sram.schema.json.j2 +++ b/roles/manage/templates/metadata_configuration/sram.schema.json.j2 @@ -342,11 +342,16 @@ "info": "Select this option to skip the consent for a user.", "default": true }, - "coin:application_url": { + "coin:login_url": { "type": "string", "format": "url", "info": "The URL of the service used to log on." }, + "coin:application_url": { + "type": "string", + "format": "url", + "info": "The URL of the service with general information." + }, "coin:application_name": { "type": "string", "info": "The name of the service / application in related applications." diff --git a/roles/manage/templates/stepup_config.json.j2 b/roles/manage/templates/stepup_config.json.j2 new file mode 100644 index 000000000..b44e34d77 --- /dev/null +++ b/roles/manage/templates/stepup_config.json.j2 @@ -0,0 +1,44 @@ +{ + "sraa": {{ manage_stepup_raas | tojson() }}, +{# part below is a literal template that is to be filled in by Manage, not by anisble -#} +{% raw -%} + "email_templates": { + "confirm_email": { + "en_GB": "

Dear {{ commonName }},

Thank you for registering your token. Please visit this link to verify your email address:

{{ verificationUrl }}

If you can not click on the URL, please copy the link and paste it in the address bar of your browser.

SURFnet

", + "nl_NL": "

Beste {{ commonName }},

Bedankt voor het registreren van je token. Klik op onderstaande link om je e-mailadres te bevestigen:

{{ verificationUrl }}

Is klikken op de link niet mogelijk? Kopieer dan de link en plak deze in de adresbalk van je browser.

SURFnet

" + }, + "registration_code_with_ras": { + "en_GB": "

Dear {{ commonName }},

Thank you for registering your token. Please visit one of the locations below within 14 days to get your token activated. After {{ expirationDate | localizeddate('full', 'none', locale) }} your activation code is no longer valid.

Please bring the following:

Activation code: {{ registrationCode }}

Location(s) to activate your token:

{% if ras is empty %}

No RAs are known.

{% else %} {% endif %}", + "nl_NL": "

Beste {{ commonName }},

Bedankt voor het registreren van je token. Ga binnen 14 dagen naar een van de onderstaande locaties om je token te laten activeren. Je activatiecode is geldig tot en met {{ expirationDate | localizeddate('full', 'none', locale) }}.

Neem daarbij het volgende mee:

Activatiecode: {{ registrationCode }}

Locatie(s) om je token te activeren:

{% if ras is empty %}

Er zijn geen RAs bekend.

{% else %} {% endif %}" + }, + "second_factor_verification_reminder_with_ras": { + "en_GB": "

Dear {{ commonName }},

You have registered, but not yet activated, a token. Please visit one of the locations below to get your token activated. After {{ expirationDate | localizeddate('full', 'none', locale) }} your activation code is no longer valid.

Please bring the following:

Activation code: {{ registrationCode }}

Location(s) to activate your token:

{% if ras is empty %}

No RAs are known.

{% else %} {% endif %}", + "nl_NL": "

Beste {{ commonName }},

Je hebt een token geregistreerd, maar het nog niet laten activeren. Je kunt tot en met {{ expirationDate | localizeddate('full', 'none', locale) }} bij een van de onderstaande locaties terecht om je token te laten activeren.

Neem daarbij het volgende mee:

Activatiecode: {{ registrationCode }}

Locatie(s) om je token te activeren:

{% if ras is empty %}

Er zijn geen RAs bekend.

{% else %} {% endif %}" + }, + "registration_code_with_ra_locations": { + "en_GB": "

Dear {{ commonName }},

Thank you for registering your token. Please visit one of the locations below within 14 days to get your token activated. After {{ expirationDate | localizeddate('full', 'none', locale) }} your activation code is no longer valid.

Please bring the following:

Activation code: {{ registrationCode }}

Location(s) to activate your token:

{% if raLocations is empty %}

No locations known.

{% else %} {% endif %}", + "nl_NL": "

Beste {{ commonName }},

Bedankt voor het registreren van je token. Ga binnen 14 dagen naar een van de onderstaande locaties om je token te laten activeren. Je activatiecode is geldig tot en met {{ expirationDate | localizeddate('full', 'none', locale) }}.

Neem daarbij het volgende mee:

Activatiecode: {{ registrationCode }}

Locatie(s) om je token te activeren:

{% if raLocations is empty %}

Er zijn geen Locaties bekend.

{% else %} {% endif %}" + }, + "second_factor_verification_reminder_with_ra_locations": { + "en_GB": "

Dear {{ commonName }},

You have registered, but not yet activated, a token. Please visit one of the locations below to get your token activated. After {{ expirationDate | localizeddate('full', 'none', locale) }} your activation code is no longer valid.

Please bring the following:

Activation code: {{ registrationCode }}

Location(s) to activate your token:

{% if raLocations is empty %}

No locations known.

{% else %} {% endif %}", + "nl_NL": "

Beste {{ commonName }},

Je hebt een token geregistreerd, maar het nog niet laten activeren. Je kunt tot en met {{ expirationDate | localizeddate('full', 'none', locale) }} bij een van de onderstaande locaties terecht om je token te laten activeren.

Neem daarbij het volgende mee:

Activatiecode: {{ registrationCode }}

Locatie(s) om je token te activeren:

{% if raLocations is empty %}

Er zijn geen Locaties bekend.

{% else %} {% endif %}" + }, + "vetted": { + "en_GB": "

Dear {{ commonName }},

Thank you for registering your token. Your token is ready to use. You can use this token for services connected to SURFconext that require two-step authentication. This e-mail contains more info on how to use your token.

Handle your token with care

Token lost?
Did you lose your token? Please visit {{ selfServiceUrl }} and remove your token registration. This way no one can take advantage of your token.

Replace token
Do you want to replace your token? Please visit {{ selfServiceUrl }}, remove your activated token and start the token registration process again.

Test token
Do you want to test your token? Please visit {{ selfServiceUrl }} and select the \"Test\" button next to the token you want to test.

", + "nl_NL": "

Beste {{ commonName }},

Bedankt voor het registreren van je token. Je token is nu klaar voor gebruik. Je kunt dit token gebruiken wanneer op SURFconext aangesloten services een tweede inlogstap vereisen. In deze e-mail vind je meer informatie over het gebruik van je token.

Ga zorgvuldig om met je token

Token verloren?
Wat moet je doen als je jouw token verloren bent? Ga naar {{ selfServiceUrl }} en verwijder je tokenregistratie. Zo kan niemand misbruik maken van jouw token.

Nieuw token aanvragen
Wil je jouw token vervangen? Log in op {{ selfServiceUrl }}, verwijder je geactiveerde token en doorloop het registratieproces opnieuw.

Token testen
Wil je de werking van je token testen? Log in op {{ selfServiceUrl }} en selecteer de \"Testen\" knop naast het token dat je wil testen.

" + }, + "second_factor_revoked": { + "en_GB": "

Dear {{ commonName }},

{% if isRevokedByRa %} The registration of your {{ tokenType }} with ID {{ tokenIdentifier }} was deleted by an administrator. {% else %} You have deleted the registration of your {{ tokenType }} token with ID {{ tokenIdentifier }}. If you did not delete your token you must immediately contact the support desk of your institution, as this may indicate that your account has been compromised. {% endif %}

You can no longer use this token to access SURFconext services that require two-step authentication.

Do you want to replace your token? Please visit {{ selfServiceUrl }} and register a new token.

", + "nl_NL": "

Beste {{ commonName }},

{% if isRevokedByRa %} De registratie van je {{ tokenType }} token met ID {{ tokenIdentifier }} is verwijderd door een beheerder. {% else %} Je hebt de registratie voor je {{ tokenType }} token met ID {{ tokenIdentifier }} verwijderd. Neem direct contact op met de helpdesk van je instelling als je dit zelf niet gedaan hebt, omdat dit kan betekenen dat je account gecompromitteerd is. {% endif %}

Je kunt dit token niet meer gebruiken om in te loggen bij op SURFconext aangesloten services die een tweede inlogstap vereisen.

Wil je een nieuw token aanvragen? Ga dan naar {{ selfServiceUrl }} en doorloop het registratieproces opnieuw.

" + }, + "recovery_token_created": { + "en_GB": "

Dear {{ commonName }},

Thank you for registering a recovery method. You can use this method if you want to reactivate a SURFsecureID token that you have lost.

Always make sure you have at least one recovery method available.

Best regards,

SURFnet

", + "nl_NL": "

Beste {{ commonName }},

Bedankt voor het registreren een herstelmethode. Je kunt deze methode gebruiken wanneer je een SURFsecureID token dat je verloren bent opnieuw wilt activeren.

Zorg er altijd voor dat je tenminste één herstelmethode beschikbaar hebt

Met vriendelijke groet,

SURF

" + }, + "recovery_token_revoked": { + "en_GB": "

Dear {{ commonName }},

{% if isRevokedByRa %} Your SURFsecureID recovery method was removed by an administrator. {% else %} Your SURFsecureID recovery method has been removed. Please contact your institution's helpdesk immediately if you did not do this yourself, as this could mean that your account has been compromised. {% endif %}

You can no longer use this recovery method to activate a SURFsecureID token.

Always make sure you have at least one recovery method available.

Best regards,

SURF

", + "nl_NL": "

Beste {{ commonName }},

{% if isRevokedByRa %} Je SURFsecureID herstelmethode is verwijderd door een beheerder. {% else %} Je SURFsecureID herstelmethode is verwijderd. Neem direct contact op met de helpdesk van je instelling als je dit niet zelf gedaan hebt, omdat dit kan betekenen dat je account gecompromitteerd is. {% endif %}

Je kunt deze herstelmethode niet meer gebruiken om een SURFsecureID token te activeren.

Zorg er altijd voor dat je tenminste één herstelmethode beschikbaar hebt.

Met vriendelijke groet,

SURF

" + } + } +} +{% endraw %} diff --git a/roles/mariadbdocker/tasks/main.yml b/roles/mariadbdocker/tasks/main.yml index 8f3d92b7e..8945d7e6f 100644 --- a/roles/mariadbdocker/tasks/main.yml +++ b/roles/mariadbdocker/tasks/main.yml @@ -30,7 +30,7 @@ - name: Create the MariaDB container community.docker.docker_container: name: openconext_mariadb - image: mariadb:10.6 + image: mariadb:10.11 state: started pull: true restart_policy: "always" diff --git a/roles/mongo/README.md b/roles/mongo/README.md index 9e96770e5..8ad896d21 100644 --- a/roles/mongo/README.md +++ b/roles/mongo/README.md @@ -14,6 +14,34 @@ Set the mongo_cluster_private_key variable encrypted in host_vars Please review the official Mongo documentation for more information. +# Mongo deployment + +To avoid surprisesyou can enable or disable cluster configuration with the boolean option mongo_configure_cluster. The role willonly initiate or reconfigure cluster if this is true (safest option is to use -e mongo_configure_cluster=true with your deployment when cluster configuration is necessary). +Another issue is the serial value, it is safest to set it to 1 in your playbook, if it is higher multiple mongo nodes can will be restarted at once and it can break your cluster. However when you want to intialise a new cluster you need to run the tasks parallel and serial needs to be as high as the amount of nodes. We handled this with a variable serial with the name serial_number in our playbook with a default 1. If cluster initialisation or reconfiguration is necessary use -e "serial_number=" + +Another option is creating separate playbooks for cluster adjustments/creation and for configuring non cluster related mongo settings. + + +See also https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_strategies.html#setting-the-batch-size-with-serial + +# Cluster reconfiguration + +Warning: the cluster reconfiguration option in the mongodb_replicationset module is experimental. and you can only add or remove one node at a time. + # Todo -- [ ] Add the possibility for adding and removing cluster members -- [ ] Add the possibility for a standalone mongo server +- [x] Check mongo_replication_roles and give a clear fail message when not set +- [ ] Add option to change the already existing admin user, for now change the password manually and change it in the ansible config accordingly +- [x] Add the possibility for adding and removing cluster members +- [x] Add the possibility for a standalone mongo server +- [x] Cluster changes can be enabled or disabled +- [ ] Reconfigure cluster always reports changed +- [ ] Initialise cluster always reports changed +- [ ] check mode for writeconcern change tasks does not report change () same for any other mongodb_shell task "remote module (community.mongodb.mongodb_shell) does not support check mode"} +- [X] Clearer error messaging for even number of votes +- [X] Role refuses to add users when a new cluster is built (3 nodes) (cannot add users on a broken cluster) +- [X] it would be helpfull if role (for example primary) is not defined in host_vars but in the mongo_cluster_members array +- [X] removing primary from the cluster will not work but the error is unclear, this is related to the todo above +- [ ] is it necessary to make votes configurable? +- [X] preflight check are cluster members in the inventory and monog_servers group +- [ ] Standalone mongo also requires cluster certificates, not logical although it doens't hurt +- [ ] Changes to mongo users are executed but not reported as an ansible change diff --git a/roles/mongo/defaults/main.yml b/roles/mongo/defaults/main.yml index a58b2a320..d96741302 100644 --- a/roles/mongo/defaults/main.yml +++ b/roles/mongo/defaults/main.yml @@ -13,35 +13,37 @@ mongo_servers: [] # Set this in group_vars # Not all mongo servers in the inventory are cluster members, so we use a separate list for this. # Set this in group_vars of your environment(s). The arbiter should go first, or change the mongo_arbiter_index. # mongo_cluster_members: -# - host: "mongoarbiter.example.com:27017" +# - host: "mongoarbiter.example.com" # priority: 1 # can vote, cannot become primary -# - host: "mongo2.example.com:27017" +# port: 27017 +# - host: "mongo2.example.com" # priority: 2 -# - host: "mongo1.example.com:27017" +# port: 27017 +# - host: "mongo1.example.com" # priority: 3 -# mongo_arbiter_index: 0 - -# The replication role -# mongo_replication_role: # Set this in host_vars, it can have the values: "primary", "secondary" or arbiter +# port: 27017 # Todo: there is a link between mongo_replication_role and priority (arbiter is priority 1, primary the highest) so # setting them separately is not ideal. # The port for mongo server -mongod_port: 27017 +mongo_port: 27017 # The password for admin user -mongo_admin_pass: "{{ mongo_admin_password }}" # Set this in secrets +# mongo_admin_password: # set this in secrets + +# Are we using a cluster? +mongo_mode: "cluster" # cluster or standalone # The name of the replication set -replica_set_name: "{{ instance_name }}" # Set this in group_vars +mongo_replica_set_name: "{{ instance_name }}" # Set this in group_vars # Add a database mongo: users: - - { name: managerw, db_name: metadata, password: "{{ mongo_passwords.manage }}" } - - { name: oidcsrw, db_name: oidc, password: "{{ mongo_passwords.oidcng }}" } - - { name: myconextrw, db_name: myconext, password: "{{ mongo_passwords.myconext }}" } + - { name: managerw, db_name: metadata, password: "{{ mongo_passwords.manage }}", role: "readWrite" } + - { name: oidcsrw, db_name: oidc, password: "{{ mongo_passwords.oidcng }}", role: "readWrite"} + - { name: myconextrw, db_name: myconext, password: "{{ mongo_passwords.myconext }}", role: "readWrite" } # Listen on all addresses by default mongo_bind_listen_address: "0.0.0.0" @@ -53,3 +55,32 @@ mongo_pki_dir: "/etc/pki/mongo" # Users and groups mongo_group: "mongod" + +# Paths +mongo_config_file: "/etc/mongod.conf" +mongo_data_path: "/var/lib/mongo" + +mongo_pymongo_version: 4.16.0 + +# cluster members +# set in group_vars +# mongo_cluster_members: +# - host: mongo1.example.com +# priority: 3 +# votes: 1 +# port: 27017 +# - host: mongo2.example.com +# priority: 2 +# votes: 1 +# port: 27017 +# - host: mongo3.example.com +# priority: 1 +# votes: 1 +# port: 27017 +# arbiterOnly: true + +mongo_cluster_write_concern: "majority" +mongo_cluster_write_timeout: 5000 + +# to avoid surprises only initiate or reconfigure cluster if this is true (safest option is to use -e mongo_configure_cluster=true with your deployment when cluster configuration is necessary) +mongo_configure_cluster: false diff --git a/roles/mongo/tasks/clusterconfig.yml b/roles/mongo/tasks/clusterconfig.yml index ced96e9b9..dd819d3da 100644 --- a/roles/mongo/tasks/clusterconfig.yml +++ b/roles/mongo/tasks/clusterconfig.yml @@ -1,60 +1,191 @@ --- -# todo this weorks only for new deployments -# rewrite so mongo config can be changed and cluster members can be added or removed -- name: Check if hosts are in clustered - ansible.builtin.command: mongosh --port {{ mongod_port }} --quiet --eval 'db.isMaster().hosts' - register: check_cluster - changed_when: false - check_mode: false - -- name: Debug check_cluster variable +# In this task file the cluster is configured + +# priority moet matchen met replication role, of replication role uit cluster mebers halen? +# todo write concern zetten + +# Do some preflight checks +- name: Check some cluster related variables + when: mongo_mode == "cluster" + block: + - name: Fail on undefined mongo_replica_set_name + when: mongo_replica_set_name is not defined + ansible.builtin.fail: + msg: "Something is wrong, mongo_mode was set to cluster but mongo_replica_set_name is undefined." + +- name: Debug replica settings ansible.builtin.debug: - msg: "{{ check_cluster }}" + msg: "Replica set name {{ mongo_replica_set_name }}" verbosity: 2 -- name: Debug mongo_cluster_members variable +# Loop over cluster members and check their presence in mong_servers group and their mode (not standalone) + +- name: Check if mongo_cluster_members exist in inventory group + ansible.builtin.assert: + that: + - item.host in groups['mongo_servers'] + fail_msg: "Server '{{ item.host }}' is not in the mongo_servers inventory group" + success_msg: "Server '{{ item.host }}' found in mongo_servers inventory group" + run_once: true + loop: "{{ mongo_cluster_members }}" + +# Loop over cluster members and check for primary + +- name: Set primary host fact + ansible.builtin.set_fact: + mongo_primary_host: "{{ (mongo_cluster_members | max(attribute='priority')).host }}" + +- name: Debug primary settings ansible.builtin.debug: - msg: "{{ mongo_cluster_members }}" + msg: "Primary is {{ mongo_primary_host }}" verbosity: 2 -- name: Debug mongo_replication_role variable +# What is the replication role of the current host +- name: Debug replication role settings ansible.builtin.debug: - msg: "{{ mongo_replication_role }}" + msg: "This nodes replication role is {{ mongo_replication_role }}" verbosity: 2 -- name: Initial cluster initialisation - community.mongodb.mongodb_replicaset: - login_host: localhost - login_user: admin - login_port: "{{ mongod_port }}" - login_password: "{{ mongo_admin_password }}" - replica_set: "{{ replica_set_name }}" - members: "{{ mongo_cluster_members }}" - arbiter_at_index: "{{ mongo_arbiter_index | default(0) }}" - validate: false - run_once: true - when: mongo_replication_role == 'primary' +# Cannot initialise a cluster without starting....... +- name: Enable and start mongod + ansible.builtin.service: + name: mongod.service + enabled: true + state: started -- name: Wait until cluster health is ok - community.mongodb.mongodb_status: - login_user: admin - login_password: "{{ mongo_admin_password }}" - login_database: admin - login_port: "{{ mongod_port }}" - validate: default - poll: 5 - interval: 12 - replica_set: "{{ replica_set_name }}" +# Initialise cluster block +- name: Initialise or reconfigure cluster block when: mongo_replication_role == 'primary' + block: + - name: Check if replica set is already initialised + community.mongodb.mongodb_shell: + login_host: localhost + login_user: admin + login_port: "{{ mongo_port }}" + login_password: "{{ mongo_admin_password }}" + eval: "rs.status().ok" + db: admin + register: rs_already_init + ignore_errors: true -- name: Add the admin user - community.mongodb.mongodb_user: - database: admin - name: admin - password: "{{ mongo_admin_password }}" - login_port: "{{ mongod_port }}" - roles: root - state: present - when: check_cluster.stdout == "" - no_log: true - run_once: true + - name: Debug cluster initialization check + ansible.builtin.debug: + msg: "{{ rs_already_init }}" + verbosity: 2 + + # This should be possible with community.mongodb.mongodb_replicaset + # But we keep getting authenticatione error so leave it like this for now + - name: Initialise replica set if necessary + community.mongodb.mongodb_shell: + login_host: localhost + login_user: admin + login_port: "{{ mongo_port }}" + login_password: "{{ mongo_admin_password }}" + eval: | + rs.initiate({ + _id: "{{ mongo_replica_set_name }}", + members: [ + {% for m in mongo_cluster_members %} + { _id: {{ loop.index0 }}, host: "{{ m.host }}:{{ m.port }}", priority: {{ m.priority }}, votes: {{ m.votes }}{% if m.arbiterOnly is defined and m.arbiterOnly and m.arbiterOnly == true %}, arbiterOnly: true {% endif %} }{{ "," if not loop.last else "" }} + {% endfor %} + ] + }) + db: admin + when: rs_already_init.failed + register: rs_init + + - name: Debug cluster initialization + ansible.builtin.debug: + msg: "{{ rs_init }}" + verbosity: 2 + + - name: Format members list + ansible.builtin.set_fact: + mongo_cluster_members_formatted: "{{ mongo_cluster_members_formatted | default([]) + [m | combine({'host': m.host ~ ':' ~ (m.port | string)}) | dict2items | rejectattr('key', 'eq', 'port') | list | items2dict] }}" + loop: "{{ mongo_cluster_members }}" + loop_control: + loop_var: m + + - name: Debug members list + ansible.builtin.debug: + msg: "{{ mongo_cluster_members }}" + verbosity: 2 + + - name: Debug formatted members list + ansible.builtin.debug: + msg: "{{ mongo_cluster_members_formatted }}" + verbosity: 2 + + # Reconfigure cluster + # todo: this always returns changed even when nothing changes + - name: Reconfigure cluster if necessary + community.mongodb.mongodb_replicaset: + login_host: localhost + login_user: admin + login_password: "{{ mongo_admin_password }}" + login_port: "{{ mongo_port }}" + reconfigure: true + replica_set: "{{ mongo_replica_set_name }}" + members: "{{ mongo_cluster_members_formatted }}" + register: rs_reconfigure + + - name: Debug cluster reconfiguration + ansible.builtin.debug: + msg: "{{ rs_reconfigure }}" + verbosity: 2 + + - name: Wait for the replicaset to stabilise + community.mongodb.mongodb_status: + replica_set: "{{ mongo_replica_set_name }}" + login_host: localhost + login_user: admin + login_password: "{{ mongo_admin_password }}" + login_port: "{{ mongo_port }}" + poll: 5 + interval: 30 + validate: minimal # default fails on even number of servers and although this is not a great situation, it is sometimes the temporary situation because we can onlye add or remove 1 node at a time + + # Cluster settings that cannot be changed with mongodb_replicaset + + - name: Get current default write concern + community.mongodb.mongodb_shell: + login_host: localhost + login_port: 27017 + login_user: admin + login_password: "{{ mongo_admin_password }}" + eval: "db.adminCommand({ getDefaultRWConcern: 1 })" + register: current_write_concern + changed_when: false + + - name: Debug write concern check + ansible.builtin.debug: + msg: "{{ current_write_concern.transformed_output.defaultWriteConcern }}" + verbosity: 2 + when: current_write_concern.transformed_output.defaultWriteConcern is defined + + - name: Set default write concern + when: > + current_write_concern.transformed_output.defaultWriteConcern is defined + and + (current_write_concern.transformed_output.defaultWriteConcern.w | string != mongo_cluster_write_concern | default('majority') | string + or + current_write_concern.transformed_output.defaultWriteConcern.wtimeout | int != mongo_cluster_write_timeout | default(5000) | int) + or current_write_concern.transformed_output.defaultWriteConcern is not defined + block: + - name: "set write concern majority" + when: mongo_cluster_write_concern == "majority" + community.mongodb.mongodb_shell: + login_host: localhost + login_user: admin + login_password: "{{ mongo_admin_password }}" + login_port: "{{ mongo_port }}" + eval: "db.adminCommand({ setDefaultRWConcern: 1, defaultWriteConcern: { w: \"{{ mongo_cluster_write_concern | default('majority') }}\", wtimeout: {{ mongo_cluster_write_timeout | default(5000) }} } })" + # could not get this to work with either majority with quotes or number without quotes so for now an ugly fix + - name: "set write concern numeric" + when: mongo_cluster_write_concern != "majority" + community.mongodb.mongodb_shell: + login_host: localhost + login_user: admin + login_password: "{{ mongo_admin_password }}" + login_port: "{{ mongo_port }}" + eval: "db.adminCommand({ setDefaultRWConcern: 1, defaultWriteConcern: { w: {{ mongo_cluster_write_concern | default('majority') }}, wtimeout: {{ mongo_cluster_write_timeout | default(5000) }} } })" diff --git a/roles/mongo/tasks/clusterhealthcheck.yml b/roles/mongo/tasks/clusterhealthcheck.yml new file mode 100644 index 000000000..aa129ed5b --- /dev/null +++ b/roles/mongo/tasks/clusterhealthcheck.yml @@ -0,0 +1,88 @@ +--- +# task file to check if cluster is up and running + +- name: Cluster check when we are not in cluster config mode + when: not mongo_configure_cluster | bool + block: + # Get the replicaset status and fail on minimal (everything but even number of nodes) + - name: Check replicaset status + community.mongodb.mongodb_status: + login_host: localhost + login_user: admin + login_port: "{{ mongo_port }}" + login_password: "{{ mongo_admin_password }}" + replica_set: "{{ mongo_replica_set_name }}" + poll: 3 + interval: 10 + register: replica_status + ignore_errors: true + + - name: Debug replica set status + ansible.builtin.debug: + msg: "{{ replica_status }}" + verbosity: 2 + + # Message for non cluster config mode + - name: Fail when there is no cluster reconfiguration options set + ansible.builtin.fail: + msg: "Your mongo cluster is broken or non existent and mongo_configure_cluster is disabled, consider enabling it and fix your cluster. The error: {{ replica_status.msg }}." + when: + - replica_status.failed + +- name: Cluster check when we are in cluster config mode + when: mongo_configure_cluster | bool + block: + # Get the replicaset status and fail on minimal (everything but even number of nodes) + - name: Check replicaset status minimal + community.mongodb.mongodb_status: + login_host: localhost + login_user: admin + login_port: "{{ mongo_port }}" + login_password: "{{ mongo_admin_password }}" + replica_set: "{{ mongo_replica_set_name }}" + poll: 3 + interval: 10 + validate: minimal + register: replica_status_minimal + ignore_errors: true + + # Message for cluster config mode gone wrong + - name: Fail when you misconfigured your replica cluster + ansible.builtin.fail: + msg: "Your mongo cluster is broken, error: {{ replica_status_minimal.msg }}." + when: + - replica_status_minimal.failed + + # Get the replicaset status votes + - name: Check replicaset status votes + community.mongodb.mongodb_status: + login_host: localhost + login_user: admin + login_port: "{{ mongo_port }}" + login_password: "{{ mongo_admin_password }}" + replica_set: "{{ mongo_replica_set_name }}" + poll: 3 + interval: 10 + validate: votes + register: replica_status_votes + ignore_errors: true + + # Message for cluster config mode wrong amount of votes + - name: Fail when you misconfigured your replica cluster + ansible.builtin.fail: + msg: | + Your mongo cluster doesn't have the right amount of members, + perhaps you are adding new nodes one by one, + in that case add the next node to cluster_members and run the play again. + The error message is: {{ replica_status_votes.msg }}." + when: + - replica_status_votes.failed + + # In non cluster config mode we use replica_status + # and here replica_status_votes and replica_status_minimal + # for better error messages, but we need a general replica_status + # for for example users.yml, so lets set it here + - name: Set a value for replica_status + ansible.builtin.set_fact: + replica_status: + failed: False diff --git a/roles/mongo/tasks/generalconfig.yml b/roles/mongo/tasks/generalconfig.yml new file mode 100644 index 000000000..7bc094d4f --- /dev/null +++ b/roles/mongo/tasks/generalconfig.yml @@ -0,0 +1,52 @@ +--- + +- name: Enable and start mongod for the first time + ansible.builtin.service: + name: mongod.service + enabled: true + state: started + +- name: Check if mongodb authentication is activated + ansible.builtin.shell: + cmd: "mongosh 'mongodb://127.0.0.1:{{ mongo_port }}/admin' --eval 'db.runCommand({ usersInfo: 1 })'" + register: mongo_authentication_disabled + changed_when: false + ignore_errors: true + check_mode: false # This can safely run in check mode because it is not changing anything + failed_when: mongo_authentication_disabled.rc > 1 # rc=1 means command failed because authentication is enabled, we need to know that but we don't need to see an error + +- name: Debug mongodb authentication check + ansible.builtin.debug: + msg: "{{ mongo_authentication_disabled }}" + verbosity: 2 + +- name: configure primary or standalone + when: mongo_mode == "standalone" or mongo_replication_role == "primary" + block: + # first run add admin user without logging in + - name: Add the admin user + community.mongodb.mongodb_user: + login_database: admin + database: admin + name: admin + password: "{{ mongo_admin_password }}" + login_port: "{{ mongo_port }}" + roles: root + state: present + # todo enable no_log: true + when: mongo_authentication_disabled.rc == 0 + +# Config for standalone and replication server +- name: Install mongodb.conf file + ansible.builtin.template: + src: "mongod.conf.j2" + dest: "/etc/mongod.conf" + owner: root + group: root + mode: "0644" + backup: true + notify: Restart mongod + +# restart mongo right away with authentication enabled +- name: Flush handlers + ansible.builtin.meta: flush_handlers \ No newline at end of file diff --git a/roles/mongo/tasks/install.yml b/roles/mongo/tasks/install.yml index 673d465e3..9103a2ca8 100644 --- a/roles/mongo/tasks/install.yml +++ b/roles/mongo/tasks/install.yml @@ -1,6 +1,5 @@ --- - name: Create the repository for mongodb - when: ansible_os_family == 'RedHat' ansible.builtin.template: src: "mongo.repo.j2" dest: "/etc/yum.repos.d/mongo.repo" @@ -8,7 +7,6 @@ mode: "0640" - name: Install the mongodb package and some helper packages - when: ansible_os_family == 'RedHat' ansible.builtin.yum: name: - mongodb-org @@ -17,7 +15,7 @@ - name: Install pymongo ansible.builtin.pip: - name: pymongo + name: pymongo=={{ mongo_pymongo_version }} - name: Install kernel settings script ansible.builtin.copy: @@ -52,17 +50,3 @@ value: 128000 state: present -- name: Install mongodb.conf file - ansible.builtin.template: - src: "mongod.conf.j2" - dest: "/etc/mongod.conf" - owner: root - group: root - mode: "0644" - notify: Restart mongod - -- name: Enable and start mongod - ansible.builtin.service: - name: mongod.service - enabled: true - state: started diff --git a/roles/mongo/tasks/main.yml b/roles/mongo/tasks/main.yml index b0a30b31f..d55d8fa8a 100644 --- a/roles/mongo/tasks/main.yml +++ b/roles/mongo/tasks/main.yml @@ -1,8 +1,26 @@ --- +# Main task file mongo role + +- name: Message for non redhat family servers + when: ansible_facts['os_family'] != 'RedHat' + ansible.builtin.fail: + msg: "Sorry, this role only works on RedHat family servers" + - name: Install and configure mongo on redhat family servers - when: ansible_os_family == 'RedHat' + when: + - ansible_facts['os_family'] == 'RedHat' block: - - name: Use temporarily python3 as remote interpreter, this fixes pymongo + - name: Debug standalone or cluster mode + ansible.builtin.debug: + msg: "{{ mongo_mode }}" + verbosity: 2 + + - name: Debug cluster reconfiguration is allowed + ansible.builtin.debug: + msg: "{{ mongo_configure_cluster }}" + verbosity: 2 + + - name: Use temporarily python3 as remote interpreter, this fixes pymongo # todo is this still necessary? ansible.builtin.set_fact: ansible_python_interpreter: "/usr/bin/python3" tags: mongo_users @@ -11,30 +29,47 @@ ansible.builtin.include_tasks: file: install.yml - - ansible.builtin.meta: flush_handlers - - name: Include Certificate tasks ansible.builtin.include_tasks: file: certs.yml - # - name: Include cluster installation tasks - # ansible.builtin.include_tasks: - # file: clusterconfig.yml + - name: Include General config tasks + ansible.builtin.include_tasks: + file: generalconfig.yml + + - name: Include cluster configuration tasks + ansible.builtin.include_tasks: + file: clusterconfig.yml + when: + - mongo_mode == "cluster" + - mongo_configure_cluster | bool # safest option is to set this to false and enable with -e mongo_configure_cluster=true + + - name: Include cluster health check tasks + ansible.builtin.include_tasks: + file: clusterhealthcheck.yml + when: + - mongo_mode == "cluster" + - mongo_replication_role == 'primary' + + - name: Include Service tasks + ansible.builtin.include_tasks: + file: services.yml - name: Include user creation ansible.builtin.include_tasks: file: users.yml + # Cannot add users on a broken cluster + when: > + (mongo_mode == 'cluster' and mongo_replication_role is defined and mongo_replication_role == 'primary' and not replica_status.failed) + or mongo_mode == 'standalone' - name: Include postinstallation tasks ansible.builtin.include_tasks: file: postinstall.yml - - name: Use python2 again as remote interpreter + - name: Use python2 again as remote interpreter on centos 7 ansible.builtin.set_fact: ansible_python_interpreter: "/usr/bin/python" - when: ansible_distribution == 'CentOS' and ansible_distribution_major_version == '7' - -- name: Message for non redhat family servers - when: ansible_os_family != 'RedHat' - ansible.builtin.debug: - msg: "Sorry, this role only works on RedHat family servers" + when: + - ansible_facts['distribution'] == 'CentOS' + - ansible_facts['distribution_major_version'] == '7' diff --git a/roles/mongo/tasks/postinstall.yml b/roles/mongo/tasks/postinstall.yml index e474a0b1e..027d05f1c 100644 --- a/roles/mongo/tasks/postinstall.yml +++ b/roles/mongo/tasks/postinstall.yml @@ -15,29 +15,33 @@ group: root mode: "0700" -- name: Install the backup script - ansible.builtin.template: - src: "backup_mongo.pl.j2" - dest: "/usr/local/sbin/backup_mongo.pl" - mode: "0700" - owner: root - group: root - when: mongo_replication_role != 'arbiter' +- name: Configure backup + when: mongo_replication_role is not defined or mongo_replication_role != 'arbiter' + block: + - name: Install the backup script + ansible.builtin.template: + src: "backup_mongo.pl.j2" + dest: "/usr/local/sbin/backup_mongo.pl" + mode: "0700" + owner: root + group: root + - name: Create cron symlink for backup script + ansible.builtin.file: + src: "/usr/local/sbin/backup_mongo.pl" + dest: "/etc/cron.daily/mongodb_backup" + state: link + mode: "0700" + owner: root -- name: Create cron symlink for backup script - ansible.builtin.file: - src: "/usr/local/sbin/backup_mongo.pl" - dest: "/etc/cron.daily/mongodb_backup" - state: link - mode: "0700" - owner: root - when: mongo_replication_role != 'arbiter' +- name: Debug mongo_cluster_members + debug: + msg: "{{ item.host }}" + verbosity: 2 + loop: + "{{ mongo_cluster_members }}" + when: mongo_mode == "cluster" -# TODO: this template gets mongo_servers from -# the inventory, maybe change that to group vars -# this is not on an per app basis. These are mongoservers -# in the same cluster. - name: Create mongosh config file ansible.builtin.template: src: mongoshrc.js.j2 diff --git a/roles/mongo/tasks/services.yml b/roles/mongo/tasks/services.yml new file mode 100644 index 000000000..5caa4c54c --- /dev/null +++ b/roles/mongo/tasks/services.yml @@ -0,0 +1,6 @@ +--- +- name: Enable and start mongod + ansible.builtin.service: + name: mongod.service + enabled: true + state: started \ No newline at end of file diff --git a/roles/mongo/tasks/standaloneconfig.yml b/roles/mongo/tasks/standaloneconfig.yml new file mode 100644 index 000000000..a8ec00126 --- /dev/null +++ b/roles/mongo/tasks/standaloneconfig.yml @@ -0,0 +1 @@ +# todo mag weg? \ No newline at end of file diff --git a/roles/mongo/tasks/users.yml b/roles/mongo/tasks/users.yml index a218bac46..5afa45151 100644 --- a/roles/mongo/tasks/users.yml +++ b/roles/mongo/tasks/users.yml @@ -1,13 +1,32 @@ -- name: Create mongo database users # requires pymongo 4+ +- name: Create mongo database users cluster # requires pymongo 4+ + when: + - mongo_mode == "cluster" + - mongo_replication_role == "primary" community.mongodb.mongodb_user: login_database: admin database: "{{ item.db_name }}" login_user: admin - login_password: "{{ mongo_admin_pass }}" + login_password: "{{ mongo_admin_password }}" name: "{{ item.name }}" password: "{{ item.password }}" - roles: readWrite - replica_set: "{{ replica_set_name }}" + roles: "{{ item.role | default('readWrite')}}" + replica_set: "{{ mongo_replica_set_name }}" + no_log: true + run_once: true + with_items: "{{ mongo.users }}" + changed_when: false + tags: mongo_users + +- name: Create mongo database users single server # requires pymongo 4+ + when: mongo_mode != "cluster" + community.mongodb.mongodb_user: + login_database: admin + database: "{{ item.db_name }}" + login_user: admin + login_password: "{{ mongo_admin_password }}" + name: "{{ item.name }}" + password: "{{ item.password }}" + roles: "{{ item.role | default('readWrite') }}" no_log: true run_once: true with_items: "{{ mongo.users }}" diff --git a/roles/mongo/templates/mongod.conf.j2 b/roles/mongo/templates/mongod.conf.j2 index f5e990add..93fe38ba1 100644 --- a/roles/mongo/templates/mongod.conf.j2 +++ b/roles/mongo/templates/mongod.conf.j2 @@ -1,8 +1,5 @@ systemLog: - destination: file - logRotate: reopen - logAppend: true - path: /var/log/mongodb/mongod.log + destination: syslog net: bindIp: {{ mongo_bind_listen_address }} @@ -14,10 +11,12 @@ net: allowConnectionsWithoutCertificates: true storage: - dbPath: /var/lib/mongo + dbPath: {{ mongo_data_path }} +{% if mongo_replica_set_name is defined and mongo_mode == "cluster" %} replication: - replSetName: {{ replica_set_name }} + replSetName: {{ mongo_replica_set_name }} +{% endif %} security: authorization: enabled diff --git a/roles/mongo/templates/mongoshrc.js.j2 b/roles/mongo/templates/mongoshrc.js.j2 index 9faf2cdb0..c8b4c92f4 100644 --- a/roles/mongo/templates/mongoshrc.js.j2 +++ b/roles/mongo/templates/mongoshrc.js.j2 @@ -1,2 +1,5 @@ -db = connect("mongodb://admin:{{ mongo_admin_password }}@{% for mongo_server in mongo_servers %}{{ mongo_server }}:{{ mongod_port }}{% if not loop.last %},{% endif %}{% endfor %}?ssl=true&tlsCAFile=/etc/pki/mongo/mongoca.pem") - +{% if mongo_mode == "cluster" %} +db = connect("mongodb://admin:{{ mongo_admin_password }}@{% for mongo_server in mongo_cluster_members %}{{ mongo_server.host }}:{{ mongo_server.port }}{% if not loop.last %},{% endif %}{% endfor %}?ssl=true&tlsCAFile=/etc/pki/mongo/mongoca.pem") +{% else %} +db = connect("mongodb://admin:{{ mongo_admin_password }}@{{ ansible_facts['fqdn'] }}:{{ mongo_port }}?ssl=true&tlsCAFile=/etc/pki/mongo/mongoca.pem") +{% endif %} diff --git a/roles/myconext/tasks/main.yml b/roles/myconext/tasks/main.yml index 326668702..097ad191b 100644 --- a/roles/myconext/tasks/main.yml +++ b/roles/myconext/tasks/main.yml @@ -116,13 +116,6 @@ group: "root" mode: "0755" -- name: Add the mongodb docker network to the list of networks when MongoDB runs in Docker - ansible.builtin.set_fact: - myconext_docker_networks: - - name: loadbalancer - - name: openconext_mongodb - when: mongodb_in_docker | default(false) | bool - - name: Create and start the server container community.docker.docker_container: name: myconextserver diff --git a/roles/oidcng/tasks/main.yml b/roles/oidcng/tasks/main.yml index a306fa7f1..981cee255 100644 --- a/roles/oidcng/tasks/main.yml +++ b/roles/oidcng/tasks/main.yml @@ -88,13 +88,6 @@ group: "root" mode: "0755" -- name: Add the mongodb docker network to the list of networks when MongoDB runs in Docker - ansible.builtin.set_fact: - oidcng_docker_networks: - - name: loadbalancer - - name: openconext_mongodb - when: mongodb_in_docker | default(false) | bool - - name: Create and start the server container community.docker.docker_container: name: oidcngserver diff --git a/roles/oidcng/templates/logback.xml.j2 b/roles/oidcng/templates/logback.xml.j2 index 7b38d5627..8ca540620 100644 --- a/roles/oidcng/templates/logback.xml.j2 +++ b/roles/oidcng/templates/logback.xml.j2 @@ -2,54 +2,54 @@ - - - %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n - - + + + %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n + + - - host.docker.internal:514 + + host.docker.internal:514 - {"app":"oidcng"} - true - - [ignore] - [ignore] - [ignore] - - + {"app":"oidcng"} + true + + [ignore] + [ignore] + [ignore] + + - - {{ smtp_server }} - {{ noreply_email }} - {{ error_mail_to }} - {{ error_subject_prefix }}Unexpected error oidcng - + + {{ smtp_server }} + {{ noreply_email }} + {{ error_mail_to }} + {{ error_subject_prefix }}Unexpected error oidcng + - com.nimbusds.oauth2.sdk.ParseException - org.springframework.security.authentication.BadCredentialsException - oidc.exceptions.UnauthorizedException - oidc.exceptions.RedirectMismatchException - org.springframework.dao.EmptyResultDataAccessException - java.lang.IllegalArgumentException - ERROR - - + com.nimbusds.oauth2.sdk.ParseException + org.springframework.security.authentication.BadCredentialsException + oidc.exceptions.UnauthorizedException + oidc.exceptions.RedirectMismatchException + org.springframework.dao.EmptyResultDataAccessException + java.lang.IllegalArgumentException + ERROR + + - - - - -{%if oidcng_logback_email |bool %} - -{%endif%} - -{%if oidcng_logback_json |bool %} - -{%endif%} - + + + + + {% if oidcng_logback_email |bool %} + + {% endif %} + + {% if oidcng_logback_json |bool %} + + {% endif %} + diff --git a/roles/pdp/handlers/main.yml b/roles/pdp/handlers/main.yml index c6179bb34..9ce5432e7 100644 --- a/roles/pdp/handlers/main.yml +++ b/roles/pdp/handlers/main.yml @@ -1,9 +1,9 @@ - name: restart pdpserver community.docker.docker_container: - name: pdpserver - state: started + name: "pdp" + state: "started" restart: true # avoid restarting it creates unexpected data loss according to docker_container_module notes comparisons: - '*': ignore - when: pdpservercontainer is success and pdpservercontainer is not change + '*': "ignore" + when: "pdpservercontainer is success and pdpservercontainer is not change" diff --git a/roles/pdp/templates/serverapplication.yml.j2 b/roles/pdp/templates/serverapplication.yml.j2 index 762c2dbdc..a6d33a914 100644 --- a/roles/pdp/templates/serverapplication.yml.j2 +++ b/roles/pdp/templates/serverapplication.yml.j2 @@ -41,9 +41,9 @@ email: voot: serviceUrl: https://voot.{{ base_domain }} sab: - password: {{ aa.sab_rest_password }} - userName: {{ aa.sab_rest_username }} - endpoint: {{ aa.sab_rest_endpoint }} + password: {{ pdp.sab_rest_password }} + userName: {{ pdp.sab_rest_username }} + endpoint: {{ pdp.sab_rest_endpoint }} policies: cachePolicies: {{ pdp.cache_policies }} manage: diff --git a/roles/remove-java-app/defaults/main.yml b/roles/remove-java-app/defaults/main.yml deleted file mode 100644 index c2ec013e2..000000000 --- a/roles/remove-java-app/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -java_apps_to_remove: [] diff --git a/roles/remove-java-app/handlers/main.yml b/roles/remove-java-app/handlers/main.yml deleted file mode 100644 index 17ddb0097..000000000 --- a/roles/remove-java-app/handlers/main.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- name: daemon_reload - systemd: - daemon_reload: yes - -- name: restart httpd - systemd: - name: httpd - state: restarted diff --git a/roles/remove-java-app/tasks/main.yml b/roles/remove-java-app/tasks/main.yml deleted file mode 100644 index b80baffbd..000000000 --- a/roles/remove-java-app/tasks/main.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -- name: Remove the httpd configuration - file: - path: /etc/httpd/conf.d/{{ item | replace('-','_') }}.conf - state: absent - notify: - - restart httpd - with_items: - - "{{ java_apps_to_remove }}" - - -- name: Disable and stop the app - service: - name: "{{ item }}" - enabled: no - state: stopped - with_items: - - "{{ java_apps_to_remove }}" - -- name: Remove the app dir and its contents - file: - path: /opt/{{ item }}/ - state: absent - with_items: - - "{{ java_apps_to_remove }}" - -- name: Remove the www dir - file: - path: /var/www/{{ item }}/ - state: absent - with_items: - - "{{ java_apps_to_remove }}" - -- name: Remove the logs - file: - path: /var/log/{{ item }}/ - state: absent - with_items: - - "{{ java_apps_to_remove }}" - -- name: Force a daemon reload before removing the systemd file - systemd: - daemon_reload: yes - -- name: Remove the systemd service file - file: - path: "/etc/systemd/system/{{ item }}.service" - state: absent - notify: - - daemon_reload - with_items: - - "{{ java_apps_to_remove }}" - register: systemd_removed - -- name: Reset failed services - command: "systemctl reset-failed" - diff --git a/roles/rsyslog/tasks/main.yml b/roles/rsyslog/tasks/main.yml index a531fd677..0eef86d46 100644 --- a/roles/rsyslog/tasks/main.yml +++ b/roles/rsyslog/tasks/main.yml @@ -4,8 +4,16 @@ - rsyslog - rsyslog-gnutls - rsyslog-relp + state: present + notify: + - "restart rsyslog" + +- name: Install rsyslog and python modules + ansible.builtin.package: + name: - python3-dateutil state: present + when: "'sysloghost' in group_names and ansible_distribution_major_version > '7'" notify: - "restart rsyslog" diff --git a/roles/rsyslog/tasks/process_auth_log_for_environment.yml b/roles/rsyslog/tasks/process_auth_log_for_environment.yml new file mode 100644 index 000000000..de97e1af5 --- /dev/null +++ b/roles/rsyslog/tasks/process_auth_log_for_environment.yml @@ -0,0 +1,78 @@ +--- + +- name: Create log_logins table for each log_login environment + community.mysql.mysql_db: + name: "{{ rsyslog_environment.db_loglogins_name }}" + login_user: "{{ rsyslog_environment.db_loglogins_user }}" + login_password: "{{ rsyslog_environment.db_loglogins_password }}" + login_host: "{{ rsyslog_environment.db_loglogins_host }}" + state: import + target: /var/tmp/log_logins.sql + changed_when: false + +- name: Create lastseen table for each log_login environment + community.mysql.mysql_db: + name: "{{ rsyslog_environment.db_lastseen_name }}" + login_user: "{{ rsyslog_environment.db_lastseen_user }}" + login_password: "{{ rsyslog_environment.db_lastseen_password }}" + login_host: "{{ rsyslog_environment.db_lastseen_host }}" + state: import + target: /var/tmp/lastseen.sql + changed_when: false + +- name: Create a python script that parses eb log_logins per environment + ansible.builtin.template: + src: parse_ebauth_to_mysql.py.j2 + dest: /usr/local/sbin/parse_ebauth_to_mysql_{{ rsyslog_environment.name }}.py + mode: 0740 + owner: root + group: root + +- name: Create a python script that parses stepup log_logins per environment + ansible.builtin.template: + src: parse_stepupauth_to_mysql.py.j2 + dest: /usr/local/sbin/parse_stepupauth_to_mysql_{{ rsyslog_environment.name }}.py + mode: 0740 + owner: root + group: root + +- name: Put log_logins logrotate scripts for eb + ansible.builtin.template: + src: logrotate_ebauth.j2 + dest: /etc/logrotate.d/logrotate_ebauth_{{ rsyslog_environment.name }} + mode: 0644 + owner: root + group: root + +- name: Put log_logins logrotate scripts for stepup + ansible.builtin.template: + src: logrotate_stepupauth.j2 + dest: /etc/logrotate.d/logrotate_stepupauth_{{ rsyslog_environment.name }} + mode: 0644 + owner: root + group: root + +- name: Create logdirectory for log_logins cleanup script + ansible.builtin.file: + path: "{{ rsyslog_dir }}/apps/{{ rsyslog_environment.name }}/loglogins_cleanup/" + state: directory + owner: root + group: "{{ rsyslog_read_group }}" + mode: 0750 + +- name: Put log_logins cleanup script + ansible.builtin.template: + src: clean_loglogins.j2 + dest: /usr/local/sbin/clean_loglogins_{{ rsyslog_environment.name }} + owner: root + group: root + mode: 0700 + +- name: Create cronjobs to run the log_logins script + ansible.builtin.cron: + name: Delete old {{ rsyslog_environment.name }} log_login data + user: root + minute: "20" + hour: "02" + job: "/usr/local/sbin/clean_loglogins_{{ rsyslog_environment.name }}" + cron_file: loglogins_cleanup_{{ rsyslog_environment.name }} diff --git a/roles/rsyslog/tasks/process_auth_logs.yml b/roles/rsyslog/tasks/process_auth_logs.yml index 804bf629b..e1cb22365 100644 --- a/roles/rsyslog/tasks/process_auth_logs.yml +++ b/roles/rsyslog/tasks/process_auth_logs.yml @@ -9,103 +9,16 @@ - log_logins.sql - lastseen.sql -- name: Create log_logins table for each log_login environment - community.mysql.mysql_db: - name: "{{ item.db_loglogins_name }}" - login_user: "{{ item.db_loglogins_user }}" - login_password: "{{ item.db_loglogins_password }}" - login_host: "{{ item.db_loglogins_host }}" - state: import - target: /var/tmp/log_logins.sql - changed_when: false - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Create lastseen table for each log_login environment - community.mysql.mysql_db: - name: "{{ item.db_lastseen_name }}" - login_user: "{{ item.db_lastseen_user }}" - login_password: "{{ item.db_lastseen_password }}" - login_host: "{{ item.db_lastseen_host }}" - state: import - target: /var/tmp/lastseen.sql - changed_when: false - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - - name: add python mysql module for parse_ebauth_to_mysql script apt: name: python3-mysqldb state: present when: ansible_os_family == "Debian" -- name: Create a python script that parses eb log_logins per environment - ansible.builtin.template: - src: parse_ebauth_to_mysql.py.j2 - dest: /usr/local/sbin/parse_ebauth_to_mysql_{{ item.name }}.py - mode: 0740 - owner: root - group: root - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Create a python script that parses stepup log_logins per environment - ansible.builtin.template: - src: parse_stepupauth_to_mysql.py.j2 - dest: /usr/local/sbin/parse_stepupauth_to_mysql_{{ item.name }}.py - mode: 0740 - owner: root - group: root - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Put log_logins logrotate scripts for eb - ansible.builtin.template: - src: logrotate_ebauth.j2 - dest: /etc/logrotate.d/logrotate_ebauth_{{ item.name }} - mode: 0644 - owner: root - group: root - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Put log_logins logrotate scripts for stepup - ansible.builtin.template: - src: logrotate_stepupauth.j2 - dest: /etc/logrotate.d/logrotate_stepupauth_{{ item.name }} - mode: 0644 - owner: root - group: root - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Create logdirectory for log_logins cleanup script - ansible.builtin.file: - path: "{{ rsyslog_dir }}/apps/{{ item.name }}/loglogins_cleanup/" - state: directory - owner: root - group: "{{ rsyslog_read_group }}" - mode: 0750 - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Put log_logins cleanup script - ansible.builtin.template: - src: clean_loglogins.j2 - dest: /usr/local/sbin/clean_loglogins_{{ item.name }} - owner: root - group: root - mode: 0700 - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined - -- name: Create cronjobs to run the log_logins script - ansible.builtin.cron: - name: Delete old {{ item.name }} log_login data - user: root - minute: "20" - hour: "02" - job: "/usr/local/sbin/clean_loglogins_{{ item.name }}" - cron_file: loglogins_cleanup_{{ item.name }} - with_items: "{{ rsyslog_environments }}" - when: item.db_loglogins_name is defined +- name: Process auth logs for each rsyslog environment + ansible.builtin.include_tasks: process_auth_log_for_environment.yml + loop: "{{ rsyslog_environments }}" + loop_control: + loop_var: rsyslog_environment + label: "{{ rsyslog_environment.name }}" + when: rsyslog_environment.db_loglogins_name is defined diff --git a/roles/rsyslog/tasks/rsyslog_central.yml b/roles/rsyslog/tasks/rsyslog_central.yml index 7dbdbac1a..1efa858f8 100644 --- a/roles/rsyslog/tasks/rsyslog_central.yml +++ b/roles/rsyslog/tasks/rsyslog_central.yml @@ -1,44 +1,44 @@ --- # The server uses a different set of keys as the client -- name: put rsyslog server key - copy: - content: "{{ rsyslogserverkey }}" +- name: Put rsyslog server key + ansible.builtin.copy: + content: "{{ rsyslogserverkey }}" dest: "/etc/pki/rsyslog/rsyslogserver.key" - mode: 0400 + mode: "0400" owner: root - name: Create the group that is allowed to read the logs - group: + ansible.builtin.group: name: "{{ rsyslog_read_group }}" state: present - name: Create directory to save the logs - file: + ansible.builtin.file: path: "{{ rsyslog_dir }}" owner: root group: "{{ rsyslog_read_group }}" mode: "0750" - recurse: true + # recurse: true # this makes everything very slow and not sure if it is necessary - name: Put rsyslog client certificate - copy: + ansible.builtin.copy: src: "{{ inventory_dir }}/files/certs/rsyslog/rsyslogserver.crt" dest: "/etc/pki/rsyslog/rsyslogserver.crt" - mode: 0644 + mode: "0644" owner: root group: adm -- name: place rsyslog CA file - copy: +- name: Place rsyslog CA file + ansible.builtin.copy: src: "{{ inventory_dir }}/files/certs/rsyslog_ca.pem" - dest: "{{ rsyslog_ca }}" - mode: 0644 + dest: "{{ rsyslog_ca }}" + mode: "0644" - name: Create directories to keep configuration file - file: + ansible.builtin.file: path: "/etc/rsyslog.d/{{ item }}" owner: root - mode: 0755 + mode: "0755" state: directory with_items: - listeners @@ -46,39 +46,46 @@ - templates - name: Create template configurations - template: + ansible.builtin.template: src: sc_template.conf.j2 dest: /etc/rsyslog.d/templates/{{ item.name }}.conf backup: true with_items: "{{ rsyslog_environments }}" + loop_control: + label: "{{ item.name }}" + notify: - "restart rsyslog" - name: Create ruleset configurations - template: + ansible.builtin.template: src: sc_ruleset.conf.j2 dest: /etc/rsyslog.d/rulesets/{{ item.name }}.conf backup: true with_items: "{{ rsyslog_environments }}" + loop_control: + label: "{{ item.name }}" notify: - "restart rsyslog" - name: Create sc listener configurations - template: + ansible.builtin.template: src: listener.conf.j2 dest: /etc/rsyslog.d/listeners/{{ item.name }}.conf backup: true with_items: "{{ rsyslog_environments }}" + loop_control: + label: "{{ item.name }}" notify: - "restart rsyslog" - name: Create logrotate file for apps and host logs - template: + ansible.builtin.template: src: centralsyslog.j2 dest: /etc/logrotate.d/centralsyslog -- name: Put ryslog config file - template: +- name: Put rsyslog config file + ansible.builtin.template: src: "rsyslog.conf.j2" dest: "/etc/rsyslog.conf" notify: diff --git a/roles/rsyslog/templates/clean_loglogins.j2 b/roles/rsyslog/templates/clean_loglogins.j2 index 0ef2ebe56..7296b01ff 100644 --- a/roles/rsyslog/templates/clean_loglogins.j2 +++ b/roles/rsyslog/templates/clean_loglogins.j2 @@ -1,26 +1,126 @@ #!/bin/bash -# Script to clean up the log_logins from mySQL -LOGFILE="{{ rsyslog_dir }}/apps/{{ item.name }}/loglogins_cleanup/loglogins_cleanup.log" -echo `date '+%h %d %H:%M:%S'` Starting cleanup of log_logins | tee -a $LOGFILE -LOGINSTAMP=$(date -d "-{{ loglogins_max_age }} months" +%Y-%m-%d) -OLDESTTIMESTAMP=$(mysql -u {{ item.db_loglogins_user }} -p{{ item.db_loglogins_password }} -h {{ item.db_loglogins_host }} {{ item.db_loglogins_name }} -se "select (DATE_FORMAT(loginstamp,'%Y-%m-%d')) from log_logins order by loginstamp asc limit 1") -if [ -z "$OLDESTTIMESTAMP" ] - then echo "No logins found in log_logins" | tee -a $LOGFILE - exit +# Script to remove logins that are older than LOG_LOGINS_MAX_AGE months from the log_logins table +# in the ${DB_LOG_LOGINS_NAME} database on the ${DB_LOG_LOGINS_HOST} host +# As a failsafe, the script will refuse to run if more than ${MAX_DELETE_DAYS} days worth of logins would be removed, +# unless the --force option is provided. +# The script is intended to be run daily from a cron job. + +MAX_DELETE_DAYS=5 +LOG_LOGINS_MAX_AGE="{{ loglogins_max_age }}" # Number of MONTHS of logins to keep +RSYSLOG_DIR="{{ rsyslog_dir }}" +RSYSLOG_ENVIRONMENT_NAME="{{ rsyslog_environment.name }}" +DB_LOG_LOGINS_NAME="{{ rsyslog_environment.db_loglogins_name }}" +DB_LOG_LOGINS_USER="{{ rsyslog_environment.db_loglogins_user }}" +DB_LOG_LOGINS_PASSWORD="{{ rsyslog_environment.db_loglogins_password }}" +DB_LOG_LOGINS_HOST="{{ rsyslog_environment.db_loglogins_host }}" +NOREPLY_EMAIL="{{ noreply_email }}" +ERROR_MAIL_TO="{{ error_mail_to }}" +ANSIBLE_HOSTNAME="{{ ansible_hostname }}" + +LOG_FILE="${RSYSLOG_DIR}/apps/${RSYSLOG_ENVIRONMENT_NAME}/loglogins_cleanup/loglogins_cleanup.log" +FORCE=0 # Whether to run if more than ${MAX_DELETE_DAYS} days of log data would be removed. + +SCRIPT_NAME=$0 + +for ARG in "$@"; do + case "$ARG" in + --force) + FORCE=1 + ;; + *) + echo "Usage: $0 [--force]" + exit 1 + ;; + esac +done + +log_message() { + echo "$(date '+%h %d %H:%M:%S')" "$@" | tee -a "$LOG_FILE" +} + +send_mail() { + local message="$*" + + log_message "Mailing error to ${ERROR_MAIL_TO}" + + echo "$message" | mail \ + -r "${NOREPLY_EMAIL}" \ + -s "log_login script on ${ANSIBLE_HOSTNAME} needs attention" \ + "${ERROR_MAIL_TO}" +} + + +log_message "Starting cleanup of old log_logins data" + +LOGIN_STAMP=$(date -d "-${LOG_LOGINS_MAX_AGE} months" +%Y-%m-%d) +log_message "Removing all logins older than ${LOG_LOGINS_MAX_AGE} months (before ${LOGIN_STAMP})" + +log_message "Using database: ${DB_LOG_LOGINS_USER}@${DB_LOG_LOGINS_HOST}:${DB_LOG_LOGINS_NAME}" + +MYSQL_DB=( + "mysql" + "-u" "${DB_LOG_LOGINS_USER}" + "-p${DB_LOG_LOGINS_PASSWORD}" + "-h" "${DB_LOG_LOGINS_HOST}" +) + +# Find the oldest timestamp in the log_logins table: +# SELECT (DATE_FORMAT(loginstamp,'%Y-%m-%d')) FROM log_logins ORDER BY loginstamp ASC LIMIT 1 +QUERY=( + "${MYSQL_DB[@]}" + # -s: silent; -e: query + "-se" "SELECT (DATE_FORMAT(loginstamp,'%Y-%m-%d')) FROM log_logins ORDER BY loginstamp ASC LIMIT 1" + "${DB_LOG_LOGINS_NAME}" +) +# DEBUG: Print query +# echo "${QUERY[@]}" +if ! OLDEST_TIMESTAMP=$( "${QUERY[@]}" ); then + log_message "Error running mysql query (OLDEST_TIMESTAMP)" + send_mail "Error running mysql query (OLDEST_TIMESTAMP)" + exit 1 +fi +if [ -z "$OLDEST_TIMESTAMP" ]; then + log_message "No logins found in log_logins. Nothing to cleanup." + send_mail "No logins found in log_logins. Nothing to cleanup." + # The log_logins table is empty. Not treating this as an error. Exit normally, + exit 0 +fi + +# If we are going to delete more than ${MAX_DELETE_DAYS} days, something might be wrong +# Warn and exit if this is the case, unless FORCE==1 +LOGIN_STAMP_UNIX=$(date -d "$LOGIN_STAMP" +%s) +OLDEST_TIMESTAMP_UNIX=$(date -d "$OLDEST_TIMESTAMP" +%s) +TIMESTAMP_DIFF=$(( (LOGIN_STAMP_UNIX-OLDEST_TIMESTAMP_UNIX)/3600/24 )) +log_message "The oldest timestamp in the database is $OLDEST_TIMESTAMP" +if [ "$TIMESTAMP_DIFF" -lt "0" ]; then + log_message "There is no data to delete. We're done." + exit 0 fi -# If we are going to delete more than 5 days, something is wrong -LOGINSTAMPUNIX=$(date -d $LOGINSTAMP +%s) -OLDESTTIMESTAMPUNIX=$(date -d $OLDESTTIMESTAMP +%s) -TIMESTAMPDIFF=$(( ($LOGINSTAMPUNIX-$OLDESTTIMESTAMPUNIX)/3600/24 )) -echo `date '+%h %d %H:%M:%S'` We will delete $TIMESTAMPDIFF days of log_logins data | tee -a $LOGFILE -if [ "$TIMESTAMPDIFF" -gt 5 ] - then - echo `date '+%h %d %H:%M:%S'` Something is up, you need to delete more than 5 days worth of logindata | tee -a $LOGFILE - - echo "The log_login cleanup script wants to delete more than 5 days of logins on the {{ ansible_hostname }}. Please investigate" | mail -r "{{ noreply_email }}" -s "log_login script on {{ ansible_hostname }} needs attention" "{{ error_mail_to }}" - exit -else - DELETEDROWS=$(mysql -u {{ item.db_loglogins_user }} -p{{ item.db_loglogins_password }} -h {{ item.db_loglogins_host }} -sNe "delete from log_logins where loginstamp < '$LOGINSTAMP'; select row_count();" {{ item.db_loglogins_name }}) - echo `date '+%h %d %H:%M:%S'` We have deleted $DELETEDROWS rows. | tee -a $LOGFILE +log_message "We will delete $TIMESTAMP_DIFF days of log_logins data" +if [ "$TIMESTAMP_DIFF" -gt "$MAX_DELETE_DAYS" ] && [ "$FORCE" -ne 1 ]; then + log_message "Something is up, you need to delete more than ${MAX_DELETE_DAYS} days worth of login data" + send_mail "The ${SCRIPT_NAME} script wants to delete more than ${MAX_DELETE_DAYS} days of logins on ${ANSIBLE_HOSTNAME}. Please investigate" + exit 0 +fi +if [ "$TIMESTAMP_DIFF" -gt "$MAX_DELETE_DAYS" ] && [ "$FORCE" -eq 1 ]; then + log_message "Force option provided; deleting more than ${MAX_DELETE_DAYS} days worth of login data" fi + +# Delete rows +# DELETE FROM log_logins WHERE loginstamp < '$LOGIN_STAMP'; SELECT row_count();" +QUERY=( + "${MYSQL_DB[@]}" + # -s: silent; -N: Don't write column names in results; -e: query; + "-sNe" "DELETE FROM log_logins WHERE loginstamp < '${LOGIN_STAMP}'; SELECT row_count();" + "${DB_LOG_LOGINS_NAME}" +) +# DEBUG: Print query +# echo "${QUERY[@]}" +if ! DELETED_ROWS=$( "${QUERY[@]}" ); then + log_message "Error running mysql query (DELETED_ROWS)" + send_mail "Error running mysql query (DELETED_ROWS)" + exit 1 +fi + +log_message "We have deleted $DELETED_ROWS rows. Done." diff --git a/roles/rsyslog/templates/logrotate_ebauth.j2 b/roles/rsyslog/templates/logrotate_ebauth.j2 index f05ab9bed..b8c460157 100644 --- a/roles/rsyslog/templates/logrotate_ebauth.j2 +++ b/roles/rsyslog/templates/logrotate_ebauth.j2 @@ -1,4 +1,4 @@ -{{ rsyslog_dir }}/log_logins/{{ item.name }}/eb-authentication.log +{{ rsyslog_dir }}/log_logins/{{ rsyslog_environment.name }}/eb-authentication.log { missingok daily @@ -10,7 +10,7 @@ delaycompress create 0640 root {{ rsyslog_read_group }} postrotate - /usr/local/sbin/parse_ebauth_to_mysql_{{ item.name }}.py > /dev/null + /usr/local/sbin/parse_ebauth_to_mysql_{{ rsyslog_environment.name }}.py > /dev/null systemctl kill -s HUP rsyslog.service endscript } diff --git a/roles/rsyslog/templates/logrotate_stepupauth.j2 b/roles/rsyslog/templates/logrotate_stepupauth.j2 index be1a50652..aa5bf4ead 100644 --- a/roles/rsyslog/templates/logrotate_stepupauth.j2 +++ b/roles/rsyslog/templates/logrotate_stepupauth.j2 @@ -1,4 +1,4 @@ -{{ rsyslog_dir }}/log_logins/{{ item.name }}/stepup-authentication.log +{{ rsyslog_dir }}/log_logins/{{ rsyslog_environment.name }}/stepup-authentication.log { missingok daily @@ -10,7 +10,7 @@ delaycompress create 0640 root {{ rsyslog_read_group }} postrotate - /usr/local/sbin/parse_stepupauth_to_mysql_{{ item.name }}.py > /dev/null + /usr/local/sbin/parse_stepupauth_to_mysql_{{ rsyslog_environment.name }}.py > /dev/null systemctl kill -s HUP rsyslog.service endscript } diff --git a/roles/rsyslog/templates/parse_ebauth_to_mysql.py.j2 b/roles/rsyslog/templates/parse_ebauth_to_mysql.py.j2 index 7e0bc7bcb..c71a1ba81 100644 --- a/roles/rsyslog/templates/parse_ebauth_to_mysql.py.j2 +++ b/roles/rsyslog/templates/parse_ebauth_to_mysql.py.j2 @@ -1,7 +1,12 @@ #!/usr/bin/python3 -# This script parses the files produced by engineblock and inserts them into a mySQL table where the SURFconext stats module will analyse the data further -# This script is intended to be used during logrotate -# It picks up all files starting with ebauth- (all rotated files) and parses them +# This script parses the authentication log files produced by engineblock and inserts them into two tables: +# - log_logins: contains login events from engineblock and stepup-gateway, and is there for use manually querying logins. +# There is no further processing done on this table. +# A daily cronjob runs clean_loglogins.j2 to remove old logins from the log_logins table. +# - last_login: contains the last login date for each user. This is used for deprovisioning (lifecycle) +# +# This script is intended to be used during logrotate with the delaycompress option set. +# It picks up all files starting with ebauth- that do not end in .gz and parses them import os import sys @@ -10,11 +15,11 @@ import json import MySQLdb from dateutil.parser import parse -mysql_host="{{ item.db_loglogins_host }}" -mysql_user="{{ item.db_loglogins_user }}" -mysql_password="{{ item.db_loglogins_password }}" -mysql_db="{{ item.db_loglogins_name }}" -workdir="{{ rsyslog_dir }}/log_logins/{{ item.name}}/" +mysql_host="{{ rsyslog_environment.db_loglogins_host }}" +mysql_user="{{ rsyslog_environment.db_loglogins_user }}" +mysql_password="{{ rsyslog_environment.db_loglogins_password }}" +mysql_db="{{ rsyslog_environment.db_loglogins_name }}" +workdir="{{ rsyslog_dir }}/log_logins/{{ rsyslog_environment.name}}/" db = MySQLdb.connect(mysql_host,mysql_user,mysql_password,mysql_db ) cursor = db.cursor() diff --git a/roles/rsyslog/templates/parse_stepupauth_to_mysql.py.j2 b/roles/rsyslog/templates/parse_stepupauth_to_mysql.py.j2 index 843fe44bc..8c98f29d2 100644 --- a/roles/rsyslog/templates/parse_stepupauth_to_mysql.py.j2 +++ b/roles/rsyslog/templates/parse_stepupauth_to_mysql.py.j2 @@ -1,8 +1,12 @@ #!/usr/bin/python3 -# This script parses rotated stepup-authentication.log files produced by engineblock. -# It filters for successful logins (authentication_result:OK) and inserts the data -# into the log_logins and last_login MySQL tables. -# This script is intended to be run separately during logrotate. +# This script parses the authentication log files produced by stepup-gateway and inserts them into two tables: +# - log_logins: contains login events from engineblock and stepup-gateway, and is there for use manually querying logins. +# There is no further processing done on this table. +# A daily cronjob runs clean_loglogins.j2 to remove old logins from the log_logins table. +# - last_login: contains the last login date for each user. This is used for deprovisioning (lifecycle) +# +# This script is intended to be used during logrotate with the delaycompress option set. +# It picks up all files starting with stepup-authentication.log- that do not end in .gz and parses them import os import sys @@ -11,11 +15,11 @@ import MySQLdb from dateutil.parser import parse # Configuration variables (to be injected by Ansible/Jinja2) -mysql_host="{{ item.db_loglogins_host }}" -mysql_user="{{ item.db_loglogins_user }}" -mysql_password="{{ item.db_loglogins_password }}" -mysql_db="{{ item.db_loglogins_name }}" -workdir="{{ rsyslog_dir }}/log_logins/{{ item.name}}/" +mysql_host="{{ rsyslog_environment.db_loglogins_host }}" +mysql_user="{{ rsyslog_environment.db_loglogins_user }}" +mysql_password="{{ rsyslog_environment.db_loglogins_password }}" +mysql_db="{{ rsyslog_environment.db_loglogins_name }}" +workdir="{{ rsyslog_dir }}/log_logins/{{ rsyslog_environment.name}}/" # Establish database connection try: diff --git a/roles/rsyslog/templates/sc_ruleset.conf.j2 b/roles/rsyslog/templates/sc_ruleset.conf.j2 index 86a0e5457..34d5392dd 100644 --- a/roles/rsyslog/templates/sc_ruleset.conf.j2 +++ b/roles/rsyslog/templates/sc_ruleset.conf.j2 @@ -19,8 +19,6 @@ if $programname == "engineblock" and $msg contains '{"channel":"authentication"' :programname, isequal, "pdp" { action(type="omfile" DynaFile="pdp-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } if $programname == "profile" and $msg startswith "{" then { action(type="omfile" DynaFile="profile-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } :programname, isequal, "profile" { action(type="omfile" DynaFile="apache-profile-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } -:programname, isequal, "teamsserver" { action(type="omfile" DynaFile="teams-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } -:programname, isequal, "teamsgui" { action(type="omfile" DynaFile="apache-teams-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } :programname, isequal, "vootserver" { action(type="omfile" DynaFile="voot-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } :programname, isequal, "mariadbd" { action(type="omfile" DynaFile="galera-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } :programname, isequal, "garb-systemd" { action(type="omfile" DynaFile="haproxy-{{ item.name }}" {{ rsyslog_dir_file_modes }} ) stop } diff --git a/roles/rsyslog/templates/sc_template.conf.j2 b/roles/rsyslog/templates/sc_template.conf.j2 index d6b765f0a..3a47df0a6 100644 --- a/roles/rsyslog/templates/sc_template.conf.j2 +++ b/roles/rsyslog/templates/sc_template.conf.j2 @@ -14,8 +14,6 @@ $template pdpanalytics-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/ $template apache-pdp-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/pdp/apache.log" $template profile-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/profile/profile.log" $template apache-profile-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/profile/apache.log" -$template teams-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/teams/teams.log" -$template apache-teams-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/teams/apache.log" $template voot-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/voot/voot.log" $template apache-voot-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/voot/apache.log" $template galera-{{ item.name }}, "{{ rsyslog_dir }}/apps/{{ item.name }}/galera/galera.log" diff --git a/roles/sram_ldap/defaults/main.yml b/roles/sram_ldap/defaults/main.yml new file mode 100644 index 000000000..35d5029f3 --- /dev/null +++ b/roles/sram_ldap/defaults/main.yml @@ -0,0 +1,38 @@ +--- +sram_ldap_image: "ghcr.io/surfscz/sram-ldap:main" +sram_ldap_conf_dir: "{{ current_release_appdir }}/sram/ldap" +sram_ldap_ldif_dir: "{{ sram_ldap_conf_dir }}/schema" +sram_ldap_certs_dir: "{{ sram_ldap_conf_dir }}/certs" +sram_ldap_backup_dir: "{{ sram_ldap_conf_dir }}/ldap" +sram_ldap_data_dir: "{{ sram_ldap_conf_dir}}/data" +sram_ldap_uri: "ldap://localhost/" + +sram_ldap_user: "openldap" +sram_ldap_group: "openldap" + +# admin_group: "ldap_admin" +sram_ldap_admins: + - name: Admin + uid: admin + pw_hash: "!" + sshkey: "" + +sram_ldap_loglevel: "stats stats2 filter" + +sram_ldap_services_password: secret +sram_ldap_monitor_password: secret +sram_ldap_ldap_monitor_password: secret + +sram_ldap_uri: "ldap://localhost/" +sram_ldap_rid_prefix: "ldap://" + +sram_ldap_base_domain: "{{ base_domain }}" +sram_ldap_base_dn: >- + {{ ((sram_ldap_base_domain.split('.')|length)*['dc=']) | + zip(sram_ldap_base_domain.split('.')) | list | map('join', '') | list | join(',') }} +sram_ldap_services_dn: + basedn: "dc=services,{{ sram_ldap_base_dn }}" + o: "Services" + binddn: "cn=admin,{{ sram_ldap_base_dn }}" + +sram_ldap_hosts: {} diff --git a/roles/sram_ldap/files/eduMember.ldif b/roles/sram_ldap/files/eduMember.ldif new file mode 100644 index 000000000..42894d596 --- /dev/null +++ b/roles/sram_ldap/files/eduMember.ldif @@ -0,0 +1,27 @@ +dn: cn=eduMember,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: eduMember +# Internet X.500 Schema for Ldappc +# Includes the eduMember ObjectClass schema +# +# +# An auxiliary object class, "eduMember," is a convenient container +# for an extensible set of attributes concerning group memberships. +# At this time, the only attributes specified as belonging to the +# object class are "isMemberOf" and "hasMember." +# +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.5.1.1 + NAME 'isMemberOf' + DESC 'identifiers for groups to which containing entity belongs' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.5.1.2 + NAME 'hasMember' + DESC 'identifiers for entities that are members of the group' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcObjectClasses: ( 1.3.6.1.4.1.5923.1.5.2.1 + NAME 'eduMember' + AUXILIARY + MAY ( isMemberOf $ hasMember ) + ) diff --git a/roles/sram_ldap/files/eduPerson.ldif b/roles/sram_ldap/files/eduPerson.ldif new file mode 100644 index 000000000..e4f2c96a0 --- /dev/null +++ b/roles/sram_ldap/files/eduPerson.ldif @@ -0,0 +1,83 @@ +dn: cn=eduperson,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: eduperson +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.1 + NAME 'eduPersonAffiliation' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.7 + NAME 'eduPersonEntitlement' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.2 + NAME 'eduPersonNickName' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.3 + NAME 'eduPersonOrgDN' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.4 + NAME 'eduPersonOrgUnitDN' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.5 + NAME 'eduPersonPrimaryAffiliation' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.8 + NAME 'eduPersonPrimaryOrgUnitDN' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.6 + NAME 'eduPersonPrincipalName' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.12 + NAME 'eduPersonPrincipalNamePrior' + DESC 'eduPersonPrincipalNamePrior per Internet2' + EQUALITY caseIgnoreMatch + SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.9 + NAME 'eduPersonScopedAffiliation' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.10 + NAME 'eduPersonTargetedID' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.11 + NAME 'eduPersonAssurance' + DESC 'eduPerson per Internet2 and EDUCAUSE' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.13 + NAME 'eduPersonUniqueId' + DESC 'eduPersonUniqueId per Internet2' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.5923.1.1.1.16 + NAME 'eduPersonOrcid' + DESC 'ORCID researcher identifiers belonging to the principal' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcObjectClasses: ( 1.3.6.1.4.1.5923.1.1.2 + NAME 'eduPerson' + AUXILIARY + MAY ( + eduPersonAffiliation $ eduPersonNickname $ eduPersonOrgDN $ + eduPersonOrgUnitDN $ eduPersonPrimaryAffiliation $ + eduPersonPrincipalName $ eduPersonEntitlement $ eduPersonPrimaryOrgUnitDN $ + eduPersonScopedAffiliation $ eduPersonTargetedID $ eduPersonAssurance $ + eduPersonPrincipalNamePrior $ eduPersonUniqueId $ eduPersonOrcid ) + ) diff --git a/roles/sram_ldap/files/groupOfMembers.ldif b/roles/sram_ldap/files/groupOfMembers.ldif new file mode 100644 index 000000000..aa10094d3 --- /dev/null +++ b/roles/sram_ldap/files/groupOfMembers.ldif @@ -0,0 +1,19 @@ +# Internet X.500 Schema for Ldappc +# Includes the groupOfMembers ObjectClass schema +# +# Taken from RFC2307bis draft 2 +# https://tools.ietf.org/html/draft-howard-rfc2307bis-02 +# +# An structural object class, "groupOfMembers" is a convenient container +# for an extensible set of attributes concerning group memberships. +# +dn: cn=groupOfMembers,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: groupOfMembers +olcObjectClasses: ( 1.3.6.1.1.1.2.18 SUP top STRUCTURAL + NAME 'groupOfMembers' + DESC 'A group with members (DNs)' + MUST cn + MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ + description $ member ) + ) diff --git a/roles/sram_ldap/files/ldap-add b/roles/sram_ldap/files/ldap-add new file mode 100644 index 000000000..3d0c5e487 --- /dev/null +++ b/roles/sram_ldap/files/ldap-add @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Copyright (C) 2015-2019 Maciej Delmanowski +# Copyright (C) 2015-2019 DebOps +# SPDX-License-Identifier: GPL-3.0-only + +# Check if specified LDAP schema file is loaded in the local slapd cn=config +# database. If not, try loading it in the server. + + +set -o nounset -o pipefail -o errexit + +schema_file="${1}" + +if [ -z "${schema_file}" ] ; then + printf "Error: You need to specify schema file to load\\n" && exit 1 +fi + +if [ ! -e "${schema_file}" ] ; then + printf "Error: %s does not exist\\n" "${schema_file}" && exit 1 +fi + +if [ ! -r "${schema_file}" ] ; then + printf "Error: %s is unreadable\\n" "${schema_file}" && exit 1 +fi + +# The schema file is already converted, we can deal with them directly +if [[ "${schema_file}" == *.ldif ]] ; then + + # Get the DN of the schema + schema_dn="$(grep -E '^^dn:\s' "${schema_file}")" + + # Get list of already installed schemas from local LDAP server + schema_list() { + ldapsearch -Y EXTERNAL -H ldapi:/// -LLLQ -b 'cn=schema,cn=config' dn \ + | sed -e '/^$/d' -e 's/{[0-9]\+}//' + } + + if schema_list | grep -q "${schema_dn}" ; then + + # Schema is already installed, do nothing + exit 80 + + else + + # Try installing the schema in the database + ldapadd -Y EXTERNAL -H ldapi:/// -f "${schema_file}" + + fi + +fi diff --git a/roles/sram_ldap/files/ldapPublicKey.ldif b/roles/sram_ldap/files/ldapPublicKey.ldif new file mode 100644 index 000000000..8968b6e96 --- /dev/null +++ b/roles/sram_ldap/files/ldapPublicKey.ldif @@ -0,0 +1,21 @@ +dn: cn=openssh-lpk-openldap,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: openssh-lpk-openldap +# +# LDAP Public Key Patch schema for use with openssh-ldappubkey +# useful with PKA-LDAP also +# +# Author: Eric AUGE +# +# Based on the proposal of : Mark Ruijter +# +# octetString SYNTAX +olcAttributeTypes: ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' + DESC 'MANDATORY: OpenSSH Public key' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) +# printableString SYNTAX yes|no +olcObjectClasses: ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY + DESC 'MANDATORY: OpenSSH LPK olcObjectClasses:' + MUST ( sshPublicKey $ uid ) + ) diff --git a/roles/sram_ldap/files/sczGroup.ldif b/roles/sram_ldap/files/sczGroup.ldif new file mode 100644 index 000000000..d1b5cb332 --- /dev/null +++ b/roles/sram_ldap/files/sczGroup.ldif @@ -0,0 +1,23 @@ +# Internet X.500 Schema for Ldappc +# Includes the sczGroup ObjectClass schema +# +# An auxiliary object class, "sczGroup," is a convenient container +# for an extensible set of attributes concerning group memberships. +# At this time, the only attribute specified as belonging to the +# object class is "sczMember." +# +# It is specifically configured to support the memberOf overlay. +# +dn: cn=sczGroup,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: sczGroup +olcAttributeTypes: ( 1.3.6.1.4.1.1076.20.40.50.1.1 + NAME 'sczMember' + DESC 'DN identifiers for entities that are members of the group' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcObjectClasses: ( 1.3.6.1.4.1.1076.20.40.50.1 + NAME 'sczGroup' + AUXILIARY + MAY ( sczMember ) + ) diff --git a/roles/sram_ldap/files/sramPerson.ldif b/roles/sram_ldap/files/sramPerson.ldif new file mode 100644 index 000000000..e194381d1 --- /dev/null +++ b/roles/sram_ldap/files/sramPerson.ldif @@ -0,0 +1,23 @@ +# Internet X.500 Schema for Ldappc +# Includes the sramPerson ObjectClass schema +# +# An auxiliary object class, "sramPerson," is a convenient container +# for an extensible set of attributes concerning sram persons. +# At this time, the only attribute specified as belonging to the +# object class is "sramInactiveDays". +# +dn: cn=sramPerson,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: sramPerson +olcAttributeTypes: ( 1.3.6.1.4.1.1076.20.100.20.2.1 NAME 'sramInactiveDays' + DESC 'Number of days this entity was inactive' + EQUALITY IntegerMatch + ORDERING IntegerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + ) +olcObjectClasses: ( 1.3.6.1.4.1.1076.20.100.20.1.1 NAME 'sramPerson' + AUXILIARY + MAY ( + sramInactiveDays + ) + ) diff --git a/roles/sram_ldap/files/voPerson.ldif b/roles/sram_ldap/files/voPerson.ldif new file mode 100644 index 000000000..bdce11ed8 --- /dev/null +++ b/roles/sram_ldap/files/voPerson.ldif @@ -0,0 +1,44 @@ +dn: cn=voperson,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: voperson +olcAttributeTypes: {0}( 1.3.6.1.4.1.34998.3.3.1.1 NAME 'voPersonApplicationUID + ' DESC 'voPerson Application-Specific User Identifier' EQUALITY caseIgnoreMat + ch SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) +olcAttributeTypes: {1}( 1.3.6.1.4.1.34998.3.3.1.2 NAME 'voPersonAuthorName' DE + SC 'voPerson Author Name' EQUALITY caseIgnoreMatch SYNTAX '1.3.6.1.4.1.1466.1 + 15.121.1.15' ) +olcAttributeTypes: {2}( 1.3.6.1.4.1.34998.3.3.1.3 NAME 'voPersonCertificateDN' + DESC 'voPerson Certificate Distinguished Name' EQUALITY distinguishedNameMat + ch SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' ) +olcAttributeTypes: {3}( 1.3.6.1.4.1.34998.3.3.1.4 NAME 'voPersonCertificateIss + uerDN' DESC 'voPerson Certificate Issuer DN' EQUALITY distinguishedNameMatch + SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' ) +olcAttributeTypes: {4}( 1.3.6.1.4.1.34998.3.3.1.5 NAME 'voPersonExternalID' DE + SC 'voPerson Scoped External Identifier' EQUALITY caseIgnoreMatch SYNTAX '1.3 + .6.1.4.1.1466.115.121.1.15' ) +olcAttributeTypes: {5}( 1.3.6.1.4.1.34998.3.3.1.6 NAME 'voPersonID' DESC 'voPe + rson Unique Identifier' EQUALITY caseIgnoreMatch SYNTAX '1.3.6.1.4.1.1466.115 + .121.1.15' ) +olcAttributeTypes: {6}( 1.3.6.1.4.1.34998.3.3.1.7 NAME 'voPersonPolicyAgreemen + t' DESC 'voPerson Policy Agreement Indicator' EQUALITY caseIgnoreMatch SYNTAX + '1.3.6.1.4.1.1466.115.121.1.15' ) +olcAttributeTypes: {7}( 1.3.6.1.4.1.34998.3.3.1.8 NAME 'voPersonSoRID' DESC 'v + oPerson External Identifier' EQUALITY caseIgnoreMatch SYNTAX '1.3.6.1.4.1.146 + 6.115.121.1.15' ) +olcAttributeTypes: {8}( 1.3.6.1.4.1.34998.3.3.1.9 NAME 'voPersonStatus' DESC ' + voPerson Status' EQUALITY caseIgnoreMatch SYNTAX '1.3.6.1.4.1.1466.115.121.1. + 15' ) +olcAttributeTypes: {9}( 1.3.6.1.4.1.34998.3.3.1.10 NAME 'voPersonAffiliation' + DESC 'voPerson Affiliation Within Local Scope' EQUALITY caseIgnoreMatch SYNTA + X '1.3.6.1.4.1.1466.115.121.1.15' ) +olcAttributeTypes: {10}( 1.3.6.1.4.1.34998.3.3.1.11 NAME 'voPersonExternalAffi + liation' DESC 'voPerson Scoped External Affiliation' EQUALITY caseIgnoreMatch + SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) +olcAttributeTypes: {11}( 1.3.6.1.4.1.34998.3.3.1.12 NAME 'voPersonScopedAffili + ation' DESC 'voPerson Affiliation With Explicit Local Scope' EQUALITY caseIgn + oreMatch SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' ) +olcObjectClasses: {0}( 1.3.6.1.4.1.34998.3.3.1 NAME 'voPerson' AUXILIARY MAY ( + voPersonAffiliation $ voPersonApplicationUID $ voPersonAuthorName $ voPerson + CertificateDN $ voPersonCertificateIssuerDN $ voPersonExternalAffiliation $ v + oPersonExternalID $ voPersonID $ voPersonPolicyAgreement $ voPersonScopedAffi + liation $ voPersonSoRID $ voPersonStatus ) ) diff --git a/roles/sram_ldap/handlers/main.yml b/roles/sram_ldap/handlers/main.yml new file mode 100644 index 000000000..f6136cfeb --- /dev/null +++ b/roles/sram_ldap/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart the ldap container + community.docker.docker_container: + name: "sram-ldap" + restart: true + state: started diff --git a/roles/sram_ldap/tasks/admins.yml b/roles/sram_ldap/tasks/admins.yml new file mode 100644 index 000000000..85ef3a594 --- /dev/null +++ b/roles/sram_ldap/tasks/admins.yml @@ -0,0 +1,62 @@ +--- +- name: determine ldap admins + set_fact: + ldap_admins: "{{ sram_ldap_admins }}" + +# Find existing ldap admins +- name: Initialize admins (Ia) + community.general.ldap_search: + dn: "{{ sram_ldap_services_dn.basedn }}" + scope: "onelevel" + filter: "(objectClass=organizationalRole)" + attrs: + - "cn" + bind_dn: "{{ sram_ldap_services_dn.binddn }}" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{sram_ldap_uri }}" + register: "existing_ldap_admins_result" + +# ansible trips over stuff like this: we need to extract the results from the result +- name: Initialize admins (Ib) + set_fact: + existing_ldap_admins: "{{ existing_ldap_admins_result.results }}" + +# Remove LDAP non-admins +- name: Initialize admins (II) + community.general.ldap_entry: + dn: "cn={{ item.cn }},{{ sram_ldap_services_dn.basedn }}" + state: absent + bind_dn: "{{ sram_ldap_services_dn.binddn }}" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + when: > + item.cn not in ldap_admins | map(attribute='uid') + and item.cn != 'admin' + loop: "{{existing_ldap_admins}}" + +# Insert LDAP admins +- name: Initialize admins (III) + community.general.ldap_entry: + dn: "cn={{ item.uid }},{{ sram_ldap_services_dn.basedn }}" + objectClass: + - simpleSecurityObject + - organizationalRole + attributes: + description: An LDAP administrator + userPassword: "{{ item.pw_hash }}" + bind_dn: "{{ sram_ldap_services_dn.binddn }}" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + loop: "{{ ldap_admins }}" + +# Make sure passwords are updated for existing admins +- name: Initialize admins (IV) + community.general.ldap_attrs: + dn: "cn={{ item.uid }},{{ sram_ldap_services_dn.basedn }}" + attributes: + userPassword: "{{ item.pw_hash }}" + bind_dn: "{{ sram_ldap_services_dn.binddn }}" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + loop: "{{ ldap_admins }}" + diff --git a/roles/sram_ldap/tasks/main.yml b/roles/sram_ldap/tasks/main.yml new file mode 100644 index 000000000..b6da9cb18 --- /dev/null +++ b/roles/sram_ldap/tasks/main.yml @@ -0,0 +1,305 @@ +--- +# playbook to install and configure all components of the LDAP +- name: Install LDAP utils + apt: + state: "present" + name: + - "python3-ldap" # for ansible ldap modules + install_recommends: false + +- name: Ensure that a number of directories exist + file: + path: "{{ item.path }}" + state: "directory" + mode: "{{ item.mode }}" + with_items: + - { path: "{{sram_ldap_ldif_dir}}", mode: "0755" } + - { path: "{{sram_ldap_certs_dir}}", mode: "0755" } + - { path: "{{sram_ldap_data_dir}}", mode: "0777" } + notify: Restart the ldap container + +- name: Copy schemas + copy: + src: "{{ item }}" + dest: "{{ sram_ldap_ldif_dir }}/{{ item }}" + mode: "0644" + with_items: + - sczGroup.ldif + - groupOfMembers.ldif + - eduPerson.ldif + - ldapPublicKey.ldif + - eduMember.ldif + - voPerson.ldif + - sramPerson.ldif + notify: Restart the ldap container + +- name: Copying ldap-add script + copy: + src: "{{ item }}" + dest: "{{ sram_ldap_conf_dir }}/{{ item }}" + mode: "0755" + with_items: + - ldap-add + +- name: Setup ldap hosts + vars: + host: + key: "%s.{{ sram_ldap_base_domain }}" + value: "%s" + etc_hosts: {} + set_fact: + etc_hosts: >- + {{ etc_hosts | + combine({ host.key | format(item.key): host.value | format(item.value) }) }} + with_dict: "{{ sram_ldap_hosts }}" + +- name: "Pull ldap image" + community.docker.docker_image_pull: + name: "{{ sram_ldap_image }}" + register: "ldap_image" + +- name: Create the ldap container + community.docker.docker_container: + name: "sram-ldap" + image: "{{ sram_ldap_image }}" + restart_policy: "always" + state: started + restart: "{{ ldap_image is changed }}" + pull: never + ports: + - 0.0.0.0:389:389 + env: + LDAP_ORGANISATION: "{{ env }}" + LDAP_DOMAIN: "{{ sram_ldap_base_domain }}" + LDAP_ROOTPASS: "{{ sram_ldap_services_password }}" + etc_hosts: "{{ etc_hosts }}" + volumes: + - "{{ sram_ldap_conf_dir }}:/opt/ldap" + networks: + - name: "loadbalancer" + labels: + traefik.enable: "true" + traefik.tcp.routers.ldap.entrypoints: "ldaps" + traefik.tcp.routers.ldap.rule: "HostSNI(`*`)" + traefik.tcp.routers.ldap.tls: "true" + traefik.tcp.services.ldap.loadbalancer.server.port: "389" + healthcheck: + test: + - "CMD" + - "bash" + - "-c" + - "[[ -S /var/run/slapd/ldapi ]]" + register: "ldap_container" + +- name: Wait for LDAP initialization + ansible.builtin.wait_for: + port: 389 + delay: 5 + +- name: Wait for 5 seconds + ansible.builtin.wait_for: + timeout: 5 + when: "ldap_container is changed" + +- name: Ensure the schemas are added to LDAP + ansible.builtin.shell: + cmd: "docker exec sram-ldap /opt/ldap/ldap-add /opt/ldap/schema/{{ item }}" + register: "result" + failed_when: "result.rc not in [0,80]" + changed_when: "result.rc != 80" + become: true + loop: + - "sczGroup.ldif" + - "groupOfMembers.ldif" + - "eduPerson.ldif" + - "ldapPublicKey.ldif" + - "eduMember.ldif" + - "voPerson.ldif" + - "sramPerson.ldif" + +- name: Set indices + community.general.ldap_attrs: + dn: "olcDatabase={1}mdb,cn=config" + attributes: + olcDbIndex: "{{item}}" + state: "present" + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + with_items: + - "entryUUID eq" + - "o eq" + - "dc eq" + - "entryCSN eq" + +- name: Set olcDatabase={-1}frontend olcSizeLimit + community.general.ldap_attrs: + dn: "olcDatabase={-1}frontend,cn=config" + state: "exact" + attributes: + olcSizeLimit: "unlimited" + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +- name: Set config + community.general.ldap_attrs: + dn: "cn=config" + state: "present" + attributes: + olcServerID: "{{ sram_ldap_server_id }}" + olcSizeLimit: "unlimited" + olcLogLevel: "{{ sram_ldap_loglevel }}" + olcAttributeOptions: "time-" + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +- name: Setup Modules + community.general.ldap_attrs: + dn: cn=module{0},cn=config + attributes: + olcModuleLoad: + - syncprov + - dynlist.so + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +- name: Setup Dynlist + community.general.ldap_entry: + dn: olcOverlay=dynlist,olcDatabase={1}mdb,cn=config + objectClass: + - olcOverlayConfig + - olcDynamicList + attributes: + olcDlAttrSet: "voPerson labeledURI member+memberOf@groupOfMembers" + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +- name: Setup Syncprov + community.general.ldap_entry: + dn: olcOverlay=syncprov,olcDatabase={1}mdb,cn=config + objectClass: + - olcOverlayConfig + - olcSyncProvConfig + attributes: + olcSpCheckpoint: 100 10 + olcSpSessionLog: 100 + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +- name: Set ACLs + community.general.ldap_attrs: + dn: "olcDatabase={1}mdb,cn=config" + attributes: + olcAccess: + - >- + to dn.regex="(([^,]+),{{ sram_ldap_services_dn.basedn }})$" + by dn.exact="{{ sram_ldap_services_dn.binddn }}" write + by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth write + by dn.exact,expand="cn=admin,$1" read + by * break + - >- + to * + by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage + by dn.regex="cn=[^,]+,{{ sram_ldap_services_dn.basedn }}" read + {% if env=="vm" %} + by dn.exact=gidNumber=1000+uidNumber=1000,cn=peercred,cn=external,cn=auth manage + {% endif %} + by * break + - >- + to attrs=userPassword + by self write + by anonymous auth + by * break + state: exact + ordered: true + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +# sram_ldap_rids: +# 101: ldaps://ldap1.scz-vm.net/ +# 102: ldaps://ldap2.scz-vm.net/ + +- name: Set rids + vars: + start: 101 + rid: + key: "%d" + value: "{{ sram_ldap_rid_prefix }}%s.{{ sram_ldap_base_domain }}/" + ldap_rids: {} + set_fact: + ldap_rids: >- + {{ ldap_rids | combine({ rid.key | format(start|int): + rid.value | format(item.key) }) }} + start: "{{ start|int + 1 }}" + with_dict: "{{ sram_ldap_hosts | dict2items | sort(attribute='key') }}" + +# Voor toekomstige Claude gebruikers: onderstaande construct levert aan het eind +# een string representatie van de dict op, die niet meer gebruikt kan worden +# in de hieropvolgende 'Setup rids' task... +# - name: Set rids +# set_fact: +# ldap_rids: >- +# {%- set result = {} %} +# {%- for host in (ldap_hosts | dict2items | sort(attribute='key')) %} +# {%- set _ = result.update({(101 + loop.index0)|string: \ +# ldap_rid_prefix ~ host.key ~ '.' ~ base_domain ~ '/'}) %} +# {%- endfor %} +# {{ result }} + +- name: Setup rids + vars: + rid: >- + rid={} + provider="{}" + searchbase="{{ sram_ldap_services_dn.basedn }}" + type=refreshAndPersist + bindmethod=simple + binddn="{{ sram_ldap_services_dn.binddn }}" + credentials={{ sram_ldap_services_password }} + retry="30 +" + timeout=30 + network-timeout=5 + rids: [] + set_fact: + rids: "{{ rids + [ rid.format(item.key, item.value) ] }}" + with_dict: "{{ dict(ldap_rids) }}" + +- name: Setup Syncrepl + community.general.ldap_attrs: + dn: olcDatabase={1}mdb,cn=config + attributes: + olcSyncrepl: "{{ rids }}" + olcMultiProvider: "TRUE" + bind_dn: "cn=admin,cn=config" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + +# We now have Syncrepl in place, so only write to primary +- name: Initialize DIT + community.general.ldap_entry: + dn: "{{ sram_ldap_services_dn.basedn }}" + state: "present" + objectClass: + - "top" + - "dcObject" + - "organization" + attributes: + dc: "{{ sram_ldap_services_dn.basedn | regex_replace('^dc=([^,]+).*', '\\1') }}" + o: "{{ sram_ldap_services_dn.o }}" + bind_dn: "{{ sram_ldap_services_dn.binddn }}" + bind_pw: "{{ sram_ldap_services_password }}" + server_uri: "{{ sram_ldap_uri }}" + when: > + inventory_hostname in groups['sram_ldap_primary'] + +# We now have Syncrepl in place, so only write to primary +- name: Add ldap admins + include_tasks: "admins.yml" + when: > + inventory_hostname in groups['sram_ldap_primary'] diff --git a/roles/sram_ldap/templates/ldap.conf.j2 b/roles/sram_ldap/templates/ldap.conf.j2 new file mode 100644 index 000000000..d7fa7c227 --- /dev/null +++ b/roles/sram_ldap/templates/ldap.conf.j2 @@ -0,0 +1,16 @@ +# +# LDAP Defaults +# + +# See ldap.conf(5) for details +# This file should be world readable but not world writable. + +#BASE dc=example,dc=com +#URI ldap://ldap.example.com ldap://ldap-master.example.com:666 + +#SIZELIMIT 12 +#TIMELIMIT 15 +#DEREF never + +# TLS certificates (needed for GnuTLS) +TLS_CACERT {{ ssl_certs_dir }}/{{ internal_base_domain }}.crt diff --git a/roles/sram_midproxy/defaults/main.yml b/roles/sram_midproxy/defaults/main.yml new file mode 100644 index 000000000..585af3dd6 --- /dev/null +++ b/roles/sram_midproxy/defaults/main.yml @@ -0,0 +1,9 @@ +--- +sram_midproxy_user: midproxy +sram_midproxy_group: midproxy +sram_midproxy_satosa_version: 8 +sram_midproxy_state_encryption_key: 'secret' +sram_midproxy_issuer: 'issuer' +sram_midproxy_client_id: 'client' +sram_midproxy_client_secret: 'secret' +sram_midproxy_sp_metadata: 'eb-metadata.xml' diff --git a/roles/sram_midproxy/files/internal_attributes.yaml b/roles/sram_midproxy/files/internal_attributes.yaml new file mode 100644 index 000000000..eb3dcd66e --- /dev/null +++ b/roles/sram_midproxy/files/internal_attributes.yaml @@ -0,0 +1,22 @@ +attributes: + displayname: + openid: [name] + saml: [displayName] + givenname: + openid: [given_name] + saml: [givenName] + mail: + openid: [email] + saml: [mail] + name: + openid: [name] + saml: [cn] + surname: + openid: [family_name] + saml: [sn, surname] + uid: + openid: [sub] + saml: [uid] + schachomeorganization: + openid: [schac_home_organization] + saml: [schacHomeOrganization] diff --git a/roles/sram_midproxy/files/plugins/attribute-maps/basic.py b/roles/sram_midproxy/files/plugins/attribute-maps/basic.py new file mode 100644 index 000000000..f98466df5 --- /dev/null +++ b/roles/sram_midproxy/files/plugins/attribute-maps/basic.py @@ -0,0 +1,51 @@ +DEF = "urn:mace:dir:attribute-def:" +TERENA = "urn:mace:terena.org:attribute-def:" + +MAP = { + "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", + "fro": { + f"{TERENA}schacHomeOrganization": "schacHomeOrganization", + f"{DEF}cn": "cn", + f"{DEF}displayName": "displayName", + f"{DEF}eduPersonAffiliation": "eduPersonAffiliation", + f"{DEF}eduPersonEntitlement": "eduPersonEntitlement", + f"{DEF}eduPersonPrincipalName": "eduPersonPrincipalName", + f"{DEF}eduPersonScopedAffiliation": "eduPersonScopedAffiliation", + f"{DEF}eduPersonTargetedID": "eduPersonTargetedID", + f"{DEF}eduPersonAssurance": "eduPersonAssurance", + f"{DEF}email": "email", + f"{DEF}emailAddress": "emailAddress", + f"{DEF}givenName": "givenName", + f"{DEF}gn": "gn", + f"{DEF}isMemberOf": "isMemberOf", + f"{DEF}mail": "mail", + f"{DEF}member": "member", + f"{DEF}name": "name", + f"{DEF}sn": "sn", + f"{DEF}surname": "surname", + f"{DEF}uid": "uid", + }, + "to": { + "schacHomeOrganization": f"{TERENA}schacHomeOrganization", + "cn": f"{DEF}cn", + "displayName": f"{DEF}displayName", + "eduPersonAffiliation": f"{DEF}eduPersonAffiliation", + "eduPersonEntitlement": f"{DEF}eduPersonEntitlement", + "eduPersonPrincipalName": f"{DEF}eduPersonPrincipalName", + "eduPersonScopedAffiliation": f"{DEF}eduPersonScopedAffiliation", + "eduPersonTargetedID": f"{DEF}eduPersonTargetedID", + "eduPersonAssurance": f"{DEF}eduPersonAssurance", + "eduPersonOrcid": f"{DEF}eduPersonOrcid", + "email": f"{DEF}email", + "emailAddress": f"{DEF}emailAddress", + "givenName": f"{DEF}givenName", + "gn": f"{DEF}gn", + "isMemberOf": f"{DEF}isMemberOf", + "mail": f"{DEF}mail", + "member": f"{DEF}member", + "name": f"{DEF}name", + "sn": f"{DEF}sn", + "surname": f"{DEF}surname", + "uid": f"{DEF}uid", + }, +} diff --git a/roles/sram_midproxy/files/plugins/backends/openid_backend.yaml b/roles/sram_midproxy/files/plugins/backends/openid_backend.yaml new file mode 100644 index 000000000..cb78fcccd --- /dev/null +++ b/roles/sram_midproxy/files/plugins/backends/openid_backend.yaml @@ -0,0 +1,14 @@ +module: satosa.backends.openid_connect.OpenIDConnectBackend +name: myaccessid +config: + provider_metadata: + issuer: !ENV SATOSA_ISSUER + client: + verify_ssl: yes + auth_req_params: + response_type: code + scope: [openid, profile, email, schac_home_organization] + client_metadata: + client_id: !ENV SATOSA_CLIENT_ID + client_secret: !ENV SATOSA_CLIENT_SECRET + redirect_uris: [/] diff --git a/roles/sram_midproxy/files/plugins/backends/saml2_backend.yaml b/roles/sram_midproxy/files/plugins/backends/saml2_backend.yaml new file mode 100644 index 000000000..ed97d539c --- /dev/null +++ b/roles/sram_midproxy/files/plugins/backends/saml2_backend.yaml @@ -0,0 +1 @@ +--- diff --git a/roles/sram_midproxy/files/plugins/frontends/ping_frontend.yaml b/roles/sram_midproxy/files/plugins/frontends/ping_frontend.yaml new file mode 100644 index 000000000..c09b218b6 --- /dev/null +++ b/roles/sram_midproxy/files/plugins/frontends/ping_frontend.yaml @@ -0,0 +1,3 @@ +module: satosa.frontends.ping.PingFrontend +name: ping +config: null diff --git a/roles/sram_midproxy/files/plugins/frontends/saml2_frontend.yaml b/roles/sram_midproxy/files/plugins/frontends/saml2_frontend.yaml new file mode 100644 index 000000000..1f8029b66 --- /dev/null +++ b/roles/sram_midproxy/files/plugins/frontends/saml2_frontend.yaml @@ -0,0 +1,63 @@ +module: satosa.frontends.saml2.SAMLFrontend +name: idp +config: + #acr_mapping: + # "": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified" + # "https://accounts.google.com": "http://eidas.europa.eu/LoA/low" + + endpoints: + single_sign_on_service: + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': sso/post + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': sso/redirect + + # If configured and not false or empty the common domain cookie _saml_idp will be set + # with or have appended the IdP used for authentication. The default is not to set the + # cookie. If the value is a dictionary with key 'domain' then the domain for the cookie + # will be set to the value for the 'domain' key. If no 'domain' is set then the domain + # from the BASE defined for the proxy will be used. + #common_domain_cookie: + # domain: .example.com + + entityid_endpoint: true + enable_metadata_reload: no + + idp_config: + organization: {display_name: SURF, name: SURF, url: 'https://www.surf.nl/'} + contact_person: + - {contact_type: technical, email_address: 'mailto:sram-beheer@surf.nl', given_name: Technical} + - {contact_type: support, email_address: 'mailto:sram-beheer@surf.nl', given_name: Support} + - {contact_type: other, email_address: 'mailto:sram-beheer@surf.nl', given_name: Security, extension_attributes: {'xmlns:remd': 'http://refeds.org/metadata', 'remd:contactType': 'http://refeds.org/metadata/contactType/security'}} + key_file: frontend.key + cert_file: frontend.crt + metadata: + # remote: + # - url: https://engine.test2.surfconext.nl/authentication/sp/metadata + # cert: null + local: [!ENV SATOSA_SP_METADATA] + entityid: //proxy.xml + accepted_time_diff: 60 + attribute_map_dir: plugins/attribute-maps + service: + idp: + endpoints: + single_sign_on_service: [] + name: Proxy IdP + ui_info: + display_name: + - lang: en + text: "MyAccessID proxy" + description: + - lang: en + text: "MyAccessID proxy" + keywords: + - lang: en + text: ["MyAccessID", "proxy"] + name_id_format: ['urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'] + policy: + default: + fail_on_missing_requested: false + name_form: urn:oasis:names:tc:SAML:2.0:attrname-format:basic + attribute_restrictions: null + lifetime: {minutes: 15} + encrypt_assertion: false + encrypted_advice_attributes: false diff --git a/roles/sram_midproxy/files/plugins/microservices/generate_attributes.yaml b/roles/sram_midproxy/files/plugins/microservices/generate_attributes.yaml new file mode 100644 index 000000000..86ac4e1f1 --- /dev/null +++ b/roles/sram_midproxy/files/plugins/microservices/generate_attributes.yaml @@ -0,0 +1,8 @@ +module: satosa.micro_services.attribute_generation.AddSyntheticAttributes +name: AddSyntheticAttributes +config: + synthetic_attributes: + default: + default: + schachomeorganization: >- + {{ uid.scope }} diff --git a/roles/sram_midproxy/files/plugins/microservices/regex_attributes.yaml b/roles/sram_midproxy/files/plugins/microservices/regex_attributes.yaml new file mode 100644 index 000000000..e820311e7 --- /dev/null +++ b/roles/sram_midproxy/files/plugins/microservices/regex_attributes.yaml @@ -0,0 +1,10 @@ +module: satosa.micro_services.attribute_processor.AttributeProcessor +name: RegexAttributeProcessor +config: + process: + - attribute: uid + processors: + - name: RegexSubProcessor + module: satosa.micro_services.processors.regex_sub_processor + regex_sub_match_pattern: ^(.+)@.+$ + regex_sub_replace_pattern: \1 diff --git a/roles/sram_midproxy/files/proxy_conf.yaml b/roles/sram_midproxy/files/proxy_conf.yaml new file mode 100644 index 000000000..136268e61 --- /dev/null +++ b/roles/sram_midproxy/files/proxy_conf.yaml @@ -0,0 +1,74 @@ +# BASE: https://example.com +BASE: !ENV SATOSA_BASE + +COOKIE_STATE_NAME: "SATOSA_STATE" +CONTEXT_STATE_DELETE: yes +#STATE_ENCRYPTION_KEY: "asdASD123" + +cookies_samesite_compat: + - ["SATOSA_STATE", "SATOSA_STATE_LEGACY"] + +INTERNAL_ATTRIBUTES: "internal_attributes.yaml" + +BACKEND_MODULES: + - "plugins/backends/openid_backend.yaml" + +FRONTEND_MODULES: + - "plugins/frontends/saml2_frontend.yaml" + - "plugins/frontends/ping_frontend.yaml" + +MICRO_SERVICES: + - "plugins/microservices/generate_attributes.yaml" + - "plugins/microservices/regex_attributes.yaml" + +LOGGING: + version: 1 + formatters: + simple: + format: "[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s] %(message)s" + handlers: + stdout: + class: logging.StreamHandler + stream: "ext://sys.stdout" + level: INFO + formatter: simple + syslog: + class: logging.handlers.SysLogHandler + address: "/dev/log" + level: INFO + formatter: simple + debug_file: + class: logging.FileHandler + filename: satosa-debug.log + encoding: utf8 + level: INFO + formatter: simple + error_file: + class: logging.FileHandler + filename: satosa-error.log + encoding: utf8 + level: ERROR + formatter: simple + info_file: + class: logging.handlers.RotatingFileHandler + filename: satosa-info.log + encoding: utf8 + maxBytes: 10485760 # 10MB + backupCount: 20 + level: INFO + formatter: simple + loggers: + satosa: + level: INFO + saml2: + level: INFO + oidcendpoint: + level: INFO + pyop: + level: INFO + oic: + level: INFO + root: + level: INFO + handlers: + - stdout diff --git a/roles/sram_midproxy/tasks/main.yml b/roles/sram_midproxy/tasks/main.yml new file mode 100644 index 000000000..19125220b --- /dev/null +++ b/roles/sram_midproxy/tasks/main.yml @@ -0,0 +1,90 @@ +--- +- name: "Create midproxy group" + group: + name: "{{ sram_midproxy_group }}" + state: "present" + register: "result" + +- name: "Create midproxy user" + user: + name: "{{ sram_midproxy_user }}" + group: "{{ sram_midproxy_group }}" + comment: "User to run Midproxy service" + shell: "/bin/false" + password: "!" + create_home: false + state: "present" + register: "result" + +- name: "Save midproxy user uid" + set_fact: + # --check fails on result.uid if user was never provisioned + midproxy_user_uid: "{{ result.uid | default(1000) }}" + +- name: Create directory to keep configfile + ansible.builtin.file: + dest: "/opt/sram/midproxy" + state: directory + owner: "{{ sram_midproxy_user }}" + group: "{{ sram_midproxy_group }}" + # mkdir: cannot create directory '.': Permission denied + mode: "0777" + +- name: Copy EB SP metadata + ansible.builtin.copy: + src: "{{ inventory_dir }}/files/sram_midproxy/{{ sram_midproxy_sp_metadata }}" + dest: "/opt/sram/midproxy/{{ sram_midproxy_sp_metadata }}" + owner: "{{ sram_midproxy_user }}" + group: "{{ sram_midproxy_group }}" + # mkdir: cannot create directory '.': Permission denied + mode: "0644" + +- name: Copy SATOSA conf files + ansible.builtin.copy: + src: "{{ item }}" + dest: "/opt/sram/midproxy/{{ item }}" + owner: "{{ sram_midproxy_user }}" + group: "{{ sram_midproxy_group }}" + with_items: + - internal_attributes.yaml + - proxy_conf.yaml + - plugins/ + +- name: "Pull satosa image" + community.docker.docker_image_pull: + name: "{{ sram_midproxy_satosa_image }}" + register: "satosa_image" + +- name: Create the SATOSA container + community.docker.docker_container: + name: sram-midproxy + image: "{{ sram_midproxy_satosa_image }}" + pull: never + restart_policy: "always" + state: started + restart: "{{ satosa_image is changed }}" + # 2026-05-21 07:25:44 +0000] [1] [ERROR] Control server error: [Errno 13] Permission denied: '/.gunicorn' + # user: "{{ midproxy_user_uid }}" + networks: + - name: "loadbalancer" + env: + SATOSA_BASE: 'https://midproxy.{{ openconextaccess_base_domain }}' + SATOSA_STATE_ENCRYPTION_KEY: '{{ sram_midproxy_state_encryption_key }}' + SATOSA_SP_METADATA: '{{ sram_midproxy_sp_metadata }}' + SATOSA_ISSUER: '{{ sram_midproxy_issuer }}' + SATOSA_CLIENT_ID: '{{ sram_midproxy_client_id }}' + SATOSA_CLIENT_SECRET: '{{ sram_midproxy_client_secret }}' + volumes: + - /opt/sram/midproxy:/etc/satosa + labels: + traefik.http.routers.midproxy.rule: "Host(`midproxy.{{ openconextaccess_base_domain }}`)" + traefik.http.routers.midproxy.tls: "true" + traefik.enable: "true" + # curl is not availavble in the minimized satosa image + # so this healthcheck won't work + # healthcheck: + # test: ["CMD", "curl", "--fail" , "http://localhost" ] + # interval: 10s + # timeout: 10s + # retries: 3 + # start_period: 10s diff --git a/roles/sram_plsc/defaults/main.yml b/roles/sram_plsc/defaults/main.yml new file mode 100644 index 000000000..6dd2780a1 --- /dev/null +++ b/roles/sram_plsc/defaults/main.yml @@ -0,0 +1,12 @@ +--- +sram_plsc_image: "ghcr.io/surfscz/sram-plsc:main" +sram_plsc_conf_dir: "{{current_release_appdir}}/sram/plsc" +sram_plsc_ansible_nolog: false +sram_plsc_ldap_uri: "ldap://ldap:389/" +sram_plsc_ldap_basedn: "dc=services,dc=vnet" +sram_plsc_ldap_binddn: "cn=admin,dc=vnet" +sram_plsc_ldap_password: "secret" +sram_plsc_sbs_host: "http://sbs-server:8080" +sram_plsc_sbs_user: "sysread" +sram_plsc_sbs_password: "secret" +sram_plsc_retry: 3 diff --git a/roles/sram_plsc/handlers/main.yml b/roles/sram_plsc/handlers/main.yml new file mode 100644 index 000000000..a0dee373a --- /dev/null +++ b/roles/sram_plsc/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart the plsc container + community.docker.docker_container: + name: sram-plsc + restart: true + state: started diff --git a/roles/sram_plsc/tasks/main.yml b/roles/sram_plsc/tasks/main.yml new file mode 100644 index 000000000..365cf0866 --- /dev/null +++ b/roles/sram_plsc/tasks/main.yml @@ -0,0 +1,35 @@ +--- +- name: Make sure clients sync directory exists + file: + path: "{{ sram_plsc_conf_dir }}" + state: directory + mode: "0755" + +- name: "Create plsc.yml source if it doesn't exist" + template: + src: "plsc.yml.j2" + dest: "{{ sram_plsc_conf_dir }}/plsc.yml" + mode: "0640" + no_log: "{{ sram_plsc_ansible_nolog }}" + notify: "Restart the plsc container" + +- name: "Pull plsc image" + community.docker.docker_image_pull: + name: "{{ sram_plsc_image }}" + register: "plsc_image" + +- name: Create the plsc container + community.docker.docker_container: + name: "sram-plsc" + image: "{{ sram_plsc_image }}" + restart_policy: "always" + state: started + restart: "{{ plsc_image is changed }}" + pull: never + mounts: + - type: bind + source: "{{ sram_plsc_conf_dir }}/plsc.yml" + target: "/opt/plsc/plsc.yml" + networks: + # TODO: Should this not be parametrized? + - name: "loadbalancer" diff --git a/roles/sram_plsc/templates/plsc.yml.j2 b/roles/sram_plsc/templates/plsc.yml.j2 new file mode 100644 index 000000000..069f14d8b --- /dev/null +++ b/roles/sram_plsc/templates/plsc.yml.j2 @@ -0,0 +1,25 @@ +--- +ldap: + src: + uri: "{{ sram_plsc_ldap_uri }}" + basedn: "{{ sram_plsc_ldap_basedn }}" + binddn: "{{ sram_plsc_ldap_binddn }}" + passwd: "{{ sram_plsc_ldap_password }}" + sizelimit: 500 + dst: + uri: "{{ sram_plsc_ldap_uri }}" + basedn: "{{ sram_plsc_ldap_basedn }}" + binddn: "{{ sram_plsc_ldap_binddn }}" + passwd: "{{ sram_plsc_ldap_password }}" + sizelimit: 500 +sbs: + src: + host: "{{ sram_plsc_sbs_host }}" + user: "{{ sram_plsc_sbs_user }}" + passwd: "{{ sram_plsc_sbs_password }}" + verify_ssl: {{ false if env=='vm' else true }} + timeout: 60 + retry: {{ sram_plsc_retry }} +pwd: "{CRYPT}!" +uid: 1000 +gid: 1000 diff --git a/roles/sram_redis/defaults/main.yml b/roles/sram_redis/defaults/main.yml new file mode 100644 index 000000000..28022a1af --- /dev/null +++ b/roles/sram_redis/defaults/main.yml @@ -0,0 +1,9 @@ +--- +sram_redis_image: "docker.io/library/redis:7" +sram_redis_conf_dir: "{{ current_release_appdir }}/sram/redis" +sram_redis_data_dir: "{{ current_release_appdir }}/sram/redis/data" +sram_redis_user: redis +sram_redis_group: redis +sram_redis_redis_user: default +sram_redis_redis_password: changethispassword +sram_redis_max_memory: 100mb diff --git a/roles/sram_redis/handlers/main.yml b/roles/sram_redis/handlers/main.yml new file mode 100644 index 000000000..b08f0b62b --- /dev/null +++ b/roles/sram_redis/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart redis container + community.docker.docker_container: + name: sram-redis + state: started + restart: true diff --git a/roles/sram_redis/tasks/main.yml b/roles/sram_redis/tasks/main.yml new file mode 100644 index 000000000..c4c8286c6 --- /dev/null +++ b/roles/sram_redis/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- name: "Create redis group" + group: + name: "{{ sram_redis_group }}" + state: "present" + register: "result" + +- name: "Save redis group gid" + set_fact: + redis_group_gid: "{{ result.gid }}" + +- name: "Create redis user" + user: + name: "{{ sram_redis_user }}" + group: "{{ sram_redis_group }}" + comment: "User to run SRAM Redis service" + shell: "/bin/false" + password: "!" + home: "{{ sram_redis_conf_dir }}" + create_home: false + state: "present" + register: "result" + +- name: "Save redis user uid" + set_fact: + redis_user_uid: "{{ result.uid }}" + +- name: "Create directories" + file: + path: "{{item.path}}" + state: "directory" + owner: "{{ sram_redis_user }}" + group: "{{ sram_redis_group }}" + mode: "{{item.mode}}" + with_items: + - { path: "{{sram_redis_conf_dir}}", mode: "0755" } + - { path: "{{sram_redis_data_dir}}", mode: "0755" } + +- name: "Create redis config" + template: + src: "redis.conf.j2" + dest: "{{ sram_redis_conf_dir }}/redis.conf" + owner: "{{ sram_redis_user }}" + group: "{{ sram_redis_group }}" + mode: "0644" + notify: "Restart redis container" + +- name: "Pull redis image" + community.docker.docker_image_pull: + name: "{{ sram_redis_image }}" + register: "redis_image" + +- name: "Create redis container" + community.docker.docker_container: + name: "sram-redis" + image: "{{ sram_redis_image }}" + restart_policy: "always" + state: "started" + restart: "{{ redis_image is changed }}" + user: "{{ redis_user_uid }}:{{ redis_group_gid }}" + command: | + redis-server /usr/local/etc/redis/redis.conf + volumes: + - "{{ sram_redis_conf_dir }}:/usr/local/etc/redis" + - "{{ sram_redis_data_dir }}:/data" + networks: + # TODO: Should this not be parametrized? + - name: loadbalancer diff --git a/roles/sram_redis/templates/redis.conf.j2 b/roles/sram_redis/templates/redis.conf.j2 new file mode 100644 index 000000000..14d3ef177 --- /dev/null +++ b/roles/sram_redis/templates/redis.conf.j2 @@ -0,0 +1,3 @@ +user {{ sram_redis_redis_user }} on +@all ~* &* >{{ sram_redis_redis_password }} +maxmemory {{ sram_redis_max_memory }} +maxmemory-policy allkeys-lru diff --git a/roles/sram_sbs/defaults/main.yml b/roles/sram_sbs/defaults/main.yml new file mode 100644 index 000000000..64bc443e4 --- /dev/null +++ b/roles/sram_sbs/defaults/main.yml @@ -0,0 +1,164 @@ +--- +sram_sbs_base_domain: "test2.sram.surf.nl" +sram_sbs_ansible_nolog: true +sram_sbs_base_url: "https://{{ sram_sbs_base_domain }}" +sram_sbs_server_image: "ghcr.io/surfscz/sram-sbs-server:main" +sram_sbs_client_image: "ghcr.io/surfscz/sram-sbs-client:main" + +sram_sbs_openidc_timeout: 86400 +sram_sbs_sram_conf_dir: "{{ current_release_appdir }}/sram" + +sram_sbs_work_dir: "{{ sram_sbs_sram_conf_dir }}/sbs" +sram_sbs_git_dir: "{{ sram_sbs_work_dir }}/sbs" +sram_sbs_env_dir: "{{ sram_sbs_work_dir }}/sbs-env" +sram_sbs_conf_dir: "{{ sram_sbs_work_dir }}/config" +sram_sbs_log_dir: "{{ sram_sbs_work_dir }}/log" +sram_sbs_apache_conf: "{{ sram_sbs_work_dir }}/sbs.conf" +sram_sbs_nginx_conf: "{{ sram_sbs_work_dir }}/nginx.conf" + +sram_sbs_db_name: "sbs" +sram_sbs_db_user: "sbsrw" +sram_sbs_migration_user: "sbsmigrate" + +sram_sbs_db_connection: "\ + mysql+mysqldb://%s:%s@{{ mariadb_host }}/{{ sram_sbs_db_name }}\ + ?ssl=true&charset=utf8mb4" +sram_sbs_db_connection_sbs: "{{ sram_sbs_db_connection | format(sram_sbs_db_user, mysql_passwords.sbs) }}" +sram_sbs_db_connection_migration: "\ + {{ sram_sbs_db_connection | format(sram_sbs_migration_user, mysql_passwords.sbsmigrate) }}" + +sram_sbs_db_secret: secret +sram_sbs_secret_key_suffix: suffix +sram_sbs_encryption_key: encryption_key + +sram_sbs_redis_host: sram-redis +sram_sbs_redis_port: 6379 +sram_sbs_redis_ssl: false +sram_sbs_redis_user: default + +sram_sbs_mail_host: "host.docker.internal" +sram_sbs_mail_port: 25 + +sram_sbs_user: "sbs" +sram_sbs_group: "sbs" + +sram_sbs_session_lifetime: 1440 +sram_sbs_secret_key_suffix: "" + +sram_sbs_oidc_crypto_password: "CHANGEME" +sram_sbs_uid_attribute: "sub" + +sram_sbs_disclaimer_color: "#a29c13" +sram_sbs_disclaimer_label: wsgi + +sram_sbs_urn_namespace: "urn:example:sbs" +sram_sbs_eppn_scope: "sbs.example.edu" +sram_sbs_restricted_co_default_org: "example.org" + +sram_sbs_mail_sender_name: "SURF" +sram_sbs_mail_sender_email: "no-reply@localhost" +sram_sbs_exceptions_mail: "root@localhost" + +sram_sbs_support_email: "sram-support@localhost" +sram_sbs_admin_email: "sram-beheer@localhost" +sram_sbs_ticket_email: "sram-support@surf.nl" +sram_sbs_eduteams_email: "eduteams@localhost" + +sram_sbs_suppress_mails: False + +sram_sbs_wiki_link: "https://www.example.org/wiki" + +sram_sbs_cron_hour_of_day: 4 +sram_sbs_seed_allowed: True +sram_sbs_api_keys_enabled: True +sram_sbs_feedback_enabled: True +sram_sbs_audit_trail_notifications_enabled: True +sram_sbs_send_exceptions: False +sram_sbs_send_js_exceptions: False +sram_sbs_second_factor_authentication_required: True +sram_sbs_totp_token_name: "SRAM-example" +sram_sbs_notifications_enabled: True +sram_sbs_invitation_reminders_enabled: True +sram_sbs_invitation_expirations_enabled: True +sram_sbs_open_requests_enabled: True +sram_sbs_scim_sweep: False +sram_sbs_impersonation_allowed: True +sram_sbs_admin_platform_backdoor_totp: True +sram_sbs_past_dates_allowed: True +sram_sbs_mock_scim_enabled: True +sram_sbs_log_to_stdout: True + +sram_sbs_delete_orphaned: True +sram_sbs_suspension_inactive_days: 365 +sram_sbs_suspension_reminder_days: 14 +sram_sbs_suspension_notify_admin: False + +sram_sbs_oidc_config_url: "http://localhost/.well-known/openid-configuration" +sram_sbs_oidc_authz_endpoint: "http://localhost/OIDC/authorization" +sram_sbs_oidc_token_endpoint: "http://localhost/OIDC/token" +sram_sbs_oidc_userinfo_endpoint: "http://localhost/OIDC/userinfo" +sram_sbs_oidc_jwks_endpoint: "http://localhost/OIDC/jwks.json" +sram_sbs_oidc_redirect_uri: "https://{{sram_sbs_base_domain}}/api/users/resume-session" +sram_sbs_oidc_jwt_audience: "https://localhost" +sram_sbs_continue_eduteams_redirect_uri: "https://localhost/continue" +sram_sbs_oidc_verify_peer: False +sram_sbs_oidc_scopes: + - openid + +sram_sbs_mfa_idp_allowed: false +sram_sbs_eduteams_continue_endpoint: "https://localhost/continue" +sram_sbs_eb_continue_endpoint: "https://engine.(.*)surfconext.nl(.*)" + +sram_sbs_manage_base_enabled: False +sram_sbs_manage_base_url: "https://manage.{{base_domain}}" +sram_sbs_manage_sram_rp_entity_id: "sbs.{{sram_sbs_base_domain}}" +sram_sbs_manage_verify_peer: False + +sram_sbs_idp_metadata_url: "https://metadata.surfconext.nl/signed/2023/edugain-downstream-idp.xml " +# backup_dir: "{{backup_base}}/sbs" + +sram_sbs_swagger_enabled: true + +sram_sbs_ssid_identity_providers: [] +sram_sbs_surf_secure_id: + environment: "unknown.example.org" + sp_entity_id: "https://sbs.{{sram_sbs_base_domain}}" + acs_url: "https://{{sram_sbs_base_domain}}/api/users/acs" + sa_gw_environment: "sa-gw.unknown.example.org" + sa_idp_certificate: | + -----BEGIN CERTIFICATE----- + 12345 + -----END CERTIFICATE----- + priv: | + -----BEGIN RSA PRIVATE KEY----- + abcde + -----END RSA PRIVATE KEY----- + pub: | + -----BEGIN CERTIFICATE----- + 12345 + -----END CERTIFICATE----- + +sram_sbs_ssid_authncontext: "\ + http://{{ sram_sbs_surf_secure_id.environment }}/assurance/sfo-level2" +sram_sbs_ssid_entityid: "\ + https://{{ sram_sbs_surf_secure_id.sa_gw_environment }}/second-factor-only/metadata" +sram_sbs_ssid_sso_endpoint: "\ + https://{{ sram_sbs_surf_secure_id.sa_gw_environment }}/second-factor-only/single-sign-on" + +sram_sbs_mfa_sso_minutes: 10 +sram_sbs_mfa_fallback_enabled: true + +sram_sbs_ldap_url: "ldap://ldap.example.com/dc=example,dc=com" +sram_sbs_ldap_bind_account: "cn=admin,dc=entity_id,dc=services,dc=sram-tst,dc=surf,dc=nl" + +sram_sbs_csp_style_hashes: + - 'sha256-0+ANsgYUJdh56RK8gGvTF2vnriYqvFHfWqtA8xXa+bA=' + - 'sha256-3SnfHQolDHbZMbDAPmhrZf1keHiXfj/KJyh2phhFAAY=' + - 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' + - 'sha256-Ng6y+QCkPChG4Q49SIfXB5ToIDcDhITtQNFkDBPpCTw=' + - 'sha256-orBPipbqpMvkNi+Z+m6qEn0XS6ymmAQE6+FwCNs1FbQ=' + - 'sha256-vFt3L2qLqpJmRpcXGbYr2UVSmgSp9VCUzz2lnqWIATw=' + - 'sha256-SU3XCwbQ/8qgzoGOWCYdkwIr3xRrl5rsvdFcpw8NSiE=' # on /new-service-request + - 'sha256-WTC9gHKjIpzl5ub1eg/YrRy/k+jlzeyRojah9dxAApc=' # on /new-service-request + +sram_sbs_engine_block_api_token: secret diff --git a/roles/sram_sbs/files/yarn.gpg b/roles/sram_sbs/files/yarn.gpg new file mode 100644 index 000000000..3e9e7d155 --- /dev/null +++ b/roles/sram_sbs/files/yarn.gpg @@ -0,0 +1,243 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +=oMDY +-----END PGP PUBLIC KEY BLOCK----- diff --git a/roles/sram_sbs/handlers/main.yml b/roles/sram_sbs/handlers/main.yml new file mode 100644 index 000000000..bc8be505b --- /dev/null +++ b/roles/sram_sbs/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Restart sbs containers + community.docker.docker_container: + name: "{{ item }}" + state: started + restart: true + loop: + - sram-sbs-client + - sram-sbs-server diff --git a/roles/sram_sbs/tasks/main.yml b/roles/sram_sbs/tasks/main.yml new file mode 100644 index 000000000..9c0d93d80 --- /dev/null +++ b/roles/sram_sbs/tasks/main.yml @@ -0,0 +1,153 @@ +--- +- name: "Create SBS group" + group: + name: "{{ sram_sbs_group }}" + state: "present" + register: "result" + +- name: "Save SBS group gid" + set_fact: + sbs_group_gid: "{{ result.gid }}" + +- name: "Create SBS user" + user: + name: "{{ sram_sbs_user }}" + group: "{{ sram_sbs_group }}" + comment: "User to run SBS service" + shell: "/bin/false" + password: "!" + home: "{{ sram_sbs_conf_dir }}" + create_home: false + state: "present" + register: "result" + +- name: "Save sbs user uid" + set_fact: + sbs_user_uid: "{{ result.uid }}" + +- name: "Create directories" + file: + path: "{{item.path}}" + state: "directory" + owner: "{{sbs_user_uid}}" + group: "{{sbs_group_gid}}" + mode: "{{item.mode}}" + with_items: + - { path: "{{sram_sbs_work_dir}}", mode: "0755" } + - { path: "{{sram_sbs_conf_dir}}", mode: "0755" } + - { path: "{{sram_sbs_conf_dir}}/saml", mode: "0755" } + - { path: "{{sram_sbs_log_dir}}", mode: "0775" } + +- name: "Fix file permissions" + file: + path: "{{sram_sbs_log_dir}}/{{item}}" + owner: "{{sbs_user_uid}}" + group: "{{sbs_group_gid}}" + mode: "0664" + state: "touch" + modification_time: "preserve" + access_time: "preserve" + with_items: + - "sbs.log" + - "sbs.debug.log" + +- name: "Create SBS config files" + template: + src: "{{item.name}}.j2" + dest: "{{ sram_sbs_conf_dir }}/{{item.name}}" + owner: "{{sbs_user_uid}}" + group: "{{sbs_group_gid}}" + mode: "{{item.mode}}" + with_items: + - { name: "config.yml", mode: "0644" } + - { name: "alembic.ini", mode: "0644" } + - { name: "disclaimer.css", mode: "0644" } + - { name: "sbs-apache.conf", mode: "0644" } + no_log: "{{ sram_sbs_ansible_nolog }}" + notify: "Restart sbs containers" + +- name: "Pull sbs image" + community.docker.docker_image_pull: + name: "{{ item }}" + with_items: + - "{{ sram_sbs_client_image }}" + - "{{ sram_sbs_server_image }}" + register: "sbs_images" + +- name: "Migration" + # For some reason --check breaks this block + when: "sbs_images is changed and not ansible_check_mode" + block: + - name: "Run SBS migrations" + throttle: 1 + community.docker.docker_container: + name: "sram-sbs-migration" + image: "{{ sram_sbs_server_image }}" + pull: "never" + state: "started" + restart_policy: "no" + detach: false + env: + RUNAS_UID: "{{ sbs_user_uid | string }}" + RUNAS_GID: "{{ sbs_group_gid | string }}" + MIGRATIONS_ONLY: "1" + # don't actually run the server + command: "/bin/true" + volumes: + - "{{ sram_sbs_conf_dir }}:/sbs-config" + - "{{ sram_sbs_log_dir }}:/opt/sbs/log" + networks: + # TODO: Should we parametrize this? + - name: "loadbalancer" + register: "result" + failed_when: "'container' not in result or result.container.State.ExitCode != 0" + changed_when: "'[alembic.runtime.migration] Running upgrade' in result.container.Output | default('')" + notify: "Restart sbs containers" + + # Remove the migration container; we can't do that with auto_remove, + # because if we use that, ansible will not save the output in result + - name: "Remove migration container" + community.docker.docker_container: + name: "sram-sbs-migration" + state: "absent" + +- name: "Start sbs client container" + community.docker.docker_container: + name: "sram-sbs-client" + image: "{{ sram_sbs_client_image }}" + pull: "never" + restart_policy: "always" + state: "started" + restart: "{{ sbs_images is changed}}" + volumes: + - "{{ sram_sbs_conf_dir }}/sbs-apache.conf:/etc/apache2/sites-enabled/sbs.conf:ro" + - "{{ sram_sbs_conf_dir }}/disclaimer.css:/opt/sbs/client/dist/disclaimer.css:ro" + networks: + - name: "loadbalancer" + labels: + traefik.http.routers.sbsclient.rule: "Host(`{{ sram_sbs_base_domain }}`)" + traefik.http.routers.sbsclient.tls: "true" + traefik.enable: "true" + +- name: "Start SBS server container" + community.docker.docker_container: + name: "sram-sbs-server" + image: "{{ sram_sbs_server_image }}" + restart_policy: "always" + state: "started" + restart: "{{ sbs_images is changed}}" + env: + RUNAS_UID: "{{ sbs_user_uid | string }}" + RUNAS_GID: "{{ sbs_group_gid | string }}" + CONFIG: "/opt/sbs/server/config/config.yml" + REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt" + RUN_MIGRATIONS: "0" + pull: "never" + volumes: + - "{{ sram_sbs_conf_dir }}:/sbs-config" + - "{{ sram_sbs_log_dir }}:/opt/sbs/log" + - "/tmp/ci-runner:/tmp/ci-runner" + networks: + - name: "loadbalancer" + etc_hosts: + host.docker.internal: host-gateway diff --git a/roles/sram_sbs/templates/alembic.ini.j2 b/roles/sram_sbs/templates/alembic.ini.j2 new file mode 100644 index 000000000..e6049eebb --- /dev/null +++ b/roles/sram_sbs/templates/alembic.ini.j2 @@ -0,0 +1,72 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = {{ sram_sbs_db_connection_migration }} + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = NOTSET +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = DEBUG +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/roles/sram_sbs/templates/config.yml.j2 b/roles/sram_sbs/templates/config.yml.j2 new file mode 100644 index 000000000..28dcfe3b4 --- /dev/null +++ b/roles/sram_sbs/templates/config.yml.j2 @@ -0,0 +1,256 @@ +--- +database: + uri: {{ sram_sbs_db_connection_sbs }} + +redis: + uri: "redis{% if sram_sbs_redis_ssl %}s{% endif %}://{{ sram_sbs_redis_user }}:{{ sram_sbs_redis_password }}@{{ sram_sbs_redis_host }}:{{ sram_sbs_redis_port }}/" + +# add a per-release suffix here to invalidate sessions on new releases +secret_key: {{ sram_sbs_db_secret }}{{ sram_sbs_secret_key_suffix }} +# Must be a base64 encoded key of 128, 192, or 256 bits. +# Generate: base64.b64encode(os.urandom(256 // 8)).decode() +encryption_key: {{ sram_sbs_encryption_key }} + +# Lifetime of session in minutes (one day is 60 * 24) +permanent_session_lifetime: {{ sram_sbs_session_lifetime }} + +logging: + log_to_stdout: {{ sram_sbs_log_to_stdout }} + +# Valid scopes are "READ" and "WRITE" +api_users: +{% for name, user in sram_sbs_api_users.items() %} + - name: "{{ name }}" + password: "{{ user.password }}" + scopes: "[ {{ user.scopes | join(', ') }} ]" +{% endfor %} + +oidc: + client_id: "{{ sram_sbs_oidc_client_id }}" + client_secret: "{{ sram_sbs_oidc_client_secret }}" + audience: "{{ sram_sbs_oidc_jwt_audience }}" + verify_peer: {{ sram_sbs_oidc_verify_peer }} + authorization_endpoint: "{{ sram_sbs_oidc_authz_endpoint}}" + token_endpoint: "{{ sram_sbs_oidc_token_endpoint }}" + userinfo_endpoint: "{{ sram_sbs_oidc_userinfo_endpoint }}" + jwks_endpoint: "{{ sram_sbs_oidc_jwks_endpoint }}" + #Note that the paths for these uri's is hardcoded and only domain and port differ per environment + redirect_uri: "{{ sram_sbs_oidc_redirect_uri }}" + continue_eduteams_redirect_uri: "{{ sram_sbs_eduteams_continue_endpoint }}" + continue_eb_redirect_uri: "{{ sram_sbs_eb_continue_endpoint }}" + second_factor_authentication_required: {{ sram_sbs_second_factor_authentication_required }} + totp_token_name: "{{ sram_sbs_totp_token_name }}" + # The service_id in the proxy_authz endpoint when logging into SBS. Most likely to equal the oidc.client_id + sram_service_entity_id: "{{ sram_sbs_oidc_client_id }}" + scopes: {{ sram_sbs_oidc_scopes }} + +base_scope: "{{ base_domain }}" +entitlement_group_namespace: "{{ sram_sbs_urn_namespace }}" +eppn_scope: " {{ sram_sbs_eppn_scope }}" +scim_schema_sram: "urn:mace:surf.nl:sram:scim:extension" +collaboration_creation_allowed_entitlement: "urn:mace:surf.nl:sram:allow-create-co" + +environment_disclaimer: "{{ sram_sbs_disclaimer_label }}" + +# All services in the white list can be requested in the create-restricted-co API +# The default organisation is a fallback for when the administrator has no schac_home_org +restricted_co: + services_white_list: [ "https://cloud" ] + default_organisation: "{{ sram_sbs_restricted_co_default_org }}" + +mail: + host: {{ sram_sbs_mail_host }} + port: {{ sram_sbs_mail_port }} + sender_name: {{ sram_sbs_mail_sender_name }} + sender_email: {{ sram_sbs_mail_sender_email }} + suppress_sending_mails: {{ sram_sbs_suppress_mails }} + info_email: {{ sram_sbs_support_email }} + beheer_email: {{ sram_sbs_admin_email }} + ticket_email: {{ sram_sbs_ticket_email }} + eduteams_email: {{ sram_sbs_eduteams_email }} + # Do we mail a summary of new Organizations and Services to the beheer_email? + audit_trail_notifications_enabled: {{ sram_sbs_audit_trail_notifications_enabled }} + account_deletion_notifications_enabled: True + send_exceptions: {{ sram_sbs_send_exceptions }} + send_js_exceptions: {{ sram_sbs_send_js_exceptions }} + send_exceptions_recipients: [ "{{ sram_sbs_exceptions_mail }}" ] + environment: "{{ base_domain }}" + +manage: + enabled: {{ sram_sbs_manage_base_enabled }} + # The entity_id of the SRAM RP in Manage for API retrieval, e.g "sbs.test2.sram.surf.nl" + sram_rp_entity_id: "{{ sram_sbs_manage_sram_rp_entity_id }}" + base_url: "{{ sram_sbs_manage_base_url }}" + user: "{{ sram_sbs_manage_user }}" + password: "{{ sram_sbs_manage_password }}" + verify_peer: {{ sram_sbs_manage_verify_peer }} + +aup: + version: 1 + url_aup_en: "https://edu.nl/6wb63" + url_aup_nl: "https://edu.nl/6wb63" + +base_url: {{ sram_sbs_base_url }} +socket_url: {{ sram_sbs_base_url }} +base_server_url: {{ sram_sbs_base_url }} +wiki_link: {{ sram_sbs_wiki_link }} + +admin_users: +{% for admin_user in sram_sbs_admin_users %} + - uid: "{{ admin_user.uid }}" +{% endfor %} + +organisation_categories: + - "HBO" + - "MBO" + - "UMC" + - "University" + - "Research" + - "SURF" + +feature: + seed_allowed: {{ sram_sbs_seed_allowed }} + api_keys_enabled: {{ sram_sbs_api_keys_enabled }} + feedback_enabled: {{ sram_sbs_feedback_enabled }} + impersonation_allowed: {{ sram_sbs_impersonation_allowed }} + sbs_swagger_enabled: {{ sram_sbs_swagger_enabled }} + admin_platform_backdoor_totp: {{ sram_sbs_admin_platform_backdoor_totp }} + past_dates_allowed: {{ sram_sbs_past_dates_allowed }} + mock_scim_enabled: {{ sram_sbs_mock_scim_enabled }} + +metadata: + idp_url: "{{ sram_sbs_idp_metadata_url }}" + parse_at_startup: True + # No need for environment specific values + scope_override: + knaw.nl: "Koninklijke Nederlandse Akademie van Wetenschappen (KNAW)" + +platform_admin_notifications: + # Do we daily check for CO join_requests and CO requests and send a summary mail to beheer_email? + enabled: False + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How long before we include open join_requests in the summary + outstanding_join_request_days_threshold: 7 + # How long before we include open CO requests in the summary + outstanding_coll_request_days_threshold: 7 + +user_requests_retention: + # Do we daily check for CO join_requests and CO requests and delete approved and denied? + enabled: {{ sram_sbs_notifications_enabled }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How long before we delete approved / denied join_requests + outstanding_join_request_days_threshold: 90 + # How long before we delete approved / denied CO requests + outstanding_coll_request_days_threshold: 90 + +# The retention config determines how long users may be inactive, how long the reminder email is valid and when do we resent the magic link +retention: + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # how many days of inactivity before a user is suspended + # 0 allows for any last_login_date in the past to trigger suspension notification + allowed_inactive_period_days: {{ sram_sbs_suspension_inactive_days }} + # how many days before suspension do we send a warning + # -1 will suspend notified users on second suspension cron + reminder_suspend_period_days: {{ sram_sbs_suspension_reminder_days }} + # how many days after suspension do we delete the account + remove_suspended_users_period_days: 90 + # how many days before deletion do we send a reminder + reminder_expiry_period_days: 7 + # whether to send a notification of the result of the retention process to the beheer_email + admin_notification_mail: {{ sram_sbs_suspension_notify_admin }} + +collaboration_expiration: + # Do we daily check for CO's that will be deleted because they have been expired? + enabled: {{ sram_sbs_notifications_enabled }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How long after expiration do we actually delete expired collaborations + expired_collaborations_days_threshold: 90 + # How many days before actual expiration do we mail the organisation members + expired_warning_mail_days_threshold: 10 + +collaboration_suspension: + # Do we daily check for CO's that will be suspended because of inactivity? + enabled: {{ sram_sbs_notifications_enabled }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # After how many days of inactivity do we suspend collaborations + collaboration_inactivity_days_threshold: 365 + # How many days before actual suspension do we mail the organisation members + inactivity_warning_mail_days_threshold: 10 + # After how many days after suspension do we actually delete the collaboration + collaboration_deletion_days_threshold: 90 + +membership_expiration: + # Do we daily check for memberships that will be deleted because they have been expired? + enabled: {{ sram_sbs_notifications_enabled }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How long after expiration do we actually delete expired memberships + expired_memberships_days_threshold: 90 + # How many days before actual expiration do we mail the co admin and member + expired_warning_mail_days_threshold: 10 + +invitation_reminders: + # Do we daily check for invitations that need a reminder? + enabled: {{ sram_sbs_invitation_reminders_enabled }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How many days before expiration of an invitation do we remind the user? + invitation_reminders_threshold: 5 + +invitation_expirations: + # Do we daily check for invitations that are expired / accepted and are eligible for deletion ? + enabled: {{ sram_sbs_invitation_expirations_enabled }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How long after expiration of an invitation do we delete the invitation? + nbr_days_remove_expired_invitations: 10 + # How long after expiration of an API created invitation do we delete the invitation? + nbr_days_remove_api_expired_invitations: 30 + +orphan_users: + # Do we daily check for users that are orphans soo they can be deleted? + enabled: {{ sram_sbs_delete_orphaned }} + cron_hour_of_day: {{ sram_sbs_cron_hour_of_day }} + # How long after created do we delete orphan users + delete_days_threshold: 14 + +open_requests: + # Do we weekly check for all open requests? + enabled: {{ sram_sbs_open_requests_enabled }} + cron_day_of_week: 1 + +scim_sweep: + # Do we enable scim sweeps? + enabled: {{ sram_sbs_scim_sweep }} + # How often do we check if scim sweeps are needed per service + cron_minutes_expression: "*/15" + +ldap: + url: "{{ sram_sbs_ldap_url }}" + bind_account: "{{ sram_sbs_ldap_bind_account }}" + +# A MFA login in a different flow is valid for X minutes +mfa_sso_time_in_minutes: {{ sram_sbs_mfa_sso_minutes }} + +# whether to fall back to TOTP MFA +mfa_fallback_enabled: {{ sram_sbs_mfa_fallback_enabled }} + +# Lower case entity ID's and schac_home allowed skipping MFA. +# Note that for a login directly into SRAM only schac_home can be used as the entity_idp of the IdP is unknown +mfa_idp_allowed: {{ sram_sbs_mfa_idp_allowed }} + +# Lower case schachome organisations / entity ID's where SURFSecure ID is used for step-up +ssid_identity_providers: {{ sram_sbs_ssid_identity_providers }} + +ssid_config_folder: saml + +pam_web_sso: + session_timeout_seconds: 300 + +rate_limit_totp_guesses_per_30_seconds: 10 + +# The uid's of user that will never be suspended or deleted +excluded_user_accounts: +{% for excluded_user in sram_sbs_excluded_users %} + - uid: "{{ excluded_user.uid }}" +{% endfor %} + +engine_block: + api_token: {{ sram_sbs_engine_block_api_token }} diff --git a/roles/sram_sbs/templates/disclaimer.css.j2 b/roles/sram_sbs/templates/disclaimer.css.j2 new file mode 100644 index 000000000..ed34bdc09 --- /dev/null +++ b/roles/sram_sbs/templates/disclaimer.css.j2 @@ -0,0 +1,6 @@ +{% if sram_sbs_disclaimer_label!="" -%} +body::after { + background: {{ sram_sbs_disclaimer_color }}; + content: "{{ sram_sbs_disclaimer_label }}"; +} +{% endif %} diff --git a/roles/sram_sbs/templates/saml_advanced_settings.json.j2 b/roles/sram_sbs/templates/saml_advanced_settings.json.j2 new file mode 100644 index 000000000..0d03c63d7 --- /dev/null +++ b/roles/sram_sbs/templates/saml_advanced_settings.json.j2 @@ -0,0 +1,35 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": true, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": true, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAttributeStatement": false, + "wantAssertionsEncrypted": false, + "requestedAuthnContext": ["{{ sram_sbs_ssid_authncontext }}"], + "requestedAuthnContextComparison": "minimum", + "failOnAuthnContextMismatch": false, + "allowSingleLabelDomains": false, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true + }, + "contactPerson": { + "technical": { + "givenName": "{{ mail.admin_name }}", + "emailAddress": "{{ mail.admin_address }}" + } + }, + "organization": { + "en-US": { + "name": "{{ org.name }}", + "displayname": "{{ org.name }}", + "url": "{{ org.url }}" + } + } +} diff --git a/roles/sram_sbs/templates/saml_settings.json.j2 b/roles/sram_sbs/templates/saml_settings.json.j2 new file mode 100644 index 000000000..073651110 --- /dev/null +++ b/roles/sram_sbs/templates/saml_settings.json.j2 @@ -0,0 +1,22 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "{{ sram_sbs_surf_secure_id.sp_entity_id }}", + "assertionConsumerService": { + "url": "{{ sram_sbs_surf_secure_id.acs_url }}", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "x509cert": "{{ sram_sbs_surf_secure_id.pub | barepem }}", + "privateKey": "{{ sram_sbs_surf_secure_id.priv | barepem }}" + }, + "idp": { + "entityId": "{{ sram_sbs_ssid_entityid }}", + "singleSignOnService": { + "url": "{{ sram_sbs_ssid_sso_endpoint }}", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "{{ sram_sbs_surf_secure_id.sa_idp_certificate | barepem }}" + } +} diff --git a/roles/sram_sbs/templates/sbs-apache.conf.j2 b/roles/sram_sbs/templates/sbs-apache.conf.j2 new file mode 100644 index 000000000..1d19099eb --- /dev/null +++ b/roles/sram_sbs/templates/sbs-apache.conf.j2 @@ -0,0 +1,33 @@ +ServerName {{ sram_sbs_base_domain }} +#ErrorLog /proc/self/fd/2 +#CustomLog /proc/self/fd/1 common +DocumentRoot /opt/sbs/client/dist + +Header set Content-Security-Policy "{{ httpd_csp.sram }}" +Header set Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(self), gamepad=(), speaker-selection=()" + +RewriteEngine On +RewriteCond %{REQUEST_URI} !^/(api|pam-weblogin|flasgger_static|swagger|health|config|info|socket.io) +RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f +RewriteRule ^/(.*)$ /index.html [L] + +ProxyRequests off +ProxyPassMatch ^/(api|pam-weblogin|flasgger_static|swagger|health|config|info) http://sram-sbs-server:8080/ +ProxyPassReverse / http://sram-sbs-server:8080/ +ProxyPass /socket.io/ ws://sram-sbs-server:8080/socket.io/ +ProxyPassReverse /socket.io/ ws://sram-sbs-server:8080/socket.io/ + + + Header set Cache-Control "no-cache, private" + + + Header set Cache-Control "public, max-age=31536000, immutable" + + + Header set Cache-Control "no-cache, private" + + + + Require all granted + Options -Indexes + diff --git a/roles/teams/defaults/main.yml b/roles/teams/defaults/main.yml deleted file mode 100644 index c0e6deeb3..000000000 --- a/roles/teams/defaults/main.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -teams_dir: /opt/teams -teams_cronjobmaster: true -teams_help_link_en: https://example.org -teams_help_link_nl: https://example.org -teams_help_link_pt: https://example.org -teams_tos_en: https://example.org -teams_tos_nl: https://example.org -teams_tos_pt: https://example.org -teams_main_link: https://www.openconext.org -teams_organization: "{{ instance_name }}" -teams_api_lifecycle_username: teams_api_lifecycle_user -teams_oauth2_token_url: "https://connect.{{ base_domain }}/oidc/token" -teams_authz_client_id: "teams.{{ base_domain }}" -teams_manage_provision_oidcrp_name_en: "Teams client credentials client for VOOT access" -teams_manage_provision_oidcrp_description_en: "OAuth client to access VOOT for group information" -teams_manage_provision_oidcrp_grants: "client_credentials" -teams_manage_provision_oidcrp_state: "prodaccepted" -teams_manage_provision_oidcrp_scopes: "groups" -teams_manage_provision_oidcrp_allowed_resource_servers: '{"name": "{{ voot.oidcng_checkToken_clientId }}"}' -teams_manage_provision_samlsp_client_id: "https://teams.{{ base_domain }}/shibboleth" -teams_manage_provision_samlsp_name_en: "{{ instance_name }} Teams" -teams_manage_provision_samlsp_description_en: "{{ instance_name }} Teams application for group memberships" -teams_manage_provision_samlsp_acs_location: "https://teams.{{ base_domain }}/Shibboleth.sso/SAML2/POST" -teams_manage_provision_samlsp_metadata_url: "https://teams.{{ base_domain }}/Shibboleth.sso/Metadata" -teams_manage_provision_samlsp_sp_cert: "" -teams_manage_provision_samlsp_trusted_proxy: false -teams_manage_provision_samlsp_sign: false -teams_spring_flyway_enabled: true -teams_docker_networks: - - name: "loadbalancer" -teams_server_restart_policy: always -teams_server_restart_retries: 0 diff --git a/roles/teams/handlers/main.yml b/roles/teams/handlers/main.yml deleted file mode 100644 index d866b5d27..000000000 --- a/roles/teams/handlers/main.yml +++ /dev/null @@ -1,9 +0,0 @@ -- name: restart teamsserver - community.docker.docker_container: - name: teamsserver - state: started - restart: true - # avoid restarting it creates unexpected data loss according to docker_container_module notes - comparisons: - '*': ignore - when: teamsserverontainer is success and teamsserverontainer is not change diff --git a/roles/teams/tasks/main.yml b/roles/teams/tasks/main.yml deleted file mode 100644 index 498c99d4c..000000000 --- a/roles/teams/tasks/main.yml +++ /dev/null @@ -1,101 +0,0 @@ ---- -- name: Create directory to keep configfile - ansible.builtin.file: - dest: "/opt/openconext/teams" - state: directory - owner: root - group: root - mode: "0770" - -- name: Place the serverapplication configfiles - ansible.builtin.template: - src: "{{ item }}.j2" - dest: /opt/openconext/teams/{{ item }} - owner: root - group: root - mode: "0644" - with_items: - - serverapplication.yml - - logback.xml - notify: restart teamsserver - -- name: Add the MariaDB docker network to the list of networks when MariaDB runs in Docker - ansible.builtin.set_fact: - teams_docker_networks: - - name: loadbalancer - - name: openconext_mariadb - when: mariadb_in_docker | default(false) | bool - -- name: Create and start the server container - community.docker.docker_container: - name: teamsserver - env: - TZ: "{{ timezone }}" - image: ghcr.io/openconext/openconext-teams-ng/teams-server:{{ teams_server_version }} - pull: true - restart_policy: "{{ teams_server_restart_policy }}" - restart_retries: "{{ teams_server_restart_retries }}" # Only for restart policy on-failure - state: started - networks: "{{ teams_docker_networks }}" - mounts: - - source: /opt/openconext/teams/serverapplication.yml - target: /application.yml - type: bind - - source: /opt/openconext/teams/logback.xml - target: /logback.xml - type: bind - command: "-Xmx512m --spring.config.location=./" - etc_hosts: - host.docker.internal: host-gateway - healthcheck: - test: - [ - "CMD", - "wget", - "-no-verbose", - "--tries=1", - "--spider", - "http://localhost:8080/internal/health", - ] - interval: 10s - timeout: 10s - retries: 3 - start_period: 10s - register: teamsserverontainer - -- name: Create the gui container - community.docker.docker_container: - name: teamsgui - image: ghcr.io/openconext/openconext-teams-ng/teams-gui:{{ teams_gui_version }} - pull: true - restart_policy: "always" - state: started - networks: - - name: "loadbalancer" - labels: - traefik.http.routers.teamsgui.rule: "Host(`teams.{{ base_domain }}`)" - traefik.http.routers.teamsgui.tls: "true" - traefik.enable: "true" - healthcheck: - test: ["CMD", "curl", "--fail", "http://localhost/internal/health"] - interval: 10s - timeout: 10s - retries: 3 - start_period: 10s - hostname: teams - mounts: - - source: /etc/localtime - target: /etc/localtime - type: bind - - source: /opt/openconext/common/favicon.ico - target: /var/www/favicon.ico - type: bind - env: - HTTPD_CSP: "{{ httpd_csp.strict_with_static_img }}" - HTTPD_SERVERNAME: "teams.{{ base_domain }}" - OPENCONEXT_INSTANCENAME: "{{ instance_name }}" - OPENCONEXT_ENGINE_LOGOUT_URL: "https://engine.{{ base_domain }}/logout" - OPENCONEXT_HELP_EMAIL: "{{ support_email }}" - SHIB_ENTITYID: "https://teams.{{ base_domain }}/shibboleth" - SHIB_REMOTE_ENTITYID: "https://engine.{{ base_domain }}/authentication/idp/metadata" - SHIB_REMOTE_METADATA: "{{ shibboleth_metadata_sources.engine }}" diff --git a/roles/teams/templates/logback.xml.j2 b/roles/teams/templates/logback.xml.j2 deleted file mode 100644 index b9c559d4f..000000000 --- a/roles/teams/templates/logback.xml.j2 +++ /dev/null @@ -1,29 +0,0 @@ -#jinja2:lstrip_blocks: True - - - - - - %d{ISO8601} %5p [%t] %logger{40}:%L - %m%n - - - - - {{ smtp_server }} - {{ noreply_email }} - {{ error_mail_to }} - {{ error_subject_prefix }}Unexpected error teams - - - - ERROR - - - - - - - - - - diff --git a/roles/teams/templates/serverapplication.yml.j2 b/roles/teams/templates/serverapplication.yml.j2 deleted file mode 100644 index f1a4088be..000000000 --- a/roles/teams/templates/serverapplication.yml.j2 +++ /dev/null @@ -1,131 +0,0 @@ -# The logging configuration. -logging: - config: file:///logback.xml - level: - org.hibernate.SQL: INFO - -api: - lifecycle: - username: {{ teams_api_lifecycle_username }} - password: "{{ teams_api_lifecycle_password }}" - -secure_cookie: true - -server: - port: 8080 - error: - path: "/error" - servlet: - session: - timeout: 28800 - cookie: - secure: true - server-header: no - -config: - support-email: {{ support_email }} - help-link-en: {{ teams_help_link_en }} - help-link-nl: {{ teams_help_link_nl }} - help-link-pt: {{ teams_help_link_pt }} - help-tos-en: {{ teams_tos_en }} - help-tos-nl: {{ teams_tos_nl }} - help-tos-pt: {{ teams_tos_pt }} - main-link: {{ teams_main_link }} - organization: {{ teams_organization }} - sponsor: {{ sponsor_name }} - supported_language_codes: {{ supported_language_codes }} - -features: - invite-migration-on: {{ teams.feature_invite_migration_on }} - -security: - user: - name: "{{ teams.voot_api_user }}" - password: "{{ external_group_provider_secrets.teams }}" - -sp_dashboard: - user-name: "{{ teams.spdashboard_api_user }}" - password: "{{ teams_api_spdashboard_password }}" - person-urn: "{{ teams.spdashboard_person_urn }}" - name: "SP Dashboard" - email: "{{ support_email }}" - -# Is this node in a load-balanced topology responsible for cleaning up resources (See ExpiredInvitationsRemover) -cron: - node-cron-job-responsible: {{ teams_cronjobmaster }} - expression: "0 0/15 * * * ?" - -teams: - default-stem-name: "{{ teams.default_stem_name }}" - group-name-context: "{{ teams.group_name_context }}" - product-name: "{{ teams.product_name }}" - non-guest-member-of: "{{ guest_qualifier }}" - -super_admins_team: - urns: - {% for value in teams.super_admins_team_urns %} -- "{{ value }}" - {% endfor %} - -voot: - serviceUrl: https://voot.{{ base_domain }} - accessTokenUri: "{{ teams_oauth2_token_url }}" - clientId: "{{ teams_authz_client_id }}" - clientSecret: "{{ teams_authz_client_secret }}" - scopes: "{{ teams_manage_provision_oidcrp_scopes }}" - -invite: - url: "https://invite.{{ base_domain }}/api/external/v1/teams" - user: "{{ invite.teamsuser }}" - password: "{{ invite.teamssecret }}" - -spring: - session: - store-type: jdbc - jdbc: - schema: classpath:org/springframework/session/jdbc/schema-mysql.sql - initialize-schema: always - cleanup-cron: "{% if teams_cronjobmaster %}0 13 * * * *{% else %}-{% endif %}" - jpa: - open-in-view: true - properties: - hibernate: - naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy - datasource: - driver-class-name: org.mariadb.jdbc.Driver - url: jdbc:mariadb://{{ teams.db_host }}/{{ teams.db_name }}?socketTimeout=30000 - username: {{ teams.db_user }} - password: "{{ teams.db_password }}" - mail: - host: {{ smtp_server }} - port: 25 - main: - banner-mode: "off" - flyway: - enabled: {{ teams_spring_flyway_enabled }} - validate-on-migrate: false - table: schema_version - security: - user: - name: na - password: na - -management: - health: - mail: - enabled: true - endpoints: - web: - exposure: - include: "health,info" - base-path: "/internal" - endpoint: - info: - enabled: true - info: - git: - mode: full - -email: - from: {{ instance_name }} Teams <{{ noreply_email }}> - base-url: https://teams.{{ base_domain }} diff --git a/roles/teams/vars/main.yml b/roles/teams/vars/main.yml deleted file mode 100644 index 207ea9b7c..000000000 --- a/roles/teams/vars/main.yml +++ /dev/null @@ -1,14 +0,0 @@ -manage_provision_oidcrp_client_id: "{{ teams_authz_client_id }}" -manage_provision_oidcrp_secret: "{{ teams_authz_client_secret }}" -manage_provision_oidcrp_name_en: "{{ teams_manage_provision_oidcrp_name_en }}" -manage_provision_oidcrp_description_en: "{{ teams_manage_provision_oidcrp_description_en }}" -manage_provision_oidcrp_grants: "{{ teams_manage_provision_oidcrp_grants }}" -manage_provision_oidcrp_allowed_resource_servers: "{{ teams_manage_provision_oidcrp_allowed_resource_servers }}" -manage_provision_samlsp_client_id: "{{ teams_manage_provision_samlsp_client_id }}" -manage_provision_samlsp_name_en: "{{ teams_manage_provision_samlsp_name_en }}" -manage_provision_samlsp_description_en: "{{ teams_manage_provision_samlsp_description_en }}" -manage_provision_samlsp_acs_location: "{{ teams_manage_provision_samlsp_acs_location }}" -manage_provision_samlsp_metadata_url: "{{ teams_manage_provision_samlsp_metadata_url }}" -manage_provision_samlsp_sp_cert: "{{ teams_manage_provision_samlsp_sp_cert }}" -manage_provision_samlsp_trusted_proxy: "{{ teams_manage_provision_samlsp_trusted_proxy }}" -manage_provision_samlsp_sign: "{{ teams_manage_provision_samlsp_sign }}" diff --git a/roles/welcome/files/site/images/teams-logo.png b/roles/welcome/files/site/images/teams-logo.png deleted file mode 100644 index 30f069553..000000000 Binary files a/roles/welcome/files/site/images/teams-logo.png and /dev/null differ diff --git a/roles/welcome/templates/site/index.html b/roles/welcome/templates/site/index.html index 1be3199c1..0601527c7 100644 --- a/roles/welcome/templates/site/index.html +++ b/roles/welcome/templates/site/index.html @@ -30,24 +30,6 @@

- {% if ( not minimal_install ) %} -
  • -

    - - Teams - -

    - -

    - Manage team members -

    -
    - - - -
    -
  • - {% endif %}

    OpenConext Administration