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
|
||||
|
|
|
@ -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;
|
||||
|
|
98
machines/gerd/services/lldap/test.nix
Normal file
98
machines/gerd/services/lldap/test.nix
Normal 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";
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -51,6 +51,7 @@ in
|
|||
|
||||
# wger
|
||||
"wger/env.age".publicKeys = defaultAccess;
|
||||
"wger/ldap-pass.age".publicKeys = defaultAccess;
|
||||
|
||||
# restic
|
||||
"restic/env.age".publicKeys = defaultAccess;
|
||||
|
|
11
secrets/wger/ldap-pass.age
Normal file
11
secrets/wger/ldap-pass.age
Normal 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ɃôŽ(
|
Loading…
Add table
Add a link
Reference in a new issue