mirror of
https://codeberg.org/icewind/haze.git
synced 2026-10-01 16:54:08 +02:00
feat(services): Add Authentik to provide SAML
Signed-off-by: Louis Chmn <louis@chmn.me>
This commit is contained in:
parent
872b2e3bac
commit
c8d6cb170a
11 changed files with 622 additions and 3 deletions
|
|
@ -82,6 +82,7 @@ Additionally, you can use the following options when starting an instance:
|
||||||
- `s3mb`: enable multi-bucket S3 setup.
|
- `s3mb`: enable multi-bucket S3 setup.
|
||||||
- `s3m`: enable multi-instance S3 setup.
|
- `s3m`: enable multi-instance S3 setup.
|
||||||
- `ldap`: set up an LDAP server.
|
- `ldap`: set up an LDAP server.
|
||||||
|
- `saml`: set up authentik as a SAML IDP.
|
||||||
- `office`: set up a Nextcloud Office server.
|
- `office`: set up a Nextcloud Office server.
|
||||||
- `onlyoffice` setup an onlyoffice document server.
|
- `onlyoffice` setup an onlyoffice document server.
|
||||||
- `push` set up [client push](https://github.com/nextcloud/notify_push).
|
- `push` set up [client push](https://github.com/nextcloud/notify_push).
|
||||||
|
|
|
||||||
96
blueprints/authentik-saml.yaml
Normal file
96
blueprints/authentik-saml.yaml
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||||
|
version: 1
|
||||||
|
metadata:
|
||||||
|
name: haze-nextcloud-saml
|
||||||
|
entries:
|
||||||
|
- model: authentik_crypto.certificatekeypair
|
||||||
|
id: haze-saml-cert
|
||||||
|
identifiers:
|
||||||
|
name: haze-saml
|
||||||
|
attrs:
|
||||||
|
certificate_data: !File /haze/authentik/public.crt
|
||||||
|
key_data: !File /haze/authentik/private.key
|
||||||
|
|
||||||
|
# Prefix SAML users with 'saml-' so that they don't collide with local users.
|
||||||
|
- model: authentik_providers_saml.samlpropertymapping
|
||||||
|
id: haze-nextcloud-uid
|
||||||
|
identifiers:
|
||||||
|
name: haze-nextcloud-uid
|
||||||
|
attrs:
|
||||||
|
saml_name: http://haze.test/nextcloud/uid
|
||||||
|
expression: 'return "saml-" + request.user.username'
|
||||||
|
|
||||||
|
- model: authentik_providers_saml.samlprovider
|
||||||
|
id: nextcloud-provider
|
||||||
|
identifiers:
|
||||||
|
name: nextcloud
|
||||||
|
attrs:
|
||||||
|
acs_url:
|
||||||
|
!Format [
|
||||||
|
"%s/index.php/apps/user_saml/saml/acs",
|
||||||
|
!File /haze/authentik/nextcloud-url,
|
||||||
|
]
|
||||||
|
audience:
|
||||||
|
!Format [
|
||||||
|
"%s/index.php/apps/user_saml/saml/metadata",
|
||||||
|
!File /haze/authentik/nextcloud-url,
|
||||||
|
]
|
||||||
|
sls_url:
|
||||||
|
!Format [
|
||||||
|
"%s/index.php/apps/user_saml/saml/sls",
|
||||||
|
!File /haze/authentik/nextcloud-url,
|
||||||
|
]
|
||||||
|
sls_binding: redirect
|
||||||
|
sp_binding: post
|
||||||
|
issuer_override:
|
||||||
|
!Format [
|
||||||
|
"%s/application/saml/nextcloud-saml/metadata/",
|
||||||
|
!File /haze/authentik/authentik-url,
|
||||||
|
]
|
||||||
|
default_name_id_policy: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||||
|
assertion_valid_not_before: minutes=-5
|
||||||
|
assertion_valid_not_on_or_after: minutes=5
|
||||||
|
session_valid_not_on_or_after: minutes=86400
|
||||||
|
digest_algorithm: http://www.w3.org/2001/04/xmlenc#sha256
|
||||||
|
signature_algorithm: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
|
||||||
|
sign_assertion: true
|
||||||
|
signing_kp: !KeyOf haze-saml-cert
|
||||||
|
authorization_flow:
|
||||||
|
!Find [
|
||||||
|
authentik_flows.flow,
|
||||||
|
[slug, default-provider-authorization-implicit-consent],
|
||||||
|
]
|
||||||
|
invalidation_flow:
|
||||||
|
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||||
|
name_id_mapping: !KeyOf haze-nextcloud-uid
|
||||||
|
property_mappings:
|
||||||
|
- !KeyOf haze-nextcloud-uid
|
||||||
|
- !Find [
|
||||||
|
authentik_providers_saml.samlpropertymapping,
|
||||||
|
[managed, goauthentik.io/providers/saml/username],
|
||||||
|
]
|
||||||
|
- !Find [
|
||||||
|
authentik_providers_saml.samlpropertymapping,
|
||||||
|
[managed, goauthentik.io/providers/saml/email],
|
||||||
|
]
|
||||||
|
- !Find [
|
||||||
|
authentik_providers_saml.samlpropertymapping,
|
||||||
|
[managed, goauthentik.io/providers/saml/name],
|
||||||
|
]
|
||||||
|
- !Find [
|
||||||
|
authentik_providers_saml.samlpropertymapping,
|
||||||
|
[managed, goauthentik.io/providers/saml/uid],
|
||||||
|
]
|
||||||
|
- !Find [
|
||||||
|
authentik_providers_saml.samlpropertymapping,
|
||||||
|
[managed, goauthentik.io/providers/saml/groups],
|
||||||
|
]
|
||||||
|
|
||||||
|
- model: authentik_core.application
|
||||||
|
identifiers:
|
||||||
|
slug: nextcloud-saml
|
||||||
|
attrs:
|
||||||
|
name: Nextcloud with SAML
|
||||||
|
provider: !KeyOf nextcloud-provider
|
||||||
|
meta_launch_url: !File /haze/authentik/nextcloud-url
|
||||||
|
meta_description: Nextcloud instance provisioned by haze
|
||||||
51
blueprints/authentik.yaml
Normal file
51
blueprints/authentik.yaml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||||
|
version: 1
|
||||||
|
metadata:
|
||||||
|
name: haze-nextcloud
|
||||||
|
entries:
|
||||||
|
- model: authentik_core.group
|
||||||
|
id: authentik-group1
|
||||||
|
identifiers:
|
||||||
|
name: authentik-group1
|
||||||
|
- model: authentik_core.group
|
||||||
|
id: authentik-group2
|
||||||
|
identifiers:
|
||||||
|
name: authentik-group2
|
||||||
|
- model: authentik_core.group
|
||||||
|
id: authentik-group3
|
||||||
|
identifiers:
|
||||||
|
name: authentik-group3
|
||||||
|
|
||||||
|
- model: authentik_core.user
|
||||||
|
id: alice
|
||||||
|
identifiers:
|
||||||
|
username: alice
|
||||||
|
attrs:
|
||||||
|
name: Authentik Alice
|
||||||
|
email: alice@haze.test
|
||||||
|
password: password
|
||||||
|
type: internal
|
||||||
|
groups:
|
||||||
|
- !KeyOf authentik-group1
|
||||||
|
- model: authentik_core.user
|
||||||
|
id: bob
|
||||||
|
identifiers:
|
||||||
|
username: bob
|
||||||
|
attrs:
|
||||||
|
name: Authentik Bob
|
||||||
|
email: bob@haze.test
|
||||||
|
password: password
|
||||||
|
type: internal
|
||||||
|
groups:
|
||||||
|
- !KeyOf authentik-group2
|
||||||
|
- model: authentik_core.user
|
||||||
|
id: charlie
|
||||||
|
identifiers:
|
||||||
|
username: charlie
|
||||||
|
attrs:
|
||||||
|
name: Authentik Charlie
|
||||||
|
email: charlie@haze.test
|
||||||
|
password: password
|
||||||
|
type: internal
|
||||||
|
groups:
|
||||||
|
- !KeyOf authentik-group3
|
||||||
28
certificates/authentik/private.key
Normal file
28
certificates/authentik/private.key
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkyLkZjXOf8974
|
||||||
|
xHp2bSFRxebTUhpPLqRKb7qLFeb7Qb192y29SfO/IHjRxQ59wwJcsZa0m88EU7J/
|
||||||
|
otAExK8YMZpBSH+3itzh8rPy1A6soBJNiWPT4iKzB736sBEOy8rI0lCkP58YFX6Z
|
||||||
|
RCaeVEbxO5WHuH8htVedstY64XVG7BFHbqsoo48u2KcdgqN98XlKWb7O1ql2rnOB
|
||||||
|
TL0zs61L3WFgCW546gP+Lh7gsklWC4wnDmC3bVqm8uV9HpEbQw5sjIqMeGmy+Myu
|
||||||
|
rYPWeWJJHCCrmGktTCOvPfTKvShuFeK+tvwGzkiosORiq/bm69phzy1EDIMlEFCl
|
||||||
|
3EYpat93AgMBAAECggEAUe7j6kqk9SFC+ppm8b9tU8V88hHetwRP+Br5u/JV+RRE
|
||||||
|
7fEvGvFMWvoAWP0MKYfvArviXUcjddlP5ZrEp7pL/VGci11K863+CfKtes3pxfeJ
|
||||||
|
YjgwBMhpzG7LDXzB3oOB/rxkEGb56fW2DusN8Kei5otj3CnmPJJ4UBb94iT8NRia
|
||||||
|
4Yd5rP4mcBlGoXOAIzNtCMBmSVbfN1Uj/lM6O3lOQbg0UiY5yoW5H9QK2FAmdQqh
|
||||||
|
YzOzX3fGuX2ZOQKMU1t0xdjemcRwBtBtvp0hfwm/P8sE8EjgKNDzKvnEMmAIvxkV
|
||||||
|
8HiRjlZRK1TeVSDa9Pb2rMGWtebGipQ/C91r5ExDAQKBgQDdh8BYTmc8sqmEsZxj
|
||||||
|
kHFHjbi/qiNqbb34DF3RVLStkhM99COal2rtXGA/y6OmTF/7mh8G2Ywf71cChEMN
|
||||||
|
yXsViZZ7wcx+LidUQoW3L7LV28cCeD02oF8TCGVD1GIHyy2ohJ/Vy8vGHJHUiArS
|
||||||
|
hCFDiSRhMH6c4dhAXs1twsWBQQKBgQC+bJd38gqEzz3N0idhW6Go2aNgc5UXC6um
|
||||||
|
79M3XO79gJPeLLrq/Nr1+EJ3hRfyvJvRTVa/vGNPSlPbIHxRStaJVb36KA6gR1Ui
|
||||||
|
g+vXLWd8r98xD5CttKdViPBMLsbjneFqd5GMNjPtzVOzXyMtG410pJg6ve0s2N9G
|
||||||
|
yFW5q4L6twKBgCCu95TPtHGDFnmKTr1twRjCcwBsFJ+OI1nmUS0iJyn4hDg+vcYA
|
||||||
|
EvmECHtBCxrs57hSK8Ox8vd/M0Iey1nMYQlzbC1EEWyIWKsYyWuWcPcWXs0hej6F
|
||||||
|
+KDxOyd/vRrTQiA7uO0tDRpkeqt1iss2TUYOhLyGEBgLRgFxOzO3abZBAoGAYdTc
|
||||||
|
hM0fRlhKwmGDxesTxPH7k+QN5sciKyPvefQO/MKANZb5eRzrSY+AZnNEeHsZ+pAn
|
||||||
|
T150Dxp6toucEw/F5MzeS5Uk3oeHX7IzClvTXSXmHwiGJhg4GCPAgQNPP0Wvt8ky
|
||||||
|
R7zZNQVWSUNJiTUsmY6ufw9wuKe7HlxyXm+VXUUCgYAKDF0ogAnxO7OzXKQcf7Kj
|
||||||
|
xi54Y+gYUXGVCtMPxcZsa7hdH4HtIto1o02HlxdZk6vPWjYSY4dnO42Q4Ck6cIjN
|
||||||
|
/m0/NMrEq0cmfmCrzOD/jACQLQE8typfUHT9lLQa2jz6JWBfY9lsdmmxhcf1RQex
|
||||||
|
ICm/Hp4iwTAo2aFE0OvhRQ==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
19
certificates/authentik/public.crt
Normal file
19
certificates/authentik/public.crt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDHTCCAgWgAwIBAgIUUmoOy7JNxHn51Xvxf9dBS+wCPb4wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwHjEcMBoGA1UEAwwTaGF6ZS1hdXRoZW50aWstc2FtbDAeFw0yNjA4MjIyMDIy
|
||||||
|
MTZaFw00NjA4MTcyMDIyMTZaMB4xHDAaBgNVBAMME2hhemUtYXV0aGVudGlrLXNh
|
||||||
|
bWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkyLkZjXOf8974xHp2
|
||||||
|
bSFRxebTUhpPLqRKb7qLFeb7Qb192y29SfO/IHjRxQ59wwJcsZa0m88EU7J/otAE
|
||||||
|
xK8YMZpBSH+3itzh8rPy1A6soBJNiWPT4iKzB736sBEOy8rI0lCkP58YFX6ZRCae
|
||||||
|
VEbxO5WHuH8htVedstY64XVG7BFHbqsoo48u2KcdgqN98XlKWb7O1ql2rnOBTL0z
|
||||||
|
s61L3WFgCW546gP+Lh7gsklWC4wnDmC3bVqm8uV9HpEbQw5sjIqMeGmy+MyurYPW
|
||||||
|
eWJJHCCrmGktTCOvPfTKvShuFeK+tvwGzkiosORiq/bm69phzy1EDIMlEFCl3EYp
|
||||||
|
at93AgMBAAGjUzBRMB0GA1UdDgQWBBRZT+y86UNIVosbPK1Zg1hx9vRJUzAfBgNV
|
||||||
|
HSMEGDAWgBRZT+y86UNIVosbPK1Zg1hx9vRJUzAPBgNVHRMBAf8EBTADAQH/MA0G
|
||||||
|
CSqGSIb3DQEBCwUAA4IBAQCSwwVwaPl/WK0oItWXXQmYFeNYeByGiHttKPYIDW0g
|
||||||
|
KBk74iZKVvIfZlm3r8Z8xhiAwCxGeFLPrOU3PUD5AcPlLurCdJqSWCYau5njXChw
|
||||||
|
jDgZVXJiVJ61QwCcULPa9TFsvoQY8/r+9UnJFqFtSNeU562WBIOavdSLKLv/UVed
|
||||||
|
SVAzqTALYlNmMkMXZGmvNC5pNscC2ekPcA8IvNiqNG/p1Vc9OWkUGstbhswRNy7E
|
||||||
|
gNxZ74RSCY0NFUhTFMuP2fpHEnh8krXkwb9P7kfwFnLxp3Z+EdsW6f7P5O6o85r7
|
||||||
|
BJbJAd113khrUFgnR8eRcruKp6WBqbO6jnXypCQEdAUE
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
|
|
||||||
extraPaths = [
|
extraPaths = [
|
||||||
./certificates
|
./certificates
|
||||||
|
./blueprints
|
||||||
];
|
];
|
||||||
|
|
||||||
withOverlays = [
|
withOverlays = [
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
inherit (lib) getExe;
|
inherit (lib) getExe;
|
||||||
inherit (lib.sources) sourceByRegex;
|
inherit (lib.sources) sourceByRegex;
|
||||||
inherit (builtins) fromTOML readFile;
|
inherit (builtins) fromTOML readFile;
|
||||||
src = sourceByRegex ../. ["Cargo.*" "(src|certificates)(/.*)?"];
|
src = sourceByRegex ../. ["Cargo.*" "(src|certificates|blueprints)(/.*)?"];
|
||||||
version = (fromTOML (readFile ../Cargo.toml)).package.version;
|
version = (fromTOML (readFile ../Cargo.toml)).package.version;
|
||||||
in
|
in
|
||||||
rustPlatform.buildRustPackage {
|
rustPlatform.buildRustPackage {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use crate::mapping::{Mapping, for_config};
|
||||||
use crate::php::PhpVersion;
|
use crate::php::PhpVersion;
|
||||||
use crate::service::Service;
|
use crate::service::Service;
|
||||||
use crate::service::ServiceTrait;
|
use crate::service::ServiceTrait;
|
||||||
|
use crate::service::deduplicate_services;
|
||||||
use crate::sources::download_nc;
|
use crate::sources::download_nc;
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use bollard::config::NetworkCreateRequest;
|
use bollard::config::NetworkCreateRequest;
|
||||||
|
|
@ -243,7 +244,7 @@ impl CloudOptions {
|
||||||
php: php
|
php: php
|
||||||
.or_else(|| get_max_php_version(&config.sources_root))
|
.or_else(|| get_max_php_version(&config.sources_root))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
services,
|
services: deduplicate_services(services),
|
||||||
app_packages: app_package,
|
app_packages: app_package,
|
||||||
mappings: vec![],
|
mappings: vec![],
|
||||||
version,
|
version,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod authentik;
|
||||||
mod clam;
|
mod clam;
|
||||||
mod dav;
|
mod dav;
|
||||||
mod imaginary;
|
mod imaginary;
|
||||||
|
|
@ -18,6 +19,7 @@ mod webhook;
|
||||||
|
|
||||||
use crate::cloud::CloudOptions;
|
use crate::cloud::CloudOptions;
|
||||||
use crate::config::{HazeConfig, Preset, ProxyConfig};
|
use crate::config::{HazeConfig, Preset, ProxyConfig};
|
||||||
|
pub use crate::service::authentik::{Authentik, AuthentikSaml};
|
||||||
pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket};
|
pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket};
|
||||||
use crate::service::dav::Dav;
|
use crate::service::dav::Dav;
|
||||||
use crate::service::imaginary::Imaginary;
|
use crate::service::imaginary::Imaginary;
|
||||||
|
|
@ -35,8 +37,8 @@ use crate::service::sftp::{Sftp, SftpKey};
|
||||||
use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard};
|
use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard};
|
||||||
use crate::service::smb::Smb;
|
use crate::service::smb::Smb;
|
||||||
use crate::service::webhook::Webhook;
|
use crate::service::webhook::Webhook;
|
||||||
use bollard::models::ContainerState;
|
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
|
use bollard::models::ContainerState;
|
||||||
use enum_dispatch::enum_dispatch;
|
use enum_dispatch::enum_dispatch;
|
||||||
use miette::{IntoDiagnostic, Report, Result, WrapErr};
|
use miette::{IntoDiagnostic, Report, Result, WrapErr};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
@ -314,6 +316,10 @@ pub enum ServiceType {
|
||||||
Webhook,
|
Webhook,
|
||||||
/// Enable DB partitioning for mariadb
|
/// Enable DB partitioning for mariadb
|
||||||
Partitioning,
|
Partitioning,
|
||||||
|
/// Authentik identity provider
|
||||||
|
Authentik,
|
||||||
|
/// Configure Authentik as a SAML IDP for Nextcloud
|
||||||
|
Saml,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
|
|
@ -347,6 +353,8 @@ pub enum Service {
|
||||||
RedisTls(RedisTls),
|
RedisTls(RedisTls),
|
||||||
FrankenPhp(FrankenPhp),
|
FrankenPhp(FrankenPhp),
|
||||||
Webhook(Webhook),
|
Webhook(Webhook),
|
||||||
|
Authentik(Authentik),
|
||||||
|
AuthentikSaml(AuthentikSaml),
|
||||||
Preset(PresetService),
|
Preset(PresetService),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,6 +401,11 @@ impl Service {
|
||||||
ServiceType::RedisTls => Some(vec![Service::RedisTls(RedisTls)]),
|
ServiceType::RedisTls => Some(vec![Service::RedisTls(RedisTls)]),
|
||||||
ServiceType::FrankenPhp => Some(vec![Service::FrankenPhp(FrankenPhp)]),
|
ServiceType::FrankenPhp => Some(vec![Service::FrankenPhp(FrankenPhp)]),
|
||||||
ServiceType::Webhook => Some(vec![Service::Webhook(Webhook)]),
|
ServiceType::Webhook => Some(vec![Service::Webhook(Webhook)]),
|
||||||
|
ServiceType::Authentik => Some(vec![Service::Authentik(Authentik)]),
|
||||||
|
ServiceType::Saml => Some(vec![
|
||||||
|
Service::Authentik(Authentik),
|
||||||
|
Service::AuthentikSaml(AuthentikSaml),
|
||||||
|
]),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
presets
|
presets
|
||||||
|
|
@ -421,6 +434,18 @@ impl Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove duplicate services.
|
||||||
|
// Useful for Authentik as it is needed by saml and oidc.
|
||||||
|
pub fn deduplicate_services(services: impl IntoIterator<Item = Service>) -> Vec<Service> {
|
||||||
|
let mut collected = Vec::new();
|
||||||
|
for service in services {
|
||||||
|
if !collected.contains(&service) {
|
||||||
|
collected.push(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collected
|
||||||
|
}
|
||||||
|
|
||||||
fn get_preset<'a>(presets: &'a [Preset], name: &str) -> Option<&'a Preset> {
|
fn get_preset<'a>(presets: &'a [Preset], name: &str) -> Option<&'a Preset> {
|
||||||
presets.iter().find(|preset| preset.name == name)
|
presets.iter().find(|preset| preset.name == name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
276
src/service/authentik/mod.rs
Normal file
276
src/service/authentik/mod.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
mod saml;
|
||||||
|
|
||||||
|
pub use saml::AuthentikSaml;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
use crate::cloud::CloudOptions;
|
||||||
|
use crate::config::{HazeConfig, ProxyConfig};
|
||||||
|
use crate::image::pull_image;
|
||||||
|
use crate::service::ServiceTrait;
|
||||||
|
use bollard::Docker;
|
||||||
|
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
|
||||||
|
use bollard::query_parameters::CreateContainerOptions;
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use maplit::hashmap;
|
||||||
|
use miette::{IntoDiagnostic, Report, WrapErr};
|
||||||
|
use std::fs::{create_dir_all, write};
|
||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
|
const AUTHENTIK_IMAGE: &str = "ghcr.io/goauthentik/server:2026.8.0";
|
||||||
|
const POSTGRES_IMAGE: &str = "docker.io/library/postgres:16-alpine";
|
||||||
|
|
||||||
|
pub(super) const AUTHENTIK_PORT: u16 = 9000;
|
||||||
|
|
||||||
|
const AUTHENTIK_TOKEN: &str = "haze";
|
||||||
|
|
||||||
|
const BLUEPRINT: &str = include_str!("../../../blueprints/authentik.yaml");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Authentik;
|
||||||
|
|
||||||
|
pub(super) async fn container_ip(
|
||||||
|
docker: &Docker,
|
||||||
|
container: &str,
|
||||||
|
network: Option<&str>,
|
||||||
|
) -> Result<IpAddr> {
|
||||||
|
let networks = docker
|
||||||
|
.inspect_container(container, None)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?
|
||||||
|
.network_settings
|
||||||
|
.and_then(|settings| settings.networks)
|
||||||
|
.ok_or_else(|| Report::msg(format!("{container} is not connected to any network")))?;
|
||||||
|
|
||||||
|
let endpoint = match network {
|
||||||
|
Some(network) => networks.get(network).cloned(),
|
||||||
|
None => networks.values().next().cloned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
endpoint
|
||||||
|
.and_then(|endpoint| endpoint.ip_address)
|
||||||
|
.ok_or_else(|| Report::msg(format!("{container} has no ip")))?
|
||||||
|
.parse()
|
||||||
|
.into_diagnostic()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write_file(path: &Utf8PathBuf, filename: &str, content: &str) -> Result<()> {
|
||||||
|
create_dir_all(path)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err_with(|| format!("Failed to create {path}"))?;
|
||||||
|
|
||||||
|
write(path.join(filename), content)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err_with(|| format!("Failed to write {filename}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn container_name(cloud_id: &str, role: &str) -> String {
|
||||||
|
if role == "server" {
|
||||||
|
format!("{cloud_id}-authentik")
|
||||||
|
} else {
|
||||||
|
format!("{cloud_id}-authentik-{role}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_name(role: &str) -> String {
|
||||||
|
if role == "server" {
|
||||||
|
"authentik".into()
|
||||||
|
} else {
|
||||||
|
format!("authentik-{role}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_authentik(
|
||||||
|
docker: &Docker,
|
||||||
|
config: &HazeConfig,
|
||||||
|
cloud_id: &str,
|
||||||
|
network: &str,
|
||||||
|
role: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let domain_name = domain_name(role);
|
||||||
|
let name = container_name(cloud_id, role);
|
||||||
|
|
||||||
|
let haze_directory = config.work_dir.join(cloud_id).join("authentik");
|
||||||
|
let blueprints_directory = haze_directory.join("blueprints");
|
||||||
|
write_file(&blueprints_directory, "nextcloud.yaml", BLUEPRINT)?;
|
||||||
|
|
||||||
|
let mut env = vec![
|
||||||
|
"AUTHENTIK_POSTGRESQL__HOST=authentik-db".to_string(),
|
||||||
|
"AUTHENTIK_POSTGRESQL__NAME=authentik".to_string(),
|
||||||
|
"AUTHENTIK_POSTGRESQL__USER=authentik".to_string(),
|
||||||
|
"AUTHENTIK_POSTGRESQL__PASSWORD=authentik".to_string(),
|
||||||
|
"AUTHENTIK_SECRET_KEY=authentik-secret".to_string(),
|
||||||
|
"AUTHENTIK_LOG_LEVEL=warning".to_string(),
|
||||||
|
"AUTHENTIK_BOOTSTRAP_PASSWORD=password".to_string(),
|
||||||
|
"AUTHENTIK_BOOTSTRAP_EMAIL=admin@haze.test".to_string(),
|
||||||
|
format!("AUTHENTIK_BOOTSTRAP_TOKEN={AUTHENTIK_TOKEN}"),
|
||||||
|
];
|
||||||
|
|
||||||
|
if !config.proxy.address.is_empty() {
|
||||||
|
let url = config.proxy.addr_with_port(
|
||||||
|
&container_name(cloud_id, "server"),
|
||||||
|
IpAddr::V4(Ipv4Addr::LOCALHOST), // ip arg is unused when a proxy is configured
|
||||||
|
AUTHENTIK_PORT,
|
||||||
|
);
|
||||||
|
env.push(format!("AUTHENTIK_WEB__BASE_URL={url}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = docker
|
||||||
|
.create_container(
|
||||||
|
Some(CreateContainerOptions {
|
||||||
|
name: Some(name.to_string()),
|
||||||
|
..CreateContainerOptions::default()
|
||||||
|
}),
|
||||||
|
ContainerCreateBody {
|
||||||
|
image: Some(AUTHENTIK_IMAGE.into()),
|
||||||
|
cmd: Some(vec![role.into()]),
|
||||||
|
env: Some(env),
|
||||||
|
host_config: Some(HostConfig {
|
||||||
|
network_mode: Some(network.to_string()),
|
||||||
|
shm_size: Some(512 * 1024 * 1024),
|
||||||
|
binds: Some(vec![
|
||||||
|
format!("{haze_directory}:/haze/authentik:ro"),
|
||||||
|
format!("{blueprints_directory}:/blueprints/custom:ro"),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
labels: Some(hashmap! {
|
||||||
|
"haze-type".to_string() => domain_name.to_string(),
|
||||||
|
"haze-cloud-id".to_string() => cloud_id.to_string(),
|
||||||
|
}),
|
||||||
|
networking_config: Some(NetworkingConfig {
|
||||||
|
endpoints_config: Some(hashmap! {
|
||||||
|
network.to_string() => EndpointSettings {
|
||||||
|
aliases: Some(vec![domain_name.to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err_with(|| format!("Failed to create {name}"))?
|
||||||
|
.id;
|
||||||
|
|
||||||
|
docker
|
||||||
|
.start_container(&id, None)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err_with(|| format!("Failed to start {name}"))?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_postgres(docker: &Docker, cloud_id: &str, network: &str) -> Result<String> {
|
||||||
|
let domain_name = domain_name("db");
|
||||||
|
let name = container_name(cloud_id, "db");
|
||||||
|
|
||||||
|
let id = docker
|
||||||
|
.create_container(
|
||||||
|
Some(CreateContainerOptions {
|
||||||
|
name: Some(name.to_string()),
|
||||||
|
..CreateContainerOptions::default()
|
||||||
|
}),
|
||||||
|
ContainerCreateBody {
|
||||||
|
image: Some(POSTGRES_IMAGE.into()),
|
||||||
|
env: Some(vec![
|
||||||
|
"POSTGRES_DB=authentik".into(),
|
||||||
|
"POSTGRES_USER=authentik".into(),
|
||||||
|
"POSTGRES_PASSWORD=authentik".into(),
|
||||||
|
]),
|
||||||
|
host_config: Some(HostConfig {
|
||||||
|
network_mode: Some(network.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
labels: Some(hashmap! {
|
||||||
|
"haze-type".to_string() => domain_name.to_string(),
|
||||||
|
"haze-cloud-id".to_string() => cloud_id.to_string(),
|
||||||
|
}),
|
||||||
|
networking_config: Some(NetworkingConfig {
|
||||||
|
endpoints_config: Some(hashmap! {
|
||||||
|
network.to_string() => EndpointSettings {
|
||||||
|
aliases: Some(vec![domain_name.to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err_with(|| format!("Failed to create {name}"))?
|
||||||
|
.id;
|
||||||
|
|
||||||
|
docker
|
||||||
|
.start_container(&id, None)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err_with(|| format!("Failed to start {name}"))?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ServiceTrait for Authentik {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"authentik"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn(
|
||||||
|
&self,
|
||||||
|
docker: &Docker,
|
||||||
|
cloud_id: &str,
|
||||||
|
network: &str,
|
||||||
|
config: &HazeConfig,
|
||||||
|
_options: &CloudOptions,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
pull_image(docker, POSTGRES_IMAGE).await?;
|
||||||
|
pull_image(docker, AUTHENTIK_IMAGE).await?;
|
||||||
|
|
||||||
|
Ok(vec![
|
||||||
|
spawn_postgres(docker, cloud_id, network).await?,
|
||||||
|
spawn_authentik(docker, config, cloud_id, network, "worker").await?,
|
||||||
|
spawn_authentik(docker, config, cloud_id, network, "server").await?,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_name(&self, cloud_id: &str) -> Option<String> {
|
||||||
|
Some(container_name(cloud_id, "server"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_port(&self) -> u16 {
|
||||||
|
AUTHENTIK_PORT
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_healthy(
|
||||||
|
&self,
|
||||||
|
docker: &Docker,
|
||||||
|
cloud_id: &str,
|
||||||
|
_options: &CloudOptions,
|
||||||
|
) -> Result<bool> {
|
||||||
|
// Authentik takes over a minute to boot, so only wait for the container.
|
||||||
|
self.is_running(docker, cloud_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_message(
|
||||||
|
&self,
|
||||||
|
docker: &Docker,
|
||||||
|
cloud_id: &str,
|
||||||
|
proxy: &ProxyConfig,
|
||||||
|
) -> Result<Option<String>> {
|
||||||
|
let container = self.container_name(cloud_id).unwrap();
|
||||||
|
let ip = container_ip(docker, &container, None).await?;
|
||||||
|
let addr = proxy.addr_with_port(&container, ip, self.proxy_port());
|
||||||
|
|
||||||
|
Ok(Some(format!(
|
||||||
|
r#"
|
||||||
|
Authentik running at: {addr} - It takes a few minute to finishes starting.
|
||||||
|
Admin login: 'akadmin' with password 'password'
|
||||||
|
Authentik users: 'alice', 'bob' and 'charlie' with password 'password'
|
||||||
|
Authentik groups: 'authentik-group1', 'authentik-group2' and 'authentik-group3'
|
||||||
|
"#,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/service/authentik/saml.rs
Normal file
121
src/service/authentik/saml.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
use super::{AUTHENTIK_PORT, container_ip, container_name, write_file};
|
||||||
|
use crate::Result;
|
||||||
|
use crate::cloud::CloudOptions;
|
||||||
|
use crate::config::{HazeConfig, ProxyConfig};
|
||||||
|
use crate::service::{ServiceTrait, split_cmnd};
|
||||||
|
use bollard::Docker;
|
||||||
|
|
||||||
|
const SIGNING_CERT: &str = include_str!("../../../certificates/authentik/public.crt");
|
||||||
|
const SIGNING_KEY: &str = include_str!("../../../certificates/authentik/private.key");
|
||||||
|
const BLUEPRINT: &str = include_str!("../../../blueprints/authentik-saml.yaml");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct AuthentikSaml;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ServiceTrait for AuthentikSaml {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"saml"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn(
|
||||||
|
&self,
|
||||||
|
_docker: &Docker,
|
||||||
|
cloud_id: &str,
|
||||||
|
_network: &str,
|
||||||
|
config: &HazeConfig,
|
||||||
|
_options: &CloudOptions,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
let haze_directory = config.work_dir.join(cloud_id).join("authentik");
|
||||||
|
write_file(&haze_directory, "public.crt", SIGNING_CERT)?;
|
||||||
|
write_file(&haze_directory, "private.key", SIGNING_KEY)?;
|
||||||
|
|
||||||
|
let blueprints_directory = config
|
||||||
|
.work_dir
|
||||||
|
.join(cloud_id)
|
||||||
|
.join("authentik")
|
||||||
|
.join("blueprints");
|
||||||
|
write_file(&blueprints_directory, "saml.yaml", BLUEPRINT)?;
|
||||||
|
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apps(&self) -> &'static [&'static str] {
|
||||||
|
&["user_saml"]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_setup(
|
||||||
|
&self,
|
||||||
|
docker: &Docker,
|
||||||
|
cloud_id: &str,
|
||||||
|
config: &HazeConfig,
|
||||||
|
) -> Result<Vec<Vec<String>>> {
|
||||||
|
let authentik_container = container_name(cloud_id, "server");
|
||||||
|
|
||||||
|
let authentik_url = config.proxy.addr_with_port(
|
||||||
|
&authentik_container,
|
||||||
|
container_ip(docker, &authentik_container, None).await?,
|
||||||
|
AUTHENTIK_PORT,
|
||||||
|
);
|
||||||
|
let nextcloud_url = config.proxy.addr(
|
||||||
|
cloud_id,
|
||||||
|
container_ip(docker, cloud_id, Some("haze")).await?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let haze_directory = config.work_dir.join(cloud_id).join("authentik");
|
||||||
|
write_file(&haze_directory, "authentik-url", &authentik_url)?;
|
||||||
|
write_file(&haze_directory, "nextcloud-url", &nextcloud_url)?;
|
||||||
|
|
||||||
|
Ok(vec![
|
||||||
|
split_cmnd("occ config:app:set --silent user_saml type --value saml"),
|
||||||
|
split_cmnd(
|
||||||
|
"occ config:app:set --silent user_saml general-allow_multiple_user_back_ends --value 1",
|
||||||
|
),
|
||||||
|
split_cmnd(
|
||||||
|
"occ config:app:set --silent user_saml general-require_provisioned_account --value 0",
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
"occ".into(),
|
||||||
|
"config:app:set".into(),
|
||||||
|
"--silent".into(),
|
||||||
|
"user_saml".into(),
|
||||||
|
"directLoginName".into(),
|
||||||
|
"--value".into(),
|
||||||
|
"Local login".into(),
|
||||||
|
],
|
||||||
|
split_cmnd("occ saml:config:create"),
|
||||||
|
vec![
|
||||||
|
"occ".into(),
|
||||||
|
"saml:config:set".into(),
|
||||||
|
"--silent".into(),
|
||||||
|
"1".into(),
|
||||||
|
"--general-idp0_display_name=Authentik SAML".into(),
|
||||||
|
format!("--general-uid_mapping=http://haze.test/nextcloud/uid"),
|
||||||
|
format!("--idp-entityId={authentik_url}/application/saml/nextcloud-saml/metadata/"),
|
||||||
|
format!("--idp-singleSignOnService.url={authentik_url}/application/saml/nextcloud-saml/"),
|
||||||
|
format!("--idp-singleLogoutService.url={authentik_url}/application/saml/nextcloud-saml/"),
|
||||||
|
format!("--idp-x509cert={}", SIGNING_CERT.trim()),
|
||||||
|
"--saml-attribute-mapping-displayName_mapping=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name".into(),
|
||||||
|
"--saml-attribute-mapping-email_mapping=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress".into(),
|
||||||
|
"--saml-attribute-mapping-group_mapping=http://schemas.xmlsoap.org/claims/Group".into(),
|
||||||
|
"--security-wantAssertionsSigned=1".into(),
|
||||||
|
],
|
||||||
|
split_cmnd("occ saml:config:validate --silent"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_message(
|
||||||
|
&self,
|
||||||
|
_docker: &Docker,
|
||||||
|
_cloud_id: &str,
|
||||||
|
proxy: &ProxyConfig,
|
||||||
|
) -> Result<Option<String>> {
|
||||||
|
if !proxy.https {
|
||||||
|
Ok(Some(
|
||||||
|
"WARNING: Nextcloud does not support SAML login in non secure setups".into(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(Some("".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue