From 0d75d4e11c75cd7fe535939dc06ff73cb1447db1 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Thu, 2 Apr 2026 13:27:20 -0400 Subject: [PATCH 1/6] allow to configure metrix with saml2 as authentication instead of ldap --- manifests/init.pp | 34 ++++++ manifests/install.pp | 218 ++++++++++++++++++++++---------------- templates/99-local.py.epp | 19 +++- 3 files changed, 177 insertions(+), 94 deletions(-) diff --git a/manifests/init.pp b/manifests/init.pp index acb6c70..86c5743 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -13,8 +13,14 @@ String $cluster_name, String $subdomain, String $slurm_user = 'slurm', + String $ssl_private_key_file = '/etc/ssl/metrix.private.key', + String $ssl_public_cert_file = '/etc/ssl/metrix.public.cert', + Enum['ldap', 'saml2'] $auth_type = 'ldap', Optional[String] $slurm_db_ip = undef, Optional[Integer] $slurm_db_port = undef, + Optional[String] $ssl_private_key = undef, + Optional[String] $ssl_public_cert = undef, + Optional[String] $idp_metadata = undef, ) { include metrix::install @@ -38,6 +44,9 @@ 'slurm_db_port' => pick($slurm_db_port, $db_port), 'base_dn' => $base_dn, 'ldap_password' => $ldap_password, + 'auth_type' => $auth_type, + 'ssl_key_file' => $ssl_private_key_file, + 'ssl_cert_file' => $ssl_public_cert_file, } ), owner => 'apache', @@ -137,6 +146,31 @@ mode => '0600', } + if $ssl_private_key != undef { + file { $ssl_private_key_file: + content => $ssl_private_key, + mode => '0400', + owner => 'apache', + group => 'apache', + } + } + if $ssl_public_cert != undef { + file { $ssl_public_cert_file: + content => $ssl_public_cert, + mode => '0422', + owner => 'apache', + group => 'apache', + } + } + if $idp_metadata != undef { + file { '/var/www/metrix/idp_metadata.xml': + content => $idp_metadata, + mode => '0422', + owner => 'apache', + group => 'apache', + } + } + exec { 'metrix_api_token': command => Sensitive($api_token_command), subscribe => [ diff --git a/manifests/install.pp b/manifests/install.pp index 01e3d56..4570802 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -2,6 +2,11 @@ String $version = '1.6.0', String $python_version = '3.13', ) { + $auth_type = lookup('metrix::auth_type') + + if $auth_type == 'saml2' { + stdlib::ensure_packages(['libffi-devel', 'xmlsec1', 'xmlsec1-openssl']) + } stdlib::ensure_packages(['gcc', 'openldap-devel',]) file { '/var/www/metrix/': @@ -20,85 +25,99 @@ cleanup => true, user => 'apache', } - # We use LDAP auth instead of SAML2 auth, so we can remove all - # code and dependencies related to SAML2 - -> file_line { 'remove_saml2_urls': - ensure => absent, - path => '/var/www/metrix/userportal/urls.py', - match => 'saml2', - match_for_absence => true, - multiple => true, - } - -> file_line { 'remove_saml2_10-base': - ensure => absent, - path => '/var/www/metrix/userportal/settings/10-base.py', - match => 'saml2', - match_for_absence => true, - multiple => true, - } - -> file { 'remove_40-saml': - ensure => absent, - path => '/var/www/metrix/userportal/settings/40-saml.py', - } - -> file_line { 'cffi': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^cffi', - match_for_absence => true, - } - -> file_line { 'cryptography': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^cryptography', - match_for_absence => true, - } - -> file_line { 'defusedxml': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^defusedxml', - match_for_absence => true, - } - -> file_line { 'djangosaml2': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^djangosaml2', - match_for_absence => true, - } - -> file_line { 'elementpath': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^elementpath', - match_for_absence => true, - } - -> file_line { 'pycparser': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pycparser', - match_for_absence => true, - } - -> file_line { 'pyparsing': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pyparsing', - match_for_absence => true, - } - -> file_line { 'pysaml2': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pysaml2', - match_for_absence => true, - } - -> file_line { 'pyOpenSSL': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pyOpenSSL', - match_for_absence => true, - } - -> file_line { 'xmlschema': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^xmlschema', - match_for_absence => true, + if $auth_type == 'ldap' { + # We use LDAP auth instead of SAML2 auth, so we can remove all + # code and dependencies related to SAML2 + file_line { 'remove_saml2_urls': + ensure => absent, + path => '/var/www/metrix/userportal/urls.py', + match => 'saml2', + match_for_absence => true, + multiple => true, + require => Archive['metrix'] + } + -> file_line { 'remove_saml2_10-base': + ensure => absent, + path => '/var/www/metrix/userportal/settings/10-base.py', + match => 'saml2', + match_for_absence => true, + multiple => true, + } + -> file { 'remove_40-saml': + ensure => absent, + path => '/var/www/metrix/userportal/settings/40-saml.py', + } + -> file_line { 'cffi': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^cffi', + match_for_absence => true, + } + -> file_line { 'cryptography': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^cryptography', + match_for_absence => true, + } + -> file_line { 'defusedxml': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^defusedxml', + match_for_absence => true, + } + -> file_line { 'djangosaml2': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^djangosaml2', + match_for_absence => true, + } + -> file_line { 'elementpath': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^elementpath', + match_for_absence => true, + } + -> file_line { 'pycparser': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pycparser', + match_for_absence => true, + } + -> file_line { 'pyparsing': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pyparsing', + match_for_absence => true, + } + -> file_line { 'pysaml2': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pysaml2', + match_for_absence => true, + } + -> file_line { 'pyOpenSSL': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pyOpenSSL', + match_for_absence => true, + } + -> file_line { 'xmlschema': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^xmlschema', + match_for_absence => true, + } + } + else { + # fixes version of cffi https://github.com/authlib/authlib/issues/681 + file_line { 'cffi': + ensure => present, + path => '/var/www/metrix/requirements.txt', + match => '^cffi', + line => 'cffi==1.17.1', + require => Archive['metrix'], + before => Uv::Venv['metrix_venv'], + } } # Next dependencies are not used by Trailblazing Turtle # they are dependencies of matplotlib which should be optional @@ -106,11 +125,12 @@ # so we remove the dependencies and install a fork of prometheus-api-client # that only make matplotlib optional. # See: https://github.com/4n4nd/prometheus-api-client-python/pull/303 - -> file_line { 'contourpy': + file_line { 'contourpy': ensure => absent, path => '/var/www/metrix/requirements.txt', match => '^contourpy', match_for_absence => true, + require => Archive['metrix'], } -> file_line { 'cycler': ensure => absent, @@ -173,22 +193,38 @@ path => '/var/www/metrix/requirements.txt', match => '^mysqlclient', line => 'pymysql~=1.1', - } - -> uv::venv { 'metrix_venv': - prefix => '/opt/software/metrix-env', - python => $python_version, - requirements => 'django-auth-ldap', - requirements_path => '/var/www/metrix/requirements.txt', - require => [ - Package['gcc'], - Package['openldap-devel'], - ], + before => Uv::Venv['metrix_venv'], + } + if $auth_type == 'ldap' { + uv::venv { 'metrix_venv': + prefix => '/opt/software/metrix-env', + python => $python_version, + requirements => 'django-auth-ldap', + requirements_path => '/var/www/metrix/requirements.txt', + require => [ + Package['gcc'], + Package['openldap-devel'], + ], + } + } + else { + uv::venv { 'metrix_venv': + prefix => '/opt/software/metrix-env', + python => $python_version, + requirements_path => '/var/www/metrix/requirements.txt', + require => [ + Package['gcc'], + Package['openldap-devel'], + Package['libffi-devel'], + ], + } } # Replace mysqlclient by pymysql in the Python code import. - -> file_line { 'pymysql': + file_line { 'pymysql': path => '/var/www/metrix/manage.py', after => '^import sys', line => 'import pymysql; pymysql.install_as_MySQLdb()', + require => Uv::Venv['metrix_venv'] } -> file_line { 'manage.py_header': path => '/var/www/metrix/manage.py', diff --git a/templates/99-local.py.epp b/templates/99-local.py.epp index ef5b40c..bcd6238 100644 --- a/templates/99-local.py.epp +++ b/templates/99-local.py.epp @@ -14,7 +14,6 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,cn=users,cn=accounts,<%= $base_dn %>" LDAP_BASE_DN = '<%= $base_dn %>' - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -42,7 +41,6 @@ DATABASES = { import ldap ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) - PROMETHEUS = { 'url': 'http://<%= $prometheus_ip %>:<%= $prometheus_port %>', 'headers': {}, @@ -55,12 +53,27 @@ STATIC_URL = '/static/' STATIC_ROOT = '/var/www/metrix-static/' AUTHENTICATION_BACKENDS = [ - 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ] + +<% if $auth_type == 'ldap' { %> +AUTHENTICATION_BACKENDS += ['django_auth_ldap.backend.LDAPBackend'] LOGIN_URL = '/accounts/login/' # So it does not use SAML2 +<% } elsif $auth_type == 'saml2' { %> + +AUTHENTICATION_BACKENDS += ['userportal.authentication.staffSaml2Backend'] +SAML_CONFIG['service']['sp']['endpoints']['assertion_consumer_service'] = ('https://<%= $subdomain %>.<%= $domain_name %>/saml2/acs/', saml2.BINDING_HTTP_POST) +SAML_CONFIG['entityid'] = 'https://<%= $subdomain %>.<%= $domain_name %>/saml2/metadata/' +SAML_CONFIG['key_file'] = '<%= $ssl_key_file %>' +SAML_CONFIG['cert_file'] = '<%= $ssl_cert_file %>' +SAML_CONFIG['encryption_keypairs'][0]['key_file'] = '<%= $ssl_key_file %>' +SAML_CONFIG['encryption_keypairs'][0]['cert_file'] = '<%= $ssl_cert_file %>' +SAML_CONFIG['metadata']['local'][0] = '/var/www/metrix/idp_metadata.xml' +<% } %> + + EXPORTER_INSTALLED = [ 'slurm-job-exporter', 'node_exporter', From 3869ec2739cef3d8f0d5ebe3057284fb5b379b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Tue, 7 Apr 2026 12:42:29 -0400 Subject: [PATCH 2/6] Move auth specific details in their own files --- manifests/auth/ldap.pp | 101 +++++++++++++++++++++++++ manifests/auth/saml2.pp | 39 ++++++++++ manifests/init.pp | 38 +++------- manifests/install.pp | 151 ++++++-------------------------------- templates/99-local.py.epp | 8 +- 5 files changed, 177 insertions(+), 160 deletions(-) create mode 100644 manifests/auth/ldap.pp create mode 100644 manifests/auth/saml2.pp diff --git a/manifests/auth/ldap.pp b/manifests/auth/ldap.pp new file mode 100644 index 0000000..d0861cb --- /dev/null +++ b/manifests/auth/ldap.pp @@ -0,0 +1,101 @@ +class metrix::auth::ldap { + # We use LDAP auth instead of SAML2 auth, so we can remove all + # code and dependencies related to SAML2 + file_line { 'remove_saml2_urls': + ensure => absent, + path => '/var/www/metrix/userportal/urls.py', + match => 'saml2', + match_for_absence => true, + multiple => true, + require => Archive['metrix'], + before => Uv::Venv['metrix_venv'], + } + file_line { 'remove_saml2_10-base': + ensure => absent, + path => '/var/www/metrix/userportal/settings/10-base.py', + match => 'saml2', + match_for_absence => true, + multiple => true, + before => Uv::Venv['metrix_venv'], + } + file { 'remove_40-saml': + ensure => absent, + path => '/var/www/metrix/userportal/settings/40-saml.py', + before => Uv::Venv['metrix_venv'], + } + file_line { 'cffi': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^cffi', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'cryptography': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^cryptography', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'defusedxml': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^defusedxml', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'djangosaml2': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^djangosaml2', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'elementpath': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^elementpath', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'pycparser': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pycparser', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'pyparsing': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pyparsing', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'pysaml2': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pysaml2', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'pyOpenSSL': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^pyOpenSSL', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'xmlschema': + ensure => absent, + path => '/var/www/metrix/requirements.txt', + match => '^xmlschema', + match_for_absence => true, + before => Uv::Venv['metrix_venv'], + } + file_line { 'django-auth-ldap': + line => 'django-auth-ldap', + path => '/var/www/metrix/requirements.txt', + before => Uv::Venv['metrix_venv'], + } +} diff --git a/manifests/auth/saml2.pp b/manifests/auth/saml2.pp new file mode 100644 index 0000000..67b57f5 --- /dev/null +++ b/manifests/auth/saml2.pp @@ -0,0 +1,39 @@ +class metrix::auth::saml2 ( + String $ssl_private_key, + String $ssl_public_cert, + String $idp_metadata, +) { + ensure_packages(['libffi-devel', 'xmlsec1', 'xmlsec1-openssl']) + + file { '/var/www/metrix/saml2-private.key': + content => $ssl_private_key, + mode => '0400', + owner => 'apache', + group => 'apache', + require => File['/var/www/metrix'], + } + file { '/var/www/metrix/saml2-public.pem': + content => $ssl_public_cert, + mode => '0422', + owner => 'apache', + group => 'apache', + require => File['/var/www/metrix'], + } + file { '/var/www/metrix/idp_metadata.xml': + content => $idp_metadata, + mode => '0422', + owner => 'apache', + group => 'apache', + require => File['/var/www/metrix'], + } + + # fixes version of cffi https://github.com/authlib/authlib/issues/681 + file_line { 'cffi': + ensure => present, + path => '/var/www/metrix/requirements.txt', + match => '^cffi', + line => 'cffi==1.17.1', + require => Archive['metrix'], + before => Uv::Venv['metrix_venv'], + } +} diff --git a/manifests/init.pp b/manifests/init.pp index 86c5743..4c048f6 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -23,6 +23,17 @@ Optional[String] $idp_metadata = undef, ) { include metrix::install + case $auth_type { + 'ldap': { + include metrix::auth::ldap + } + 'saml2': { + include metrix::auth::saml2 + } + default: { + fail('Unsupported auth_type') + } + } file { '/var/www/metrix/userportal/settings/99-local.py': show_diff => false, @@ -45,8 +56,6 @@ 'base_dn' => $base_dn, 'ldap_password' => $ldap_password, 'auth_type' => $auth_type, - 'ssl_key_file' => $ssl_private_key_file, - 'ssl_cert_file' => $ssl_public_cert_file, } ), owner => 'apache', @@ -146,31 +155,6 @@ mode => '0600', } - if $ssl_private_key != undef { - file { $ssl_private_key_file: - content => $ssl_private_key, - mode => '0400', - owner => 'apache', - group => 'apache', - } - } - if $ssl_public_cert != undef { - file { $ssl_public_cert_file: - content => $ssl_public_cert, - mode => '0422', - owner => 'apache', - group => 'apache', - } - } - if $idp_metadata != undef { - file { '/var/www/metrix/idp_metadata.xml': - content => $idp_metadata, - mode => '0422', - owner => 'apache', - group => 'apache', - } - } - exec { 'metrix_api_token': command => Sensitive($api_token_command), subscribe => [ diff --git a/manifests/install.pp b/manifests/install.pp index 4570802..3b42a00 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,4 +1,5 @@ class metrix::install ( + String $source_url = 'https://github.com/guilbaults/TrailblazingTurtle/archive/refs/tags/v${version}.tar.gz', String $version = '1.6.0', String $python_version = '3.13', ) { @@ -7,16 +8,17 @@ if $auth_type == 'saml2' { stdlib::ensure_packages(['libffi-devel', 'xmlsec1', 'xmlsec1-openssl']) } - stdlib::ensure_packages(['gcc', 'openldap-devel',]) + stdlib::ensure_packages(['gcc', 'openldap-devel', 'httpd']) file { '/var/www/metrix/': - ensure => 'directory', - owner => 'apache', - group => 'apache', + ensure => 'directory', + owner => 'apache', + group => 'apache', + require => Package['httpd'], } -> archive { 'metrix': ensure => present, - source => "https://github.com/guilbaults/TrailblazingTurtle/archive/refs/tags/v${version}.tar.gz", + source => inline_template($source_url), creates => '/var/www/metrix/manage.py', path => '/tmp/metrix.tar.gz', extract => true, @@ -25,100 +27,7 @@ cleanup => true, user => 'apache', } - if $auth_type == 'ldap' { - # We use LDAP auth instead of SAML2 auth, so we can remove all - # code and dependencies related to SAML2 - file_line { 'remove_saml2_urls': - ensure => absent, - path => '/var/www/metrix/userportal/urls.py', - match => 'saml2', - match_for_absence => true, - multiple => true, - require => Archive['metrix'] - } - -> file_line { 'remove_saml2_10-base': - ensure => absent, - path => '/var/www/metrix/userportal/settings/10-base.py', - match => 'saml2', - match_for_absence => true, - multiple => true, - } - -> file { 'remove_40-saml': - ensure => absent, - path => '/var/www/metrix/userportal/settings/40-saml.py', - } - -> file_line { 'cffi': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^cffi', - match_for_absence => true, - } - -> file_line { 'cryptography': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^cryptography', - match_for_absence => true, - } - -> file_line { 'defusedxml': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^defusedxml', - match_for_absence => true, - } - -> file_line { 'djangosaml2': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^djangosaml2', - match_for_absence => true, - } - -> file_line { 'elementpath': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^elementpath', - match_for_absence => true, - } - -> file_line { 'pycparser': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pycparser', - match_for_absence => true, - } - -> file_line { 'pyparsing': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pyparsing', - match_for_absence => true, - } - -> file_line { 'pysaml2': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pysaml2', - match_for_absence => true, - } - -> file_line { 'pyOpenSSL': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^pyOpenSSL', - match_for_absence => true, - } - -> file_line { 'xmlschema': - ensure => absent, - path => '/var/www/metrix/requirements.txt', - match => '^xmlschema', - match_for_absence => true, - } - } - else { - # fixes version of cffi https://github.com/authlib/authlib/issues/681 - file_line { 'cffi': - ensure => present, - path => '/var/www/metrix/requirements.txt', - match => '^cffi', - line => 'cffi==1.17.1', - require => Archive['metrix'], - before => Uv::Venv['metrix_venv'], - } - } + # Next dependencies are not used by Trailblazing Turtle # they are dependencies of matplotlib which should be optional # dependencies of prometheus-api-client, but currently aren't @@ -190,41 +99,25 @@ } # Replace mysqlclient by a pure python compatible alternative to reduce install dependencies -> file_line { 'mysqlclient': - path => '/var/www/metrix/requirements.txt', - match => '^mysqlclient', - line => 'pymysql~=1.1', + path => '/var/www/metrix/requirements.txt', + match => '^mysqlclient', + line => 'pymysql~=1.1', before => Uv::Venv['metrix_venv'], } - if $auth_type == 'ldap' { - uv::venv { 'metrix_venv': - prefix => '/opt/software/metrix-env', - python => $python_version, - requirements => 'django-auth-ldap', - requirements_path => '/var/www/metrix/requirements.txt', - require => [ - Package['gcc'], - Package['openldap-devel'], - ], - } - } - else { - uv::venv { 'metrix_venv': - prefix => '/opt/software/metrix-env', - python => $python_version, - requirements_path => '/var/www/metrix/requirements.txt', - require => [ - Package['gcc'], - Package['openldap-devel'], - Package['libffi-devel'], - ], - } + + uv::venv { 'metrix_venv': + prefix => '/opt/software/metrix-env', + python => $python_version, + requirements_path => '/var/www/metrix/requirements.txt', } + Package <| tag == 'metrix' |> -> Uv::Venv['metrix_venv'] + # Replace mysqlclient by pymysql in the Python code import. file_line { 'pymysql': - path => '/var/www/metrix/manage.py', - after => '^import sys', - line => 'import pymysql; pymysql.install_as_MySQLdb()', - require => Uv::Venv['metrix_venv'] + path => '/var/www/metrix/manage.py', + after => '^import sys', + line => 'import pymysql; pymysql.install_as_MySQLdb()', + require => Uv::Venv['metrix_venv'], } -> file_line { 'manage.py_header': path => '/var/www/metrix/manage.py', diff --git a/templates/99-local.py.epp b/templates/99-local.py.epp index bcd6238..a452ff1 100644 --- a/templates/99-local.py.epp +++ b/templates/99-local.py.epp @@ -66,10 +66,10 @@ LOGIN_URL = '/accounts/login/' # So it does not use SAML2 AUTHENTICATION_BACKENDS += ['userportal.authentication.staffSaml2Backend'] SAML_CONFIG['service']['sp']['endpoints']['assertion_consumer_service'] = ('https://<%= $subdomain %>.<%= $domain_name %>/saml2/acs/', saml2.BINDING_HTTP_POST) SAML_CONFIG['entityid'] = 'https://<%= $subdomain %>.<%= $domain_name %>/saml2/metadata/' -SAML_CONFIG['key_file'] = '<%= $ssl_key_file %>' -SAML_CONFIG['cert_file'] = '<%= $ssl_cert_file %>' -SAML_CONFIG['encryption_keypairs'][0]['key_file'] = '<%= $ssl_key_file %>' -SAML_CONFIG['encryption_keypairs'][0]['cert_file'] = '<%= $ssl_cert_file %>' +SAML_CONFIG['key_file'] = '/var/www/metrix/saml2-private.key' +SAML_CONFIG['cert_file'] = '/var/www/metrix/saml2-public.pem' +SAML_CONFIG['encryption_keypairs'][0]['key_file'] = '/var/www/metrix/saml2-private.key' +SAML_CONFIG['encryption_keypairs'][0]['cert_file'] = '/var/www/metrix/saml2-public.pem' SAML_CONFIG['metadata']['local'][0] = '/var/www/metrix/idp_metadata.xml' <% } %> From fef66801a3e7e291fd3b6efca6fb9136d4e7bcae Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Wed, 8 Apr 2026 11:39:12 -0400 Subject: [PATCH 3/6] update to metrix 1.7.0 which removes need to fix cffi --- manifests/auth/saml2.pp | 10 ---------- manifests/install.pp | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/manifests/auth/saml2.pp b/manifests/auth/saml2.pp index 67b57f5..3853ce2 100644 --- a/manifests/auth/saml2.pp +++ b/manifests/auth/saml2.pp @@ -26,14 +26,4 @@ group => 'apache', require => File['/var/www/metrix'], } - - # fixes version of cffi https://github.com/authlib/authlib/issues/681 - file_line { 'cffi': - ensure => present, - path => '/var/www/metrix/requirements.txt', - match => '^cffi', - line => 'cffi==1.17.1', - require => Archive['metrix'], - before => Uv::Venv['metrix_venv'], - } } diff --git a/manifests/install.pp b/manifests/install.pp index 3b42a00..7a7bee7 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,6 +1,6 @@ class metrix::install ( String $source_url = 'https://github.com/guilbaults/TrailblazingTurtle/archive/refs/tags/v${version}.tar.gz', - String $version = '1.6.0', + String $version = '1.7.0', String $python_version = '3.13', ) { $auth_type = lookup('metrix::auth_type') @@ -27,7 +27,6 @@ cleanup => true, user => 'apache', } - # Next dependencies are not used by Trailblazing Turtle # they are dependencies of matplotlib which should be optional # dependencies of prometheus-api-client, but currently aren't From 82ae3e97c6b474b97bc5ba14fbea1ff1767e45fa Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 10 Apr 2026 13:16:40 -0400 Subject: [PATCH 4/6] add configurable extra attributes to request, to access and to get staff privilege --- manifests/auth/ldap.pp | 12 ++++++++ manifests/auth/saml2.pp | 18 ++++++++++++ manifests/init.pp | 8 +++--- .../{99-local.py.epp => 91-local.py.epp} | 22 ++------------- templates/92-local_ldap.py.epp | 2 ++ templates/92-local_saml2.py.epp | 28 +++++++++++++++++++ 6 files changed, 67 insertions(+), 23 deletions(-) rename templates/{99-local.py.epp => 91-local.py.epp} (72%) create mode 100644 templates/92-local_ldap.py.epp create mode 100644 templates/92-local_saml2.py.epp diff --git a/manifests/auth/ldap.pp b/manifests/auth/ldap.pp index d0861cb..2ece1b2 100644 --- a/manifests/auth/ldap.pp +++ b/manifests/auth/ldap.pp @@ -98,4 +98,16 @@ path => '/var/www/metrix/requirements.txt', before => Uv::Venv['metrix_venv'], } + + file { '/var/www/metrix/userportal/settings/92-local_ldap.py': + show_diff => false, + content => epp('metrix/92-local_ldap.py', + { + } + ), + owner => 'apache', + group => 'apache', + mode => '0600', + require => Class['metrix::install'], + } } diff --git a/manifests/auth/saml2.pp b/manifests/auth/saml2.pp index 3853ce2..c2327a3 100644 --- a/manifests/auth/saml2.pp +++ b/manifests/auth/saml2.pp @@ -2,6 +2,9 @@ String $ssl_private_key, String $ssl_public_cert, String $idp_metadata, + Array[String] $extra_required_attributes = [], + Array[Hash[String, String]] $staff_attributes = [], + Array[Hash[String, String]] $required_access_attributes = [], ) { ensure_packages(['libffi-devel', 'xmlsec1', 'xmlsec1-openssl']) @@ -26,4 +29,19 @@ group => 'apache', require => File['/var/www/metrix'], } + + file { '/var/www/metrix/userportal/settings/92-local_saml2.py': + show_diff => false, + content => epp('metrix/92-local_saml2.py', + { + 'extra_required_attributes' => $extra_required_attributes, + 'staff_attributes' => $staff_attributes, + 'required_access_attributes' => $required_access_attributes + } + ), + owner => 'apache', + group => 'apache', + mode => '0600', + require => Class['metrix::install'], + } } diff --git a/manifests/init.pp b/manifests/init.pp index 4c048f6..ad2ad08 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -35,9 +35,9 @@ } } - file { '/var/www/metrix/userportal/settings/99-local.py': + file { '/var/www/metrix/userportal/settings/91-local.py': show_diff => false, - content => epp('metrix/99-local.py', + content => epp('metrix/91-local.py', { 'password' => $password, 'slurm_user' => $slurm_user, @@ -103,7 +103,7 @@ subscribe => [ Mysql::Db['metrix'], Class['metrix::install'], - File['/var/www/metrix/userportal/settings/99-local.py'], + File['/var/www/metrix/userportal/settings/91-local.py'], File['/var/www/metrix/userportal/local.py'], ], notify => Service['metrix'], @@ -116,7 +116,7 @@ '/opt/software/metrix-env/bin', ], require => [ - File['/var/www/metrix/userportal/settings/99-local.py'], + File['/var/www/metrix/userportal/settings/91-local.py'], File['/var/www/metrix/userportal/local.py'], Class['metrix::install'], ], diff --git a/templates/99-local.py.epp b/templates/91-local.py.epp similarity index 72% rename from templates/99-local.py.epp rename to templates/91-local.py.epp index a452ff1..f1f1e64 100644 --- a/templates/99-local.py.epp +++ b/templates/91-local.py.epp @@ -4,8 +4,10 @@ pymysql.install_as_MySQLdb() SECRET_KEY = '<%= $secret_key %>' DEBUG = False +BASE_URL = 'https://<%= $subdomain %>.<%= $domain_name %>' + ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] -CSRF_TRUSTED_ORIGINS = ['https://<%= $subdomain %>.<%= $domain_name %>'] +CSRF_TRUSTED_ORIGINS = [BASE_URL] AUTH_LDAP_SERVER_URI = 'ldaps://ipa.int.<%= $domain_name %>/' AUTH_LDAP_BIND_DN = 'uid=admin,cn=users,cn=accounts,<%= $base_dn %>', @@ -56,24 +58,6 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', ] - -<% if $auth_type == 'ldap' { %> -AUTHENTICATION_BACKENDS += ['django_auth_ldap.backend.LDAPBackend'] -LOGIN_URL = '/accounts/login/' # So it does not use SAML2 - -<% } elsif $auth_type == 'saml2' { %> - -AUTHENTICATION_BACKENDS += ['userportal.authentication.staffSaml2Backend'] -SAML_CONFIG['service']['sp']['endpoints']['assertion_consumer_service'] = ('https://<%= $subdomain %>.<%= $domain_name %>/saml2/acs/', saml2.BINDING_HTTP_POST) -SAML_CONFIG['entityid'] = 'https://<%= $subdomain %>.<%= $domain_name %>/saml2/metadata/' -SAML_CONFIG['key_file'] = '/var/www/metrix/saml2-private.key' -SAML_CONFIG['cert_file'] = '/var/www/metrix/saml2-public.pem' -SAML_CONFIG['encryption_keypairs'][0]['key_file'] = '/var/www/metrix/saml2-private.key' -SAML_CONFIG['encryption_keypairs'][0]['cert_file'] = '/var/www/metrix/saml2-public.pem' -SAML_CONFIG['metadata']['local'][0] = '/var/www/metrix/idp_metadata.xml' -<% } %> - - EXPORTER_INSTALLED = [ 'slurm-job-exporter', 'node_exporter', diff --git a/templates/92-local_ldap.py.epp b/templates/92-local_ldap.py.epp new file mode 100644 index 0000000..ec021ed --- /dev/null +++ b/templates/92-local_ldap.py.epp @@ -0,0 +1,2 @@ +AUTHENTICATION_BACKENDS += ['django_auth_ldap.backend.LDAPBackend'] +LOGIN_URL = '/accounts/login/' # So it does not use SAML2 diff --git a/templates/92-local_saml2.py.epp b/templates/92-local_saml2.py.epp new file mode 100644 index 0000000..9e43086 --- /dev/null +++ b/templates/92-local_saml2.py.epp @@ -0,0 +1,28 @@ +AUTHENTICATION_BACKENDS += ['userportal.authentication.staffSaml2Backend'] +SAML_CONFIG['service']['sp']['endpoints']['assertion_consumer_service'] = (BASE_URL + '/saml2/acs/', saml2.BINDING_HTTP_POST) +SAML_CONFIG['entityid'] = BASE_URL + '/saml2/metadata/' +SAML_CONFIG['key_file'] = '/var/www/metrix/saml2-private.key' +SAML_CONFIG['cert_file'] = '/var/www/metrix/saml2-public.pem' +SAML_CONFIG['encryption_keypairs'][0]['key_file'] = '/var/www/metrix/saml2-private.key' +SAML_CONFIG['encryption_keypairs'][0]['cert_file'] = '/var/www/metrix/saml2-public.pem' +SAML_CONFIG['metadata']['local'][0] = '/var/www/metrix/idp_metadata.xml' +SAML_CONFIG['service']['sp']['required_attributes'] += [ +<% $extra_required_attributes.each |$attribute| { -%> + '<%= $attribute %>', +<% } -%> +] +SAML_CONFIG['required_access_attributes'] = [ +<% $required_access_attributes.each |$pair| { -%> +<% $pair.each |$attribute, $value| { -%> + ('<%= $attribute %>', '<%= $value %>'), +<% } -%> +<% } -%> +] + +SAML_CONFIG['staff_attributes'] = [ +<% $staff_attributes.each |$pair| { -%> +<% $pair.each |$attribute, $value| { -%> + ('<%= $attribute %>', '<%= $value %>'), +<% } -%> +<% } -%> +] From d93dfb8a6c313776138370c7c3b5f6c5afecf68f Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 10 Apr 2026 17:09:03 -0400 Subject: [PATCH 5/6] generalized attributes logic to make it applicable wider than SAML2 --- manifests/init.pp | 37 ++++++++++++++++++--------------- templates/91-local.py.epp | 16 ++++++++++++++ templates/92-local_saml2.py.epp | 17 ++------------- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/manifests/init.pp b/manifests/init.pp index ad2ad08..707699b 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -16,6 +16,8 @@ String $ssl_private_key_file = '/etc/ssl/metrix.private.key', String $ssl_public_cert_file = '/etc/ssl/metrix.public.cert', Enum['ldap', 'saml2'] $auth_type = 'ldap', + Array[Hash[String, String]] $staff_attributes = [], + Array[Hash[String, String]] $required_access_attributes = [], Optional[String] $slurm_db_ip = undef, Optional[Integer] $slurm_db_port = undef, Optional[String] $ssl_private_key = undef, @@ -39,23 +41,24 @@ show_diff => false, content => epp('metrix/91-local.py', { - 'password' => $password, - 'slurm_user' => $slurm_user, - 'slurm_password' => $slurm_password, - 'cluster_name' => $cluster_name, - 'secret_key' => stdlib::seeded_rand_string(32, $password), - 'domain_name' => $domain_name, - 'subdomain' => $subdomain, - 'logins' => $logins, - 'prometheus_ip' => $prometheus_ip, - 'prometheus_port' => $prometheus_port, - 'db_ip' => $db_ip, - 'db_port' => $db_port, - 'slurm_db_ip' => pick($slurm_db_ip, $db_ip), - 'slurm_db_port' => pick($slurm_db_port, $db_port), - 'base_dn' => $base_dn, - 'ldap_password' => $ldap_password, - 'auth_type' => $auth_type, + 'password' => $password, + 'slurm_password' => $slurm_password, + 'cluster_name' => $cluster_name, + 'secret_key' => seeded_rand_string(32, $password), + 'domain_name' => $domain_name, + 'subdomain' => $subdomain, + 'logins' => $logins, + 'prometheus_ip' => $prometheus_ip, + 'prometheus_port' => $prometheus_port, + 'db_ip' => $db_ip, + 'db_port' => $db_port, + 'slurm_db_ip' => pick($slurm_db_ip, $db_ip), + 'slurm_db_port' => pick($slurm_db_port, $db_port), + 'base_dn' => $base_dn, + 'ldap_password' => $ldap_password, + 'auth_type' => $auth_type, + 'staff_attributes' => $staff_attributes, + 'required_access_attributes' => $required_access_attributes, } ), owner => 'apache', diff --git a/templates/91-local.py.epp b/templates/91-local.py.epp index f1f1e64..5cb347d 100644 --- a/templates/91-local.py.epp +++ b/templates/91-local.py.epp @@ -58,6 +58,22 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', ] +REQUIRED_ACCESS_ATTRIBUTES = [ +<% $required_access_attributes.each |$pair| { -%> +<% $pair.each |$attribute, $value| { -%> + ('<%= $attribute %>', '<%= $value %>'), +<% } -%> +<% } -%> +] + +STAFF_ATTRIBUTES = [ +<% $staff_attributes.each |$pair| { -%> +<% $pair.each |$attribute, $value| { -%> + ('<%= $attribute %>', '<%= $value %>'), +<% } -%> +<% } -%> +] + EXPORTER_INSTALLED = [ 'slurm-job-exporter', 'node_exporter', diff --git a/templates/92-local_saml2.py.epp b/templates/92-local_saml2.py.epp index 9e43086..49827b8 100644 --- a/templates/92-local_saml2.py.epp +++ b/templates/92-local_saml2.py.epp @@ -11,18 +11,5 @@ SAML_CONFIG['service']['sp']['required_attributes'] += [ '<%= $attribute %>', <% } -%> ] -SAML_CONFIG['required_access_attributes'] = [ -<% $required_access_attributes.each |$pair| { -%> -<% $pair.each |$attribute, $value| { -%> - ('<%= $attribute %>', '<%= $value %>'), -<% } -%> -<% } -%> -] - -SAML_CONFIG['staff_attributes'] = [ -<% $staff_attributes.each |$pair| { -%> -<% $pair.each |$attribute, $value| { -%> - ('<%= $attribute %>', '<%= $value %>'), -<% } -%> -<% } -%> -] +SAML_CONFIG['required_access_attributes'] = REQUIRED_ACCESS_ATTRIBUTES +SAML_CONFIG['staff_attributes'] = STAFF_ATTRIBUTES From a0c3cb3d2103b7d06e812b9ef07eac1e320f0912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Wed, 15 Apr 2026 12:25:10 -0400 Subject: [PATCH 6/6] Remove auth_type from 91-local.py.epp --- manifests/init.pp | 1 - 1 file changed, 1 deletion(-) diff --git a/manifests/init.pp b/manifests/init.pp index 707699b..014ca38 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -56,7 +56,6 @@ 'slurm_db_port' => pick($slurm_db_port, $db_port), 'base_dn' => $base_dn, 'ldap_password' => $ldap_password, - 'auth_type' => $auth_type, 'staff_attributes' => $staff_attributes, 'required_access_attributes' => $required_access_attributes, }