From 8bbfd82101e1281d442ee19b13e7dc92f2ac5de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Fri, 3 Jul 2026 12:01:16 -0400 Subject: [PATCH 1/4] Update metrix to 1.7.0 --- manifests/install.pp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manifests/install.pp b/manifests/install.pp index 01e3d56..5cca6a9 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,5 +1,6 @@ class metrix::install ( - String $version = '1.6.0', + String $source_url = "https://github.com/guilbaults/TrailblazingTurtle/archive/refs/tags/v${version}.tar.gz", + String $version = '1.7.0', String $python_version = '3.13', ) { stdlib::ensure_packages(['gcc', 'openldap-devel',]) @@ -11,7 +12,7 @@ } -> 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, From 14a92c4b0be32055c78d08579814eeb3e820623b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Fri, 3 Jul 2026 12:02:19 -0400 Subject: [PATCH 2/4] Add support for SAML2 authentication Co-Authored-By: Maxime Boissonneault --- manifests/auth/ldap.pp | 113 ++++++++++++++++ manifests/auth/saml2.pp | 43 ++++++ manifests/init.pp | 20 ++- manifests/install.pp | 123 ++++-------------- .../{99-local.py.epp => 91-local.py.epp} | 9 +- templates/92-local_ldap.py.epp | 2 + templates/92-local_saml2.py.epp | 13 ++ 7 files changed, 215 insertions(+), 108 deletions(-) create mode 100644 manifests/auth/ldap.pp create mode 100644 manifests/auth/saml2.pp rename templates/{99-local.py.epp => 91-local.py.epp} (93%) 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 new file mode 100644 index 0000000..174f173 --- /dev/null +++ b/manifests/auth/ldap.pp @@ -0,0 +1,113 @@ +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'], + } + + file { '/var/www/metrix/userportal/settings/92-local_auth.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 new file mode 100644 index 0000000..f89d992 --- /dev/null +++ b/manifests/auth/saml2.pp @@ -0,0 +1,43 @@ +class metrix::auth::saml2 ( + String $ssl_private_key, + String $ssl_public_cert, + String $idp_metadata, + Array[String] $extra_required_attributes = [], +) { + 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'], + } + + file { '/var/www/metrix/userportal/settings/92-local_auth.py': + show_diff => false, + content => epp('metrix/92-local_saml2.py', + { + 'extra_required_attributes' => $extra_required_attributes, + } + ), + owner => 'apache', + group => 'apache', + mode => '0600', + require => Class['metrix::install'], + } +} diff --git a/manifests/init.pp b/manifests/init.pp index acb6c70..0869a5b 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -13,14 +13,26 @@ String $cluster_name, String $subdomain, String $slurm_user = 'slurm', + Enum['ldap', 'saml2'] $auth_type = 'ldap', Optional[String] $slurm_db_ip = undef, Optional[Integer] $slurm_db_port = 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': + 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, @@ -85,7 +97,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'], @@ -98,7 +110,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/manifests/install.pp b/manifests/install.pp index 5cca6a9..1c78bca 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -3,12 +3,18 @@ String $version = '1.7.0', String $python_version = '3.13', ) { - stdlib::ensure_packages(['gcc', 'openldap-devel',]) + $auth_type = lookup('metrix::auth_type') + + if $auth_type == 'saml2' { + stdlib::ensure_packages(['libffi-devel', 'xmlsec1', 'xmlsec1-openssl']) + } + 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, @@ -21,97 +27,18 @@ 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, - } # 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 # 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, @@ -171,25 +98,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'], } - -> uv::venv { 'metrix_venv': + + 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'], - ], } + 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()', + 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/91-local.py.epp similarity index 93% rename from templates/99-local.py.epp rename to templates/91-local.py.epp index ef5b40c..a51642c 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 %>', @@ -14,7 +16,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 +43,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 +55,9 @@ STATIC_URL = '/static/' STATIC_ROOT = '/var/www/metrix-static/' AUTHENTICATION_BACKENDS = [ - 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ] -LOGIN_URL = '/accounts/login/' # So it does not use SAML2 - 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..3df9cd6 --- /dev/null +++ b/templates/92-local_saml2.py.epp @@ -0,0 +1,13 @@ +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 %>', +<% } -%> +] From 9250ee43850e0960ca5d7515e39318af9fcea532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Fri, 3 Jul 2026 11:42:02 -0400 Subject: [PATCH 3/4] update SAML config and CSRF_TRUSTED_ORIGINS for BASE_URL with trailing slash Co-Authored-By: Maxime Boissonneault --- templates/91-local.py.epp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/91-local.py.epp b/templates/91-local.py.epp index a51642c..11218d0 100644 --- a/templates/91-local.py.epp +++ b/templates/91-local.py.epp @@ -7,7 +7,7 @@ DEBUG = False BASE_URL = 'https://<%= $subdomain %>.<%= $domain_name %>/' ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] -CSRF_TRUSTED_ORIGINS = [BASE_URL] +CSRF_TRUSTED_ORIGINS = [BASE_URL.strip('/')] AUTH_LDAP_SERVER_URI = 'ldaps://ipa.int.<%= $domain_name %>/' AUTH_LDAP_BIND_DN = 'uid=admin,cn=users,cn=accounts,<%= $base_dn %>', From 8cdcd508864f91fa9f21d7c50cd11343ffdf2604 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 19 Jun 2026 15:38:34 -0400 Subject: [PATCH 4/4] add the actual domain in the ALLOWED_HOSTS --- templates/91-local.py.epp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/91-local.py.epp b/templates/91-local.py.epp index 11218d0..68214ba 100644 --- a/templates/91-local.py.epp +++ b/templates/91-local.py.epp @@ -6,7 +6,7 @@ DEBUG = False BASE_URL = 'https://<%= $subdomain %>.<%= $domain_name %>/' -ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] +ALLOWED_HOSTS = ['<%= $subdomain %>.<%= $domain_name%>', '127.0.0.1', 'localhost'] CSRF_TRUSTED_ORIGINS = [BASE_URL.strip('/')] AUTH_LDAP_SERVER_URI = 'ldaps://ipa.int.<%= $domain_name %>/'