diff --git a/machines/gerd/services/lldap/bootstrap/groups.py b/machines/gerd/services/lldap/bootstrap/groups.py index 63e13c8..9868449 100644 --- a/machines/gerd/services/lldap/bootstrap/groups.py +++ b/machines/gerd/services/lldap/bootstrap/groups.py @@ -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] diff --git a/machines/gerd/services/lldap/bootstrap/lldap-state-module.nix b/machines/gerd/services/lldap/bootstrap/lldap-state-module.nix new file mode 100644 index 0000000..2d71647 --- /dev/null +++ b/machines/gerd/services/lldap/bootstrap/lldap-state-module.nix @@ -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} + ''; + }; + }; +} diff --git a/machines/gerd/services/lldap/bootstrap/main.py b/machines/gerd/services/lldap/bootstrap/main.py index f7aa028..4d8cb50 100644 --- a/machines/gerd/services/lldap/bootstrap/main.py +++ b/machines/gerd/services/lldap/bootstrap/main.py @@ -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() diff --git a/machines/gerd/services/lldap/bootstrap/users.py b/machines/gerd/services/lldap/bootstrap/users.py index b52a835..10f1f7b 100644 --- a/machines/gerd/services/lldap/bootstrap/users.py +++ b/machines/gerd/services/lldap/bootstrap/users.py @@ -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] diff --git a/machines/gerd/services/lldap/bootstrap/utils.py b/machines/gerd/services/lldap/bootstrap/utils.py index 32dc0bf..32edcd9 100644 --- a/machines/gerd/services/lldap/bootstrap/utils.py +++ b/machines/gerd/services/lldap/bootstrap/utils.py @@ -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"(? 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 +6TM*tqreI/`qa܍-p0Jb0Q$~qzɃ( \ No newline at end of file