diff --git a/machines/gerd.nix b/machines/gerd.nix index 3fe01d6..f49e3a6 100644 --- a/machines/gerd.nix +++ b/machines/gerd.nix @@ -11,7 +11,7 @@ ./gerd/services/fricloud-website.nix ./gerd/services/member-website - ./gerd/services/lldap + ./gerd/services/lldap.nix ./gerd/services/authelia ./gerd/services/forgejo ./gerd/services/teeworlds.nix diff --git a/machines/gerd/services/lldap/default.nix b/machines/gerd/services/lldap.nix similarity index 100% rename from machines/gerd/services/lldap/default.nix rename to machines/gerd/services/lldap.nix diff --git a/machines/gerd/services/lldap/bootstrap/attributes.py b/machines/gerd/services/lldap/bootstrap/attributes.py deleted file mode 100644 index d7b6790..0000000 --- a/machines/gerd/services/lldap/bootstrap/attributes.py +++ /dev/null @@ -1,171 +0,0 @@ -from typing import Any - -import gql -from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation -from gql.transport.aiohttp import AIOHTTPTransport - -from pprint import pprint -import re - -import logging - - -logger = logging.getLogger(__name__) - - -def to_camelcase(s): - s = re.sub(r"(_|-)+", " ", s).title().replace(" ", "").replace("*", "") - return "".join([s[0].lower(), s[1:]]) - - -def to_snakecase(s): - return re.sub(r"(?" - - -class AttributeValue: - def __init__(self, raw_attribute: dict[str, Any]): - self.name: str = raw_attribute["name"] - - tmpValue = raw_attribute.get("value", []) - if isinstance(tmpValue, str): - tmpValue = [tmpValue] - - self.value: list[str] = tmpValue - - def __repr__(self): - return f"" - - -class LLDAPAttributes: - def __init__( - self, - client: gql.Client, - using_user_attributes: bool = False, - using_group_attributes: bool = False, - ): - self._client = client - - self._using_user_attributes = using_user_attributes - self._using_group_attributes = using_group_attributes - - if self._using_user_attributes and self._using_group_attributes: - raise Exception("can not both use user attributes and group attributes") - - if not self._using_user_attributes and not self._using_group_attributes: - raise Exception("neither user attributes and group attributes specified") - - def list_all(self) -> dict[str, AttributeSchema]: - ds = DSLSchema(self._client.schema) - - if self._using_user_attributes: - querySchema = ds.Schema.userSchema - querySchemaStr = "userSchema" - else: - querySchema = ds.Schema.groupSchema - querySchemaStr = "groupSchema" - - query = dsl_gql( - DSLQuery( - ds.Query.schema().select( - querySchema.select( - ds.AttributeList.attributes.select( - ds.AttributeSchema.name, - ds.AttributeSchema.attributeType, - ds.AttributeSchema.isList, - ds.AttributeSchema.isVisible, - ds.AttributeSchema.isEditable, - ds.AttributeSchema.isReadonly, - ds.AttributeSchema.isHardcoded, - ) - ) - ), - ), - ) - - result = self._client.execute(query) - - attrs: dict[str, AttributeSchema] = {} - for raw_attribute in result["schema"][querySchemaStr]["attributes"]: - attr = AttributeSchema(raw_attribute) - attrs[attr.name] = attr - - return attrs - - def create( - self, - name: str, - attributeType: str, - isList: bool, - isVisible: bool, - isEditable: bool, - ): - logger.debug( - f"adding attribute, name:'{name}' attributeType:'{attributeType}' isList:'{isList}' isVisible:'{isVisible}' isEditable:'{isEditable}'" - ) - - ds = DSLSchema(self._client.schema) - - if self._using_user_attributes: - mutationSchema = ds.Mutation.addUserAttribute - else: - mutationSchema = ds.Mutation.addGroupAttribute - - query = dsl_gql( - DSLMutation( - mutationSchema.args( - name=name, - attributeType=attributeType, - isList=isList, - isVisible=isVisible, - isEditable=isEditable, - ).select(ds.Success.ok) - ), - ) - self._client.execute(query) - - def get(self, name: str) -> AttributeSchema | None: - attrs = self.list_all() - return attrs.get(name) - - def update(self, name: str): - raise Exception("unable to update attribute") - - def delete(self, name: str): - logger.debug(f"deleting attribute '{name}'") - - ds = DSLSchema(self._client.schema) - - if self._using_user_attributes: - mutationSchema = ds.Mutation.deleteUserAttribute - else: - mutationSchema = ds.Mutation.deleteGroupAttribute - - query = dsl_gql( - DSLMutation( - mutationSchema.args( - name=name, - ).select(ds.Success.ok) - ), - ) - self._client.execute(query) diff --git a/machines/gerd/services/lldap/bootstrap/groups.py b/machines/gerd/services/lldap/bootstrap/groups.py deleted file mode 100644 index 63e13c8..0000000 --- a/machines/gerd/services/lldap/bootstrap/groups.py +++ /dev/null @@ -1,136 +0,0 @@ -from typing import Any -import re - -import gql -from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation -from gql.transport.aiohttp import AIOHTTPTransport - -from .utils import to_camelcase, to_snakecase - -import logging - -logger = logging.getLogger(__name__) - - -class Group: - def __init__( - self, - raw_attrs: list[dict[str, str]], - ): - self._attributes: dict[str, Any] = { - item["name"]: item["value"] for item in raw_attrs - } - - self.groupId: int = int(self.__getattr__("groupId")[0]) - self.name: str = self.__getattr__("displayName")[0] - - def _attributes_camelcase(self) -> dict[str, str]: - return {to_camelcase(k): v for k, v in self._attributes.items()} - - def __getattr__(self, key: str): - return self._attributes_camelcase().get(key, "") - - def __repr__(self): - return f"" - - -class LLDAPGroups: - def __init__(self, client: gql.Client): - self._client = client - - def list_all(self) -> dict[str, Group]: - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLQuery( - ds.Query.groups().select( - ds.Group.attributes.select( - ds.AttributeValue.name, - ds.AttributeValue.value, - ds.AttributeValue.schema.select( - ds.AttributeSchema.isHardcoded, - ), - ), - ), - ), - ) - - result = self._client.execute(query) - - groups: dict[str, Group] = {} - for group in result["groups"]: - g = Group( - raw_attrs=group.get("attributes", []), - ) - groups[g.name] = g - - return groups - - def create(self, groupName: str): - logger.debug(f"creating group with name '{groupName}'") - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.createGroup.args( - name=groupName, - ).select(ds.Group.displayName) - ), - ) - self._client.execute(query) - - def get_by_name(self, groupName: str) -> Group | None: - groups = self.list_all() - return groups.get(groupName) - - def get_by_id(self, groupId: int) -> Group | None: - groups = self.list_all() - for group in groups.values(): - if group.groupId == groupId: - return group - - return None - - def name_to_id(self, groupName: str) -> int: - group = self.get_by_name(groupName) - if not group: - raise Exception(f"no group with the name {groupName}") - - return group.groupId - - def update(self, groupId: int, attrs: dict[str, str | list[str]]): - insertAttributes: list[dict[str, str | list[str]]] = [] - for k, v in attrs.items(): - if isinstance(v, str): - v = [v] - - insertAttributes.append({"name": to_snakecase(k), "value": v}) - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.updateGroup.args( - group={ - "id": groupId, - "insertAttributes": insertAttributes, - }, - ).select(ds.Success.ok), - ), - ) - self._client.execute(query) - - def delete(self, groupId: int): - logger.debug(f"deleting group with id '{groupId}'") - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.deleteGroup.args( - groupId=groupId, - ).select(ds.Success.ok), - ), - ) - self._client.execute(query) - - def test(self): - self.list_all() - # self.update("testusername", {"displayName": "Test User Name"}) diff --git a/machines/gerd/services/lldap/bootstrap/main.py b/machines/gerd/services/lldap/bootstrap/main.py deleted file mode 100644 index f7aa028..0000000 --- a/machines/gerd/services/lldap/bootstrap/main.py +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env nix-shell -#!nix-shell --pure --keep LLDAP_TOKEN -i python3 -p "python3.withPackages (ps: with ps; [ requests gql aiohttp])" - -from typing import Any -import subprocess -import secrets -import json -import os - -import gql -from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation -from gql.transport.aiohttp import AIOHTTPTransport -from pprint import pprint - - -from .users import LLDAPUsers -from .groups import LLDAPGroups -from .attributes import LLDAPAttributes - -import logging - -logging.basicConfig() -logging.getLogger("lldapbootstrap").setLevel(logging.DEBUG) -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - - -class LLDAP: - def __init__(self, server_url: str, auth_token: str): - self._server_url: str = server_url - self._server_auth_token: str = auth_token - - self._client: gql.Client = self._init_gql_client() - - self._users = LLDAPUsers(self._client) - self._groups = LLDAPGroups(self._client) - self._attrsUser = LLDAPAttributes(self._client, using_user_attributes=True) - self._attrsGroup = LLDAPAttributes(self._client, using_group_attributes=True) - - def _init_gql_client(self) -> gql.Client: - # Select your transport with a defined url endpoint - transport = AIOHTTPTransport( - url=f"{self._server_url}/api/graphql", - headers={"Authorization": f"Bearer {self._server_auth_token}"}, - ) - - # Create a GraphQL client using the defined transport - client = gql.Client(transport=transport, fetch_schema_from_transport=True) - - # force fetch schema - query = gql.gql( - """ - query { - users { - displayName - } - } - """ - ) - result = client.execute(query) - - return client - - def _run_ensure_attrs_user(self, attrsClass, neededAttrsUser: list[dict[str, Any]]): - dictNeededAttrUser = {v["name"]: v for v in neededAttrsUser} - remoteAttrs = attrsClass.list_all() - - # add needed attributes - for neededAttr in neededAttrsUser: - neededAttrName = neededAttr["name"] - - if neededAttrName in remoteAttrs: - cattr = remoteAttrs[neededAttrName] - if ( - neededAttr["attributeType"] != cattr.attributeType - or neededAttr["isEditable"] != cattr.isEditable - or neededAttr["isList"] != cattr.isList - or neededAttr["isVisible"] != cattr.isVisible - ): - logger.debug( - f"attribute '{neededAttrName}' out of sync, deleting and adding again" - ) - attrsClass.delete(neededAttrName) - else: - continue - - attrsClass.create( - neededAttrName, - attributeType=neededAttr["attributeType"], - isEditable=neededAttr["isEditable"], - isList=neededAttr["isList"], - isVisible=neededAttr["isVisible"], - ) - - # remove unneeded attributes - for remoteAttrName, remoteAttr in remoteAttrs.items(): - # skip hardcoded ones - if remoteAttr.isHardcoded: - continue - - if remoteAttrName not in dictNeededAttrUser: - attrsClass.delete(remoteAttrName) - - def _run_ensure_groups(self, neededGroups: list[dict[str, Any]]): - tmpNeededGroups = {v["display_name"]: v for v in neededGroups} - remoteGroups = self._groups.list_all() - - for neededGroup in neededGroups: - neededGroupDisplay_Name = neededGroup["display_name"] - - if neededGroupDisplay_Name not in remoteGroups: - self._groups.create(neededGroupDisplay_Name) - - # refresh groups - remoteGroups = self._groups.list_all() - - remoteGroup = remoteGroups[neededGroupDisplay_Name] - - # we cannot update the display name, and we never would anyways - del neededGroup["display_name"] - - self._groups.update(remoteGroup.groupId, neededGroup) - - # delete unused groups - for remoteGroupName, remoteGroup in remoteGroups.items(): - # skip all lldap_ groups - if remoteGroupName.startswith("lldap_"): - continue - - if remoteGroupName not in tmpNeededGroups: - self._groups.delete(remoteGroup.groupId) - - def _run_ensure_users( - self, - neededUsers: list[dict[str, Any]], - softDelete: bool = True, - ): - tmpNeededUsers = {v["user_id"]: v for v in neededUsers} - remoteUsers = self._users.list_all() - - for neededUser in neededUsers: - # get required info from dict, and DELETE from dict - # while we're at it. This means that we can safely use - # `neededUser` for updating later - neededUserId = neededUser.pop("user_id") - neededUserGroups = neededUser.pop("groups", []) - neededUserPassword: str | None = neededUser.pop("password", None) - - # create user if needed - if neededUserId not in remoteUsers: - self._users.create( - neededUserId, - neededUser.get("mail", "no-email-specified"), - ) - - # refresh users - remoteUsers = self._users.list_all() - - # update user - self._users.update(neededUserId, neededUser) - - # set correct groups - remoteUser = remoteUsers[neededUserId] - - # print warning about groups attribute - if neededUserGroups: - logger.info( - f"using attribute 'groups' for userId '{neededUserId}', for setting groups, NOT SETTING AS ATTRIBUTE!!" - ) - - # add to correct groups - for groupName in neededUserGroups: - if groupName not in remoteUser.groups: - self._users.add_group( - neededUserId, - self._groups.name_to_id(groupName), - ) - - # remove from unused groups - for groupName, group in remoteUser.groups.items(): - if groupName not in neededUserGroups: - self._users.remove_group( - neededUserId, - self._groups.name_to_id(groupName), - ) - - if neededUserPassword: - logger.info( - f"using attribute 'password' for userId '{neededUserId}', for setting password, NOT SETTING AS ATTRIBUTE!!" - ) - - if neededUserPassword.startswith("file:"): - passwordFile = neededUserPassword[len("file:") :] - logger.debug( - f"reading password from file from file '{passwordFile}' for user '{neededUserId}'" - ) - - self._user_set_password( - neededUserId, - open(passwordFile, "r").read().strip(), - ) - elif neededUserPassword.startswith("env:"): - cleanedPasswordEnv = neededUserPassword.strip()[len("env:") :] - logger.debug( - f"reading password from envvar '{cleanedPasswordEnv}' for user '{neededUserId}'" - ) - - password = os.getenv(cleanedPasswordEnv) - if not password: - raise Exception( - f"could not find env '{cleanedPasswordEnv}' for getting password" - ) - self._user_set_password( - neededUserId, - password.strip(), - ) - else: - logger.debug( - f"using the raw value of password, as the password for user '{neededUserId}'" - ) - self._user_set_password( - neededUserId, - neededUserPassword.strip(), - ) - - # delete unused users - for remoteUserName, remoteUser in remoteUsers.items(): - if remoteUserName not in tmpNeededUsers: - if softDelete: - self._user_disable(remoteUserName) - else: - self._users.delete(remoteUser.userId) - - def _user_disable(self, userId: str, disabled_group_name: str = "disabled"): - user = self._users.get(userId) - if not user: - return - - # remove all groups - for groupName, groupId in user.groups.items(): - if groupName == disabled_group_name: - continue - - self._users.remove_group(userId, groupId) - - # if disabled group is in the users groups, then return - if disabled_group_name in user.groups: - return - - # ensure group exists - groups = self._groups.list_all() - if disabled_group_name not in groups: - self._groups.create(disabled_group_name) - - # add disabled group - self._users.add_group(userId, self._groups.name_to_id(disabled_group_name)) - - # set password to a long string - self._user_set_password(userId, secrets.token_urlsafe(128)) - - def _user_set_password(self, userId: str, password: str): - subprocess.check_output( - [ - "lldap_set_password", - f"--base-url={self._server_url}", - f"--token={self._server_auth_token}", - f"--username={userId}", - f"--password={password}", - ] - ) - - def run(self): - data = json.load(open("test2.json", "r")) - - self._run_ensure_attrs_user(self._attrsUser, data["user_attributes"]) - self._run_ensure_attrs_user(self._attrsGroup, data["group_attributes"]) - self._run_ensure_groups(data["groups"]) - self._run_ensure_users(data["users"]) - - -if __name__ == "__main__": - auth_token = os.getenv("LLDAP_TOKEN") - if not auth_token: - raise Exception("No LLDAP_TOKEN provided. please set") - - x = LLDAP("https://ldap.fricloud.dk", auth_token) - x.run() diff --git a/machines/gerd/services/lldap/bootstrap/users.py b/machines/gerd/services/lldap/bootstrap/users.py deleted file mode 100644 index b52a835..0000000 --- a/machines/gerd/services/lldap/bootstrap/users.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import re - -import gql -from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation -from gql.transport.aiohttp import AIOHTTPTransport - -from .utils import to_camelcase, to_snakecase - -import logging - -logger = logging.getLogger(__name__) - - -class User: - def __init__( - self, - raw_groups: list[dict[str, Any]], - raw_attrs: list[dict[str, str]], - ): - self._attributes: dict[str, Any] = { - item["name"]: item["value"] for item in raw_attrs - } - - self.groups: dict[str, int] = { - info["displayName"]: info["id"] for info in raw_groups - } - self.userId: str = self.__getattr__("userId")[0] - self.email: str = self.__getattr__("mail")[0] - - def _attributes_camelcase(self) -> dict[str, str]: - return {to_camelcase(k): v for k, v in self._attributes.items()} - - def __getattr__(self, key: str): - return self._attributes_camelcase().get(key, "") - - -class LLDAPUsers: - def __init__(self, client: gql.Client): - self._client = client - - def list_all(self) -> dict[str, User]: - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLQuery( - ds.Query.users().select( - ds.User.groups.select( - ds.Group.id, - ds.Group.displayName, - ), - ds.User.attributes.select( - ds.AttributeValue.name, - ds.AttributeValue.value, - ds.AttributeValue.schema.select( - ds.AttributeSchema.isHardcoded, - ), - ), - ), - ), - ) - - result = self._client.execute(query) - - users: dict[str, User] = {} - for user in result["users"]: - u = User( - raw_groups=user.get("groups", []), - raw_attrs=user.get("attributes", []), - ) - users[u.userId] = u - - return users - - def create(self, userId: str, email: str): - logger.debug(f"creating user with name '{userId}' and email '{email}'") - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.createUser.args( - user={"id": userId, "email": email}, - ).select(ds.User.displayName) - ), - ) - self._client.execute(query) - - def get(self, userId: str): - users = self.list_all() - return users.get(userId) - - def update(self, userId: str, attrs: dict[str, str | list[str]]): - insertAttributes: list[dict[str, str | list[str]]] = [] - for k, v in attrs.items(): - if isinstance(v, str): - v = [v] - - insertAttributes.append({"name": to_snakecase(k), "value": v}) - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.updateUser.args( - user={ - "id": userId, - "insertAttributes": insertAttributes, - }, - ).select(ds.Success.ok), - ), - ) - self._client.execute(query) - - def delete(self, userId: str): - logger.debug(f"deleting user with name '{userId}'") - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.deleteUser.args( - userId=userId, - ).select(ds.Success.ok), - ), - ) - self._client.execute(query) - - # groups - def add_group(self, userId: str, groupId: int): - logger.debug(f"adding user '{userId}' to group '{groupId}'") - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.addUserToGroup.args( - userId=userId, - groupId=groupId, - ).select(ds.Success.ok) - ), - ) - - self._client.execute(query) - - def remove_group(self, userId: str, groupId: int): - logger.debug(f"removing user '{userId}' from group '{groupId}'") - - ds = DSLSchema(self._client.schema) - query = dsl_gql( - DSLMutation( - ds.Mutation.removeUserFromGroup.args( - userId=userId, - groupId=groupId, - ).select(ds.Success.ok) - ), - ) - - self._client.execute(query) diff --git a/machines/gerd/services/lldap/bootstrap/utils.py b/machines/gerd/services/lldap/bootstrap/utils.py deleted file mode 100644 index 32dc0bf..0000000 --- a/machines/gerd/services/lldap/bootstrap/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -import re - - -def to_camelcase(s): - s = re.sub(r"(_|-)+", " ", s).title().replace(" ", "").replace("*", "") - return "".join([s[0].lower(), s[1:]]) - - -def to_snakecase(s): - return re.sub(r"(?