diff --git a/gitlab/manage_users_groups.py b/gitlab/manage_users_groups.py new file mode 100755 index 0000000..a6f5263 --- /dev/null +++ b/gitlab/manage_users_groups.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +import logging + +import click +import yaml + +import gitlab + +logger = logging.getLogger(__name__) + + +@click.command() +@click.option( + "--gitlab", + "-g", + "gitlab_instance", + help="Which GitLab instance to use, as configured in the python-gitlab config", +) +@click.option( + "--do-it", + "do_it", + is_flag=True, + help="Actually perform the operations", +) +@click.argument("config_file") +def cli(gitlab_instance, do_it, config_file): + """Ensure that GitLab users and group memberships match the structure defined in the + configuration file. This uses the python-gitlab configuration parsing.""" + gl = gitlab.Gitlab.from_config(gitlab_instance) + gl.auth() + + with open(config_file, "r") as f: + config = yaml.safe_load(f) + + if not do_it: + logger.info( + "Will not perform any actions, please use --do-it once you're satisfied" + " with the expected actions." + ) + + for group_path, group_conf in config["groups"].items(): + group = gl.groups.get(group_path, with_projects=False) + expected_members = { + username: gitlab.const.AccessLevel[access_level.upper()] + for username, access_level in group_conf["users"].items() + } + recorded_members = set() + + expected_group_shares = { + other_path: gitlab.const.AccessLevel[access_level.upper()] + for other_path, access_level in group_conf.get( + "share_with_groups", {} + ).items() + } + recorded_members = set() + + remove_extra_memberships = group_conf.get("remove_extra_memberships", False) + for member in group.members.list(): + username = member.username + expected_access_level = expected_members.get(member.username) + if expected_access_level and member.access_level != expected_access_level: + logger.info( + "Adjusting membership for %s in %s to %s (was %s)", + username, + group_path, + expected_access_level.name, + member.access_level, + ) + if do_it: + member.access_level = expected_access_level + member.save() + + if remove_extra_memberships and not expected_access_level: + logger.info("Removing member %s from %s", username, group_path) + if do_it: + member.delete() + + recorded_members.add(username) + + for username, access_level in expected_members.items(): + if username in recorded_members: + continue + + users = gl.users.list(username=username) + if not users: + logger.warning( + "User %s not found, cannot add them to %s!", username, group_path + ) + continue + + user_id = users[0].id + + logger.info("Adding member %s in %s at level %s", username, access_level) + if do_it: + group.members.create({"user_id": user_id, "access_level": access_level}) + + recorded_group_shares = set() + + for group_share in group.shared_with_groups: + other_path = group_share["group_full_path"] + other_id = group_share["group_id"] + other_access_level = group_share["group_access_level"] + expected_access_level = expected_group_shares.get(other_path) + if expected_access_level and other_access_level != expected_access_level: + logger.info( + "Adjusting group_share for %s in %s to %s (was %s)", + other_path, + group_path, + expected_access_level, + other_access_level, + ) + if do_it: + group.share(other_id, expected_access_level) + + if remove_extra_memberships and not expected_access_level: + logger.info("Removing group %s from %s", other_path, group_path) + if do_it: + group.unshare(other_id) + + recorded_group_shares.add(other_path) + + for other_path, access_level in expected_group_shares.items(): + if other_path in recorded_group_shares: + continue + + other_group = gl.groups.get(other_path) + if not other_group: + logger.warning( + "Group %s not found, cannot add them to %s!", other_path, group_path + ) + continue + + logger.info( + "Adding group %s in %s at level %s", + other_path, + group_path, + access_level, + ) + if do_it: + group.share(other_group.id, access_level) + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, format="%(asctime)s %(name)s:%(levelname)s %(message)s" + ) + cli() diff --git a/gitlab/users_groups.yml b/gitlab/users_groups.yml new file mode 100644 index 0000000..a7a32ce --- /dev/null +++ b/gitlab/users_groups.yml @@ -0,0 +1,80 @@ +# Owner +# Maintainer +# Developer +# Reporter +# Guest + +groups: + teams/staff: + users: + phabricator-migration: Owner + anlambert: Maintainer + ardumont: Maintainer + bchauvet: Owner + douardda: Maintainer + jayeshv: Maintainer + lunar: Maintainer + marla.dasilva: Maintainer + moranegg: Maintainer + olasd: Maintainer + rdicosmo: Owner + sgranger: Maintainer + vlorentz: Maintainer + vsellier: Maintainer + zack: Owner + remove_extra_memberships: true + teams/developers: + users: + phabricator-migration: Owner + anlambert: Maintainer + ardumont: Maintainer + bchauvet: Maintainer + douardda: Maintainer + jayeshv: Maintainer + lunar: Maintainer + moranegg: Maintainer + olasd: Maintainer + rdicosmo: Maintainer + vlorentz: Maintainer + vsellier: Maintainer + zack: Maintainer + remove_extra_memberships: true + teams/sysadmin: + users: + phabricator-migration: Owner + ardumont: Owner + olasd: Owner + vsellier: Owner + remove_extra_memberships: true + teams/interns: + users: + phabricator-migration: Owner + remove_extra_memberships: true + teams/management: + users: + phabricator-migration: Owner + bchauvet: Owner + douardda: Maintainer + rdicosmo: Owner + vsellier: Maintainer + zack: Owner + remove_extra_memberships: true + infra: + users: + phabricator-migration: Owner + remove_extra_memberships: true + share_with_groups: + teams/sysadmin: Owner + modules: + users: + phabricator-migration: Owner + share_with_groups: + teams/developers: Maintainer + remove_extra_memberships: true + migrated: + users: + phabricator-migration: Owner + share_with_groups: + teams/sysadmin: Owner + teams/developers: Maintainer + remove_extra_memberships: true