diff --git a/manifests/auth/ldap.pp b/manifests/auth/ldap.pp new file mode 100644 index 0000000..2ece1b2 --- /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_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 new file mode 100644 index 0000000..c2327a3 --- /dev/null +++ b/manifests/auth/saml2.pp @@ -0,0 +1,47 @@ +class metrix::auth::saml2 ( + 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']) + + 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_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 acb6c70..014ca38 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -13,31 +13,51 @@ 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', + 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, + Optional[String] $ssl_public_cert = undef, + 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': + 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, - '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, + '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, + 'staff_attributes' => $staff_attributes, + 'required_access_attributes' => $required_access_attributes, } ), owner => 'apache', @@ -85,7 +105,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 +118,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 01e3d56..7a7bee7 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,17 +1,24 @@ 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',]) + $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, - 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, @@ -20,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, @@ -170,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 85% rename from templates/99-local.py.epp rename to templates/91-local.py.epp index ef5b40c..5cb347d 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,11 +55,24 @@ 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 +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', 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..49827b8 --- /dev/null +++ b/templates/92-local_saml2.py.epp @@ -0,0 +1,15 @@ +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 +SAML_CONFIG['staff_attributes'] = STAFF_ATTRIBUTES