From 93d1210200ace091b8b0f93cdb558d4add2337b5 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Thu, 2 Apr 2026 13:27:20 -0400 Subject: [PATCH 1/4] allow to configure metrix with saml2 as authentication instead of ldap --- manifests/init.pp | 34 ++++++ manifests/install.pp | 220 ++++++++++++++++++++++---------------- templates/99-local.py.epp | 19 +++- 3 files changed, 178 insertions(+), 95 deletions(-) diff --git a/manifests/init.pp b/manifests/init.pp index 2c4001e..a49bd16 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -12,6 +12,12 @@ String $slurm_password, String $cluster_name, String $subdomain, + 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] $ssl_private_key = undef, + Optional[String] $ssl_public_cert = undef, + Optional[String] $idp_metadata = undef, ) { include metrix::install @@ -32,6 +38,9 @@ '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', @@ -131,6 +140,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 390d989..9c9ef3d 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -2,7 +2,12 @@ String $version = '1.6.0', String $python_version = '3.13', ) { - ensure_packages(['gcc', 'openldap-devel',]) + $auth_type = lookup('metrix::auth_type') + + if $auth_type == 'saml2' { + ensure_packages(['libffi-devel', 'xmlsec1', 'xmlsec1-openssl']) + } + ensure_packages(['gcc', 'openldap-devel']) file { '/var/www/metrix/': ensure => 'directory', @@ -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 3f53df7..aff2582 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 a33595a0cd207f0e71e2ba364845682359a094b9 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Wed, 8 Apr 2026 11:39:12 -0400 Subject: [PATCH 2/4] update to metrix 1.7.0 which removes need to fix cffi --- manifests/install.pp | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/manifests/install.pp b/manifests/install.pp index 9c9ef3d..d5a1a6e 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,5 +1,5 @@ class metrix::install ( - String $version = '1.6.0', + String $version = '1.7.0', String $python_version = '3.13', ) { $auth_type = lookup('metrix::auth_type') @@ -108,17 +108,6 @@ 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 From af04b1b80695dc7f7f8e7274108423fba69840c2 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 10 Apr 2026 13:16:40 -0400 Subject: [PATCH 3/4] add configurable extra attributes to request, to access and to get staff privilege --- manifests/init.pp | 38 ++++++++++++++++++++++---------------- templates/99-local.py.epp | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/manifests/init.pp b/manifests/init.pp index a49bd16..4d6fec0 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -15,6 +15,9 @@ 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[String] $saml2_extra_required_attributes = [], + Array[Hash[String, String]] $saml2_staff_attributes = [], + Array[Hash[String, String]] $saml2_required_access_attributes = [], Optional[String] $ssl_private_key = undef, Optional[String] $ssl_public_cert = undef, Optional[String] $idp_metadata = undef, @@ -25,22 +28,25 @@ show_diff => false, content => epp('metrix/99-local.py', { - '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, - '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, + '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, + '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, + 'saml2_extra_required_attributes' => $saml2_extra_required_attributes, + 'saml2_staff_attributes' => $saml2_staff_attributes, + 'saml2_required_access_attributes' => $saml2_required_access_attributes, } ), owner => 'apache', diff --git a/templates/99-local.py.epp b/templates/99-local.py.epp index aff2582..fc3933e 100644 --- a/templates/99-local.py.epp +++ b/templates/99-local.py.epp @@ -71,6 +71,27 @@ 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' +SAML_CONFIG['service']['sp']['required_attributes'] += [ +<% $saml2_extra_required_attributes.each |$attribute| { -%> + '<%= $attribute %>', +<% } -%> +] +SAML_CONFIG['required_access_attributes'] = [ +<% $saml2_required_access_attributes.each |$pair| { -%> +<% $pair.each |$attribute, $value| { -%> + ('<%= $attribute %>', '<%= $value %>'), +<% } -%> +<% } -%> +] + +SAML_CONFIG['staff_attributes'] = [ +<% $saml2_staff_attributes.each |$pair| { -%> +<% $pair.each |$attribute, $value| { -%> + ('<%= $attribute %>', '<%= $value %>'), +<% } -%> +<% } -%> +] + <% } %> From fcebcedf7101cdd19bac6071fbd5e1fc96512df3 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 10 Apr 2026 17:09:03 -0400 Subject: [PATCH 4/4] generalized attributes logic to make it applicable wider than SAML2 --- manifests/init.pp | 8 ++++---- templates/99-local.py.epp | 33 ++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/manifests/init.pp b/manifests/init.pp index 4d6fec0..e6bad4b 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -16,8 +16,8 @@ String $ssl_public_cert_file = '/etc/ssl/metrix.public.cert', Enum['ldap', 'saml2'] $auth_type = 'ldap', Array[String] $saml2_extra_required_attributes = [], - Array[Hash[String, String]] $saml2_staff_attributes = [], - Array[Hash[String, String]] $saml2_required_access_attributes = [], + Array[Hash[String, String]] $staff_attributes = [], + Array[Hash[String, String]] $required_access_attributes = [], Optional[String] $ssl_private_key = undef, Optional[String] $ssl_public_cert = undef, Optional[String] $idp_metadata = undef, @@ -45,8 +45,8 @@ 'ssl_key_file' => $ssl_private_key_file, 'ssl_cert_file' => $ssl_public_cert_file, 'saml2_extra_required_attributes' => $saml2_extra_required_attributes, - 'saml2_staff_attributes' => $saml2_staff_attributes, - 'saml2_required_access_attributes' => $saml2_required_access_attributes, + 'staff_attributes' => $staff_attributes, + 'required_access_attributes' => $required_access_attributes, } ), owner => 'apache', diff --git a/templates/99-local.py.epp b/templates/99-local.py.epp index fc3933e..31085da 100644 --- a/templates/99-local.py.epp +++ b/templates/99-local.py.epp @@ -56,6 +56,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 %>'), +<% } -%> +<% } -%> +] + <% if $auth_type == 'ldap' { %> AUTHENTICATION_BACKENDS += ['django_auth_ldap.backend.LDAPBackend'] @@ -76,21 +92,8 @@ SAML_CONFIG['service']['sp']['required_attributes'] += [ '<%= $attribute %>', <% } -%> ] -SAML_CONFIG['required_access_attributes'] = [ -<% $saml2_required_access_attributes.each |$pair| { -%> -<% $pair.each |$attribute, $value| { -%> - ('<%= $attribute %>', '<%= $value %>'), -<% } -%> -<% } -%> -] - -SAML_CONFIG['staff_attributes'] = [ -<% $saml2_staff_attributes.each |$pair| { -%> -<% $pair.each |$attribute, $value| { -%> - ('<%= $attribute %>', '<%= $value %>'), -<% } -%> -<% } -%> -] +SAML_CONFIG['required_access_attributes'] = REQUIRED_ACCESS_ATTRIBUTES +SAML_CONFIG['staff_attributes'] = STAFF_ATTRIBUTES <% } %>