more work on ldap bootstrapping
This commit is contained in:
parent
19cd1b3255
commit
ae3c110e18
10 changed files with 362 additions and 11 deletions
|
@ -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]
|
||||
|
||||
|
|
148
machines/gerd/services/lldap/bootstrap/lldap-state-module.nix
Normal file
148
machines/gerd/services/lldap/bootstrap/lldap-state-module.nix
Normal 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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue