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