diff --git a/hosts/bekkalokk/services/gitea/import-users/default.nix b/hosts/bekkalokk/services/gitea/import-users/default.nix index cae6283..609ef2e 100644 --- a/hosts/bekkalokk/services/gitea/import-users/default.nix +++ b/hosts/bekkalokk/services/gitea/import-users/default.nix @@ -14,6 +14,9 @@ in preStart=''${pkgs.rsync}/bin/rsync -e "${pkgs.openssh}/bin/ssh -o UserKnownHostsFile=$CREDENTIALS_DIRECTORY/ssh-known-hosts -i $CREDENTIALS_DIRECTORY/sshkey" -a pvv@smtp.pvv.ntnu.no:/etc/passwd /tmp/passwd-import''; serviceConfig = { ExecStart = pkgs.writers.writePython3 "gitea-import-users" { + flakeIgnore = [ + "E501" # Line over 80 chars lol + ]; libraries = with pkgs.python3Packages; [ requests ]; } (builtins.readFile ./gitea-import-users.py); LoadCredential=[ diff --git a/hosts/bekkalokk/services/gitea/import-users/gitea-import-users.py b/hosts/bekkalokk/services/gitea/import-users/gitea-import-users.py index 7211c64..de851ef 100644 --- a/hosts/bekkalokk/services/gitea/import-users/gitea-import-users.py +++ b/hosts/bekkalokk/services/gitea/import-users/gitea-import-users.py @@ -2,18 +2,92 @@ import requests import secrets import os + EMAIL_DOMAIN = os.getenv('EMAIL_DOMAIN') if EMAIL_DOMAIN is None: EMAIL_DOMAIN = 'pvv.ntnu.no' + API_TOKEN = os.getenv('API_TOKEN') if API_TOKEN is None: raise Exception('API_TOKEN not set') + GITEA_API_URL = os.getenv('GITEA_API_URL') if GITEA_API_URL is None: GITEA_API_URL = 'https://git.pvv.ntnu.no/api/v1' + +def gitea_list_all_users() -> dict[str, dict[str, any]] | None: + r = requests.get( + GITEA_API_URL + '/admin/users', + headers={'Authorization': 'token ' + API_TOKEN} + ) + + if r.status_code != 200: + print('Failed to get users:', r.text) + return None + + return {user['login']: user for user in r.json()} + + +def gitea_create_user(username: str, userdata: dict[str, any]) -> bool: + r = requests.post( + GITEA_API_URL + '/admin/users', + json=userdata, + headers={'Authorization': 'token ' + API_TOKEN}, + ) + + if r.status_code != 201: + print(f'ERR: Failed to create user {username}:', r.text) + return False + + return True + + +def gitea_edit_user(username: str, userdata: dict[str, any]) -> bool: + r = requests.patch( + GITEA_API_URL + f'/admin/users/{username}', + json=userdata, + headers={'Authorization': 'token ' + API_TOKEN}, + ) + + if r.status_code != 200: + print(f'ERR: Failed to update user {username}:', r.text) + return False + + return True + + +def gitea_list_teams_for_organization(org: str) -> dict[str, any] | None: + r = requests.get( + GITEA_API_URL + f'/orgs/{org}/teams', + headers={'Authorization': 'token ' + API_TOKEN}, + ) + + if r.status_code != 200: + print(f"ERR: Failed to list teams for {org}:", r.text) + return None + + return {team['name']: team for team in r.json()} + + +def gitea_add_user_to_organization_team(username: str, team_id: int) -> bool: + r = requests.put( + GITEA_API_URL + f'/teams/{team_id}/members/{username}', + headers={'Authorization': 'token ' + API_TOKEN}, + ) + + if r.status_code != 204: + print(f'ERR: Failed to add user {username} to org team {team_id}:', r.text) + return False + + return True + + +# If a passwd user has one of the following shells, +# it is most likely not a PVV user, but rather a system user. +# Users with these shells should thus be ignored. BANNED_SHELLS = [ "/usr/bin/nologin", "/usr/sbin/nologin", @@ -22,59 +96,11 @@ BANNED_SHELLS = [ "/bin/msgsh", ] -existing_users = {} - -# This function should only ever be called when adding users -# from the passwd file -def add_user(username, name): - user = { - "full_name": name, - "username": username, - "login_name": username, - "source_id": 1, # 1 = SMTP - } - - if username not in existing_users: - user["password"] = secrets.token_urlsafe(32) - user["must_change_password"] = False - user["visibility"] = "private" - user["email"] = username + '@' + EMAIL_DOMAIN - - r = requests.post(GITEA_API_URL + '/admin/users', json=user, - headers={'Authorization': 'token ' + API_TOKEN}) - if r.status_code != 201: - print('ERR: Failed to create user ' + username + ': ' + r.text) - return - - print('Created user ' + username) - existing_users[username] = user - - else: - user["visibility"] = existing_users[username]["visibility"] - r = requests.patch(GITEA_API_URL + f'/admin/users/{username}', - json=user, - headers={'Authorization': 'token ' + API_TOKEN}) - if r.status_code != 200: - print('ERR: Failed to update user ' + username + ': ' + r.text) - return - - print('Updated user ' + username) - - -def main(): - # Fetch existing users - r = requests.get(GITEA_API_URL + '/admin/users', - headers={'Authorization': 'token ' + API_TOKEN}) - - if r.status_code != 200: - raise Exception('Failed to get users: ' + r.text) - - for user in r.json(): - existing_users[user['login']] = user - - # Read the file, add each user - with open("/tmp/passwd-import", 'r') as f: +# Reads out a passwd-file line for line, and filters out +# real PVV users (as opposed to system users meant for daemons and such) +def passwd_file_parser(passwd_path): + with open(passwd_path, 'r') as f: for line in f.readlines(): uid = int(line.split(':')[2]) if uid < 1000: @@ -86,8 +112,85 @@ def main(): username = line.split(':')[0] name = line.split(':')[4].split(',')[0] + yield (username, name) - add_user(username, name) +# This function either creates a new user in gitea +# and fills it out with some default information if +# it does not exist, or ensures that the default information +# is correct if the user already exists. All user information +# (including non-default fields) is pulled from gitea and added +# to the `existing_users` dict +def add_or_patch_gitea_user( + username: str, + name: str, + existing_users: dict[str, dict[str, any]], +) -> None: + user = { + "full_name": name, + "username": username, + "login_name": username, + "source_id": 1, # 1 = SMTP + } + + if username not in existing_users: + user["password"] = secrets.token_urlsafe(32) + user["must_change_password"] = False + user["visibility"] = "private" + user["email"] = username + '@' + EMAIL_DOMAIN + + if not gitea_create_user(username, user): + return + + print('Created user', username) + existing_users[username] = user + + else: + user["visibility"] = existing_users[username]["visibility"] + + if not gitea_edit_user(username, user): + return + + print('Updated user', username) + + +# This function adds a user to a gitea team (part of organization) +# if the user is not already part of said team. +def ensure_gitea_user_is_part_of_team( + username: str, + org: str, + team_name: str, +) -> None: + teams = gitea_list_teams_for_organization(org) + + if teams is None: + return + + if team_name not in teams: + print(f'ERR: could not find team "{team_name}" in organization "{org}"') + + gitea_add_user_to_organization_team(username, teams[team_name]['id']) + + print(f'User {username} is now part of {org}/{team_name}') + + +# List of teams that all users should be part of by default +COMMON_USER_TEAMS = [ + ("Projects", "Members"), + ("Kurs", "Members"), +] + + +def main(): + existing_users = gitea_list_all_users() + if existing_users is None: + exit(1) + + for username, name in passwd_file_parser("/tmp/passwd-import"): + print(f"Processing {username}") + add_or_patch_gitea_user(username, name, existing_users) + for org, team_name in COMMON_USER_TEAMS: + ensure_gitea_user_is_part_of_team(username, org, team_name) + print() if __name__ == '__main__': diff --git a/secrets/bekkalokk/bekkalokk.yaml b/secrets/bekkalokk/bekkalokk.yaml index 5b2680f..c315b7e 100644 --- a/secrets/bekkalokk/bekkalokk.yaml +++ b/secrets/bekkalokk/bekkalokk.yaml @@ -6,7 +6,7 @@ gitea: email-password: ENC[AES256_GCM,data:KRwC+aL1aPvJuXt91Oq1ttATMnFTnuUy,iv:ats8TygB/2pORkaTZzPOLufZ9UmvVAKoRcWNvYF1z6w=,tag:Do0fA+4cZ3+l7JJyu8hjBg==,type:str] passwd-ssh-key: ENC[AES256_GCM,data:L0lF0wvpayss1NU9m3A45cH0bCMQzODTFVrq6EPd1JHx54wIcoaRBYLmxXKXASzBlCg9zlwXMUIk3OQcS3kdzMKL0iqcSL2iicAcKjFIHyrWLqXgwV5pRSP/tRPcVw8KW8gz0bh33EgESs5ReddZ3VZ0Cy1s2YupMRQvBXr89k1+Hv70OWB6P06hvxhv/zKcMGI1N/dWLroMgrQuT9imw4+/Q1RqwzTYeEU+eUn24AM9GjcBg4qf3OI+6g0nXUat/upIYE28iF5J3lbUSmDSmirBLc8xgHLdOyyJPTObWYWYxlSL78T7IqiMm9lI3rtBlpJDDcn/YxZpVqN5bg2154GISNK+uR0TVSLdJ+drdGHIfIX3G78XSxf2L9rbJyRn8MQlgStfdBIQicLavQKVMrmj+XQfvEMez23WbPLjH4oViBQFI+GrOHOGy/f16cz8Sn4n+69OcsOeTxs3tKYdfq6r1XLYSJ/fe/zvxBpaZiyGXljsuyEdIyBL2A8D6uSXe3Nd3/DAdBtceFfIdN1olCdutixzVWgxaJnrel161z5A/4w=,iv:Uy46yY3jFYSvpxrgCHxRMUksnWfhf5DViLMvCXVMMl4=,tag:wFEJ5+icFrOKkc56gY0A5g==,type:str] ssh-known-hosts: ENC[AES256_GCM,data:zlRLoelQeumMxGqPmgMTB69X1RVWXIs2jWwc67lk0wrdNOHUs5UzV5TUA1JnQ43RslBU92+js7DkyvE5enGzw7zZE5F1ZYdGv/eCgvkTMC9BoLfzHzP6OzayPLYEt3xJ5PRocN8JUAD55cuu4LgsuebuydHPi2oWOfpbSUBKSeCh6dvk5Pp1XRDprPS5SzGLW8Xjq98QlzmfGv50meI9CDJZVF9Wq/72gkyfgtb3YVdr,iv:AF06TBitHegfWk6w07CdkHklh4ripQCmA45vswDQgss=,tag:zKh7WVXMJN2o9ZIwIkby3Q==,type:str] - import-user-env: ENC[AES256_GCM,data:vfaqjGEnUM9VtOPvBurz7nFwzGZt3L2EqijrQej4wiOcGCrRA4tN6kBV6NmhHqlFPsw=,iv:viPGkyOOacCWcgTu25da4qH7DC4wz2qdeC1W2WcMUdI=,tag:BllNqGQoaxqUo3lTz9LGnw==,type:str] + import-user-env: ENC[AES256_GCM,data:wArFwTd0ZoB4VXHPpichfnmykxGxN8y2EQsMgOPHv7zsm6A+m2rG9BWDGskQPr5Ns9o=,iv:gPUzYFSNoALJb1N0dsbNlgHIb7+xG7E9ANpmVNZURQ0=,tag:JghfRy2OcDFWKS9zX1XJ9A==,type:str] runners: alpha: ENC[AES256_GCM,data:gARxCufePz+EMVwEwRsL2iZUfh9HUowWqtb7Juz3fImeeAdbt+k3DvL/Nwgegg==,iv:3fEaWd7v7uLGTy2J7EFQGfN0ztI0uCOJRz5Mw8V5UOU=,tag:Aa6LwWeW2hfDz1SqEhUJpA==,type:str] beta: ENC[AES256_GCM,data:DVjS78IKWiWgf+PuijCZKx4ZaEJGhQr7vl+lc7QOg1JlA4p9Kux/tOD8+f2+jA==,iv:tk3Xk7lKWNdZ035+QVIhxXy2iJbHwunI4jRFM4It46E=,tag:9Mr6o//svYEyYhSvzkOXMg==,type:str] @@ -92,8 +92,8 @@ sops: UHpLRkdQTnhkeGlWVG9VS1hkWktyckEKAdwnA9URLYZ50lMtXrU9Q09d0L3Zfsyr 4UsvjjdnFtsXwEZ9ZzOQrpiN0Oz24s3csw5KckDni6kslaloJZsLGg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2024-08-13T19:49:24Z" - mac: ENC[AES256_GCM,data:AeJ53D+8A8mHYRmVHdqhcS1ZTbqVe5gQqJsJjMk4T/ZlNX8/V4M9mqAW2FB9m/JSdj234gDu+PBHcW70ZrCqeVsoUW/ETVgUX3W2gBmBgYJiRETp8I7/eks/5YEV6vIIxQsZNP/9dZTNX4T2wD74ELl23NSTXA/6k2tyzBlTMYo=,iv:DABafHvw+5w0PHCKqLgpwmQnv0uHOTyj+s8gdnHFTZ4=,tag:SNZ7W+6zdyuuv2AB9ir8eg==,type:str] + lastmodified: "2024-08-26T19:38:58Z" + mac: ENC[AES256_GCM,data:3FyfZPmJ7znQEul+IwqN1ZaM53n6os3grquJwJ9vfyDSc2h8UZBhqYG+2uW9Znp9DSIjuhCUI8iqGKRJE0M/6IDICeXms/5+ynVFOS9bA2cdzPvWaj0FFAd2x3g4Vhs47+vRlsnIe/tMiKU3IOvzOfI6KAUHc9L2ySrzH7z2+fo=,iv:1iZSR9qOIEtf+fNbtWSwJBIUEQGKadfHSVOnkFzOwq8=,tag:Sk6JEU1B6Rd1GXLYC6rQtQ==,type:str] pgp: - created_at: "2024-08-04T00:03:28Z" enc: |-