more work on ldap bootstrapping

This commit is contained in:
eyjhb 2025-02-02 00:12:38 +01:00
parent 19cd1b3255
commit ae3c110e18
Signed by: eyjhb
GPG key ID: 609F508E3239F920
10 changed files with 362 additions and 11 deletions

View file

@ -5,7 +5,7 @@ import gql
from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation
from gql.transport.aiohttp import AIOHTTPTransport
from .utils import to_camelcase, to_snakecase
from .utils import to_camelcase, to_snakecase, get_value
import logging
@ -100,6 +100,7 @@ class LLDAPGroups:
def update(self, groupId: int, attrs: dict[str, str | list[str]]):
insertAttributes: list[dict[str, str | list[str]]] = []
for k, v in attrs.items():
v = get_value(v)
if isinstance(v, str):
v = [v]

View file

@ -0,0 +1,148 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) types;
cfg = config.mine.lldap_provision;
# helpers
_configFile = {
user_attributes = lib.mapAttrsToList (n: v: v) cfg.user_attributes;
group_attributes = lib.mapAttrsToList (n: v: v) cfg.group_attributes;
users = lib.mapAttrsToList (n: v: v // {
user_id = if v ? user_id then v.user_id else n;
}) cfg.users;
groups = lib.mapAttrsToList (n: v: v // {
display_name = if v ? display_name then v.display_name else n;
}) cfg.groups;
};
configFile = (pkgs.formats.json {}).generate "lldap-declarative.json" _configFile;
# opts
optsAttributes = { name, config, ... }: {
options = {
name = lib.mkOption {
type = types.str;
default = name;
description = "The name of the attribute";
};
attributeType = lib.mkOption {
type = types.enum [ "STRING" "INTEGER" "JPEG_PHOTO" "DATE_TIME" ];
description = "Type of the attribute";
};
isList = lib.mkOption {
type = types.bool;
default = false;
description = "Is this attribute a list (multiple values for this attribute)";
};
isEditable = lib.mkOption {
type = types.bool;
default = false;
description = "Should the user be able to edit this value?";
};
isVisible = lib.mkOption {
type = types.bool;
default = false;
description = "Should the user be able to see this value?";
};
};
};
in {
options = {
mine.lldap_provision = {
enable = lib.mkEnableOption "LLDAP declarative setup";
url = lib.mkOption {
type = types.str;
default = config.services.lldap.settings.http_url;
description = "URL for the LLDAP instance";
};
username = lib.mkOption {
type = types.str;
description = "Username to use when signing into lldap";
};
passwordFile = lib.mkOption {
type = types.path;
description = "Path for the password file to authenticate the user";
};
group_attributes = lib.mkOption {
type = types.attrsOf (types.submodule optsAttributes);
default = {};
};
user_attributes = lib.mkOption {
type = types.attrsOf (types.submodule optsAttributes);
default = {};
};
users = lib.mkOption {
type = types.attrsOf types.anything;
default = {};
example = {
user1 = {
password = "env:LLDAP_USER1_PASSWORD";
mail = "something@something.dk";
foo = "value for user attribute foo";
bar = "value for user attribute bar";
groups = [ "group1" "group2" ];
};
user2 = { user_id = "superuserawesome"; };
};
};
groups = lib.mkOption {
type = types.attrsOf types.anything;
default = {};
example = {
base_member = {
foo = "value for group attribute foo";
bar = "value for group attribute bar";
};
system = {
display_name = "system_service - override display_name";
};
testgroup = {};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.lldapsetup = {
description = "setup lldap declaratively";
wantedBy = [ config.systemd.services.lldap.name "multi-user.target" ];
after = [ config.systemd.services.lldap.name ];
environment = {
LLDAP_URL = cfg.url;
LLDAP_USERNAME = cfg.username;
LLDAP_PASSWORD = "file:${cfg.passwordFile}";
};
path = with pkgs; [
lldap
];
script = let
pythonEnv = pkgs.python3.withPackages(ps: with ps; [ gql aiohttp requests ]);
pythonDir = pkgs.runCommand "lldap-bootstrap" {} ''
mkdir -p $out/bootstrap
cp -a ${./.}/. $out/bootstrap
'';
in ''
cd ${pythonDir}
${pythonEnv}/bin/python -m bootstrap.main ${configFile}
'';
};
};
}

View file

@ -3,8 +3,10 @@
from typing import Any
import subprocess
import requests
import secrets
import json
import sys
import os
import gql
@ -26,9 +28,19 @@ logger.setLevel(logging.DEBUG)
class LLDAP:
def __init__(self, server_url: str, auth_token: str):
def __init__(
self,
server_url: str,
username: str,
password: str,
):
if password.startswith("file:"):
password = open(password[5:], "r").read().strip()
self._server_url: str = server_url
self._server_auth_token: str = auth_token
self._server_refresh_token: str | None = None
self._simple_login(username, password)
self._client: gql.Client = self._init_gql_client()
@ -37,6 +49,35 @@ class LLDAP:
self._attrsUser = LLDAPAttributes(self._client, using_user_attributes=True)
self._attrsGroup = LLDAPAttributes(self._client, using_group_attributes=True)
def _simple_login(self, username: str, password: str):
r = requests.post(
f"{self._server_url}/auth/simple/login",
headers={"content-type": "application/json"},
data=json.dumps(
{
"username": username,
"password": password,
}
),
)
if r.status_code != 200:
raise Exception(f"failed to signin got response: {r.text}")
rjson = r.json()
self._server_auth_token = rjson["token"]
self._server_refresh_token = rjson["refreshToken"]
def _logout(self):
if not self._server_refresh_token:
return
r = requests.get(
f"{self._server_url}/auth/logout",
headers={"refresh-token": self._server_refresh_token},
)
print(r.text, r.status_code)
def _init_gql_client(self) -> gql.Client:
# Select your transport with a defined url endpoint
transport = AIOHTTPTransport(
@ -269,8 +310,8 @@ class LLDAP:
]
)
def run(self):
data = json.load(open("test2.json", "r"))
def run(self, provision_file_path: str):
data = json.load(open(provision_file_path, "r"))
self._run_ensure_attrs_user(self._attrsUser, data["user_attributes"])
self._run_ensure_attrs_user(self._attrsGroup, data["group_attributes"])
@ -279,9 +320,26 @@ class LLDAP:
if __name__ == "__main__":
auth_token = os.getenv("LLDAP_TOKEN")
if not auth_token:
raise Exception("No LLDAP_TOKEN provided. please set")
url = os.getenv("LLDAP_URL")
if not url:
raise Exception("No LLDAP_URL provided. please set")
x = LLDAP("https://ldap.fricloud.dk", auth_token)
x.run()
auth_username = os.getenv("LLDAP_USERNAME")
if not auth_username:
raise Exception("No LLDAP_USERNAME provided. please set")
auth_password = os.getenv("LLDAP_PASSWORD")
if not auth_password:
raise Exception("No LLDAP_PASSWORD provided. please set")
if len(sys.argv) != 2:
raise Exception(
"Please provide a JSON file containing the provisioning details"
)
x = LLDAP(url, auth_username, auth_password)
try:
x.run(sys.argv[1])
finally:
x._logout()

View file

@ -5,7 +5,7 @@ import gql
from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation
from gql.transport.aiohttp import AIOHTTPTransport
from .utils import to_camelcase, to_snakecase
from .utils import to_camelcase, to_snakecase, get_value
import logging
@ -91,6 +91,7 @@ class LLDAPUsers:
def update(self, userId: str, attrs: dict[str, str | list[str]]):
insertAttributes: list[dict[str, str | list[str]]] = []
for k, v in attrs.items():
v = get_value(v)
if isinstance(v, str):
v = [v]

View file

@ -1,4 +1,8 @@
import logging
import re
import os
logger = logging.getLogger(__name__)
def to_camelcase(s):
@ -8,3 +12,21 @@ def to_camelcase(s):
def to_snakecase(s):
return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
def get_value(s):
if not isinstance(s, str):
return s
if s.startswith("file:"):
filepath = s[len("file:") :]
logger.debug(f"reading from file '{filepath}'")
return open(filepath, "r").read().strip()
if s.startswith("env:"):
envkey = s.strip()[len("env:") :]
logger.debug(f"reading from envvar '{envkey}'")
return os.getenv(envkey)
logger.debug(f"using original value")
return s

View file

@ -21,6 +21,10 @@ index 6f42473..b3746a1 100644
pkgLLDAPCli = pkgs.callPackage ./../../../../shared/pkgs/lldap-cli.nix {};
in {
imports = [
# ./test.nix
];
environment.systemPackages = [
pkgLLDAPCli
];
@ -102,6 +106,8 @@ in {
groups = {
admin = "lldap_admin";
member = "base_member";
system = "system_service";
system_email = "system_email";
};
ou = {
@ -116,6 +122,10 @@ in {
email = "mail";
avatar = "jpegPhoto";
groupname = "cn";
# custom
member_email = "member_email";
mail_disk_quota = "mail_disk_quota";
};
age_secret = config.age.secrets.lldap-bind-user-pass.path;

View file

@ -0,0 +1,98 @@
{ config, lib, ... }:
let
mkEmail = name: "${name}@${config.mine.shared.settings.domain}";
mkUserNormal = name: {
user_id = name;
member_email = mkEmail name;
mail = "env:EMAIL_${lib.toUpper name}";
groups = [ "base_member" ];
mail_disk_quota = 100*1024*1024; # mb
};
mkUserSystem = name: password_file: {
user_id = name;
member_email = mkEmail name;
password = "file:${password_file}";
# TODO: remove base_member in the future, or have
# more granular controls for emails and shit
groups = [ "base_member" "system_service" ];
mail_disk_quota = 10*1024*1024; # mb
};
mkUserAdmin = name: {
user_id = name;
member_email = mkEmail name;
groups = [ "base_member" "lldap_admin" ];
mail_disk_quota = 100*1024*1024; # mb
};
in {
imports = [
./bootstrap/lldap-state-module.nix
];
mine.lldap_provision = {
enable = true;
url = config.mine.shared.meta.lldap.url;
username = "admin";
passwordFile = config.age.secrets.lldap-admin-user-pass.path;
# username = "testusername";
# passwordFile = ./test.txt;
group_attributes = {
group_foo = {
attributeType = "STRING";
isEditable = true;
isVisible = true;
};
};
user_attributes = {
member_email = {
attributeType = "STRING";
isEditable = false;
isVisible = true;
};
mail_disk_quota = {
attributeType = "INTEGER";
};
};
groups = let
gs = [
"base_member"
"system_service"
"system_email"
];
in lib.listToAttrs (lib.forEach gs (v: lib.nameValuePair v { display_name = v; }));
users = {
# normal users
testusername = {
member_email = "env:USER1_EMAIL";
};
user1 = mkUserNormal "thief420";
# admin users
admin = mkUserAdmin "admin";
eyjhb = mkUserAdmin "eyjhb";
rasmus = mkUserAdmin "rasmus";
# system users
authelia = mkUserSystem "authelia" config.age.secrets.authelia-smtp-password.path;
wger = mkUserSystem "wger" config.age.secrets.wger-ldap-pass.path;
# bind user
bind_user = {
groups = [ "lldap_password_manager" "lldap_strict_readonly" ];
};
};
};
systemd.services.lldapsetup.environment = {
USER1_EMAIL = "eyjhbbbbbbb@fricloud.dk";
EMAIL_THIEF420 = "someemail@gmail.com";
};
}

View file

@ -41,6 +41,7 @@
# wger
wger-env.file = ./wger/env.age;
wger-ldap-pass.file = ./wger/ldap-pass.age;
# restic
restic-env.file = ./restic/env.age;

View file

@ -51,6 +51,7 @@ in
# wger
"wger/env.age".publicKeys = defaultAccess;
"wger/ldap-pass.age".publicKeys = defaultAccess;
# restic
"restic/env.age".publicKeys = defaultAccess;

View file

@ -0,0 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg uYPdHX2B6acTsvvU49DtkE6seTek1FT/+WIeexfKIDI
koEouYTBn6/VencZc4HmooytCR7zcdrSL/77ScL47yQ
-> X25519 sTTQGj2XPszrqQrUSiXVYlbQsxGYLn9Ee46isVezMQI
1obJbY9FMAmCPlPWdaJdCjYm9Z01WdiGv72Dy9NTZNM
-> ssh-ed25519 n8n9DQ 5+yZMCzJNRvpLYWDOof40LSVX31DdawJfKzjPhwiUy8
bUUVYjDaJ3kvB/gc6wAjLX090YGG4VigulNwc3kioMo
-> ssh-ed25519 BTp6UA 2WxuEBEe12Bx0hJaLmfrJhN5HKLZIQtpzekTprvTdTc
LdxlDEXOGsYgBB8p+qj/Twv7F1RK6W3DXiM+cwBvU+o
--- RdzYVsyAfuRcvj9nz+f57KbfNV+MG5EkIzDmBbzlT1A
—ù6T®ñ…ŠMåò*ÞÂtqƒr³eIù/µØ`q¦aÜ<>Áýû-p0ÕJb0ðQ³$~qÁzɃôŽ(