|
| 1 | +#!/usr/bin/env python3.8 |
| 2 | + |
| 3 | +import argparse |
| 4 | +from dotenv import load_dotenv |
| 5 | +import requests |
| 6 | +import os |
| 7 | +import time |
| 8 | + |
| 9 | +# Environment variables |
| 10 | +# --------------------- |
| 11 | + |
| 12 | +# Below are expected environment variables defined in .env file beside this script: |
| 13 | +# - GITLAB_URL: the URL to the GitLab instance (e.g. "https://gitlab.com") |
| 14 | +# - ORGANIZATION_NAME: organization name for GitLab (e.g. "Orange-OpenSource") |
| 15 | +# - ROLE_ID_TO_CHANGE: the identifier of the role to change (e.g. "50" for "Owner") |
| 16 | +# - CUSTOM_ROLE_ID_TO_APPLY: the identifier of the new role to apply (e.g. "2004291" for a custom role) |
| 17 | +# - PROTECTED_USERS: the list of users for whom role must not be changed, comma separated (e.g. "bbailleux,lmarie23,pylapersonne-orange,NicolasToussaint") |
| 18 | +# - GITLAB_PRIVATE_TOKEN: private API token for GitLab organization |
| 19 | + |
| 20 | +# See for values of roles doc at https://docs.gitlab.com/development/permissions/predefined_roles/ |
| 21 | +# Get custom identifier for role at https://gitlab.com/groups/{ORG_NAME}/-/settings/roles_and_permissions |
| 22 | + |
| 23 | +load_dotenv() |
| 24 | + |
| 25 | +# Configuration |
| 26 | +# ------------- |
| 27 | + |
| 28 | +GITLAB_URL = os.getenv('GITLAB_URL') |
| 29 | +ORG_NAME = os.getenv('ORGANIZATION_NAME') |
| 30 | +ROLE_ID_TO_CHANGE = int(os.getenv('ROLE_ID_TO_CHANGE'), 10) |
| 31 | +CUSTOM_ROLE_ID_TO_APPLY = int(os.getenv('CUSTOM_ROLE_ID_TO_APPLY', 10)) |
| 32 | +GITLAB_PRIVATE_TOKEN = os.getenv('GITLAB_PRIVATE_TOKEN') |
| 33 | +PROTECTED_USERS = os.getenv('PROTECTED_USERS', '').split(',') |
| 34 | + |
| 35 | +headers = { |
| 36 | + 'Private-Token': GITLAB_PRIVATE_TOKEN |
| 37 | +} |
| 38 | + |
| 39 | +# Environement checks |
| 40 | +# ------------------- |
| 41 | + |
| 42 | +if not GITLAB_URL: |
| 43 | + raise ValueError("💥 Error: The environment variable 'GITLAB_URL' is not set or is empty.") |
| 44 | +if not ORG_NAME: |
| 45 | + raise ValueError("💥 Error: The environment variable 'ORG_NAME' is not set or is empty.") |
| 46 | +if not ROLE_ID_TO_CHANGE: |
| 47 | + raise ValueError("💥 Error: The environment variable 'ROLE_ID_TO_CHANGE' is not set or is empty.") |
| 48 | +if not CUSTOM_ROLE_ID_TO_APPLY: |
| 49 | + raise ValueError("💥 Error: The environment variable 'CUSTOM_ROLE_ID_TO_APPLY' is not set or is empty.") |
| 50 | +if not GITLAB_PRIVATE_TOKEN: |
| 51 | + raise ValueError("💥 Error: The environment variable 'GITLAB_PRIVATE_TOKEN' is not set or is empty.") |
| 52 | +if not PROTECTED_USERS: |
| 53 | + raise ValueError("💥 Error: The environment variable 'PROTECTED_USERS' is not set or is empty.") |
| 54 | +try: |
| 55 | + CUSTOM_ROLE_ID_TO_APPLY = int(CUSTOM_ROLE_ID_TO_APPLY) |
| 56 | +except ValueError: |
| 57 | + raise ValueError("💥 Error: The environment variable 'CUSTOM_ROLE_ID_TO_APPLY' must be an integer.") |
| 58 | +try: |
| 59 | + ROLE_ID_TO_CHANGE = int(ROLE_ID_TO_CHANGE) |
| 60 | +except ValueError: |
| 61 | + raise ValueError("💥 Error: The environment variable 'ROLE_ID_TO_CHANGE' must be an integer.") |
| 62 | + |
| 63 | +# Services API |
| 64 | +# ------------ |
| 65 | + |
| 66 | +# Change roles for groups and projects |
| 67 | +# ------------------------------------ |
| 68 | + |
| 69 | +def change_role(project_id, user_id): |
| 70 | + """ |
| 71 | + Change the role of a member in a project by applying role with id CUSTOM_ROLE_ID_TO_APPLY. |
| 72 | +
|
| 73 | + :param project_id: ID of the project where the role needs to be changed. |
| 74 | + :param user_id: ID of the user whose role needs to be changed. |
| 75 | + :return: True if the change was successful, otherwise False. |
| 76 | + """ |
| 77 | + url = f"{GITLAB_URL}/api/v4/projects/{project_id}/members/{user_id}" |
| 78 | + data = {'access_level': CUSTOM_ROLE_ID_TO_APPLY} |
| 79 | + response = requests.put(url, headers=headers, data=data) |
| 80 | + if response.status_code != 200: |
| 81 | + print(f"❌ Failed to change role for project: {response.status_code} - {response.text}") |
| 82 | + return False |
| 83 | + else: |
| 84 | + return True |
| 85 | + |
| 86 | +def change_group_role(group_id, user_id): |
| 87 | + """ |
| 88 | + Change the role of a member in a group by applying role with id CUSTOM_ROLE_ID_TO_APPLY. |
| 89 | +
|
| 90 | + :param group_id: ID of the group where the role needs to be changed. |
| 91 | + :param user_id: ID of the user whose role needs to be changed. |
| 92 | + :return: True if the change was successful, otherwise False. |
| 93 | + """ |
| 94 | + url = f"{GITLAB_URL}/api/v4/groups/{group_id}/members/{user_id}" |
| 95 | + data = {'access_level': CUSTOM_ROLE_ID_TO_APPLY} |
| 96 | + response = requests.put(url, headers=headers, data=data) |
| 97 | + if response.status_code != 200: |
| 98 | + print(f"❌ Failed to change role for group: {response.status_code} - {response.text}") |
| 99 | + return False |
| 100 | + else: |
| 101 | + return True |
| 102 | + |
| 103 | +# List roles to change for groups and projects |
| 104 | +# -------------------------------------------- |
| 105 | + |
| 106 | +def list_group_role_to_change(group_id): |
| 107 | + """ |
| 108 | + List all members with the role identified by ROLE_ID_TO_CHANGE to change in a group. |
| 109 | +
|
| 110 | + :param group_id: ID of the group whose members need to be listed. |
| 111 | + :return: List of members with the role ROLE_ID_TO_CHANGE. |
| 112 | + """ |
| 113 | + members_url = f"{GITLAB_URL}/api/v4/groups/{group_id}/members" |
| 114 | + response = requests.get(members_url, headers=headers) |
| 115 | + |
| 116 | + if response.status_code == 200: |
| 117 | + members = response.json() |
| 118 | + return [member for member in members if member['access_level'] == ROLE_ID_TO_CHANGE] |
| 119 | + else: |
| 120 | + print(f"❌ Failed to retrieve members for group '{group_id}': {response.status_code} - {response.text}") |
| 121 | + return [] |
| 122 | + |
| 123 | +def list_project_role_to_change(project_id): |
| 124 | + """ |
| 125 | + List all members with the role identified by ROLE_ID_TO_CHANGE in a project. |
| 126 | +
|
| 127 | + :param project_id: ID of the project whose members need to be listed. |
| 128 | + :return: List of members with the role ROLE_ID_TO_CHANGE. |
| 129 | + """ |
| 130 | + members_url = f"{GITLAB_URL}/api/v4/projects/{project_id}/members" |
| 131 | + response = requests.get(members_url, headers=headers) |
| 132 | + |
| 133 | + if response.status_code == 200: |
| 134 | + members = response.json() |
| 135 | + return [member for member in members if member['access_level'] == ROLE_ID_TO_CHANGE] |
| 136 | + else: |
| 137 | + print(f"❌ Failed to retrieve members for project '{project_id}': {response.status_code} - {response.text}") |
| 138 | + return [] |
| 139 | + |
| 140 | +# Get groups and projects names |
| 141 | +# ----------------------------- |
| 142 | + |
| 143 | +def get_group_name(group_id): |
| 144 | + """ |
| 145 | + Get the name of the group by its ID. |
| 146 | +
|
| 147 | + :param group_id: ID of the group. |
| 148 | + :return: Name of the group. |
| 149 | + """ |
| 150 | + url = f"{GITLAB_URL}/api/v4/groups/{group_id}" |
| 151 | + response = requests.get(url, headers=headers) |
| 152 | + if response.status_code == 200: |
| 153 | + return response.json().get('name') |
| 154 | + else: |
| 155 | + print(f"❌ Failed to retrieve group name for group '{group_id}': {response.status_code} - {response.text}") |
| 156 | + return None |
| 157 | + |
| 158 | +def get_project_name(project_id): |
| 159 | + """ |
| 160 | + Get the name of the project by its ID. |
| 161 | +
|
| 162 | + :param project_id: ID of the project. |
| 163 | + :return: Name of the project. |
| 164 | + """ |
| 165 | + url = f"{GITLAB_URL}/api/v4/projects/{project_id}" |
| 166 | + response = requests.get(url, headers=headers) |
| 167 | + if response.status_code == 200: |
| 168 | + return response.json().get('name') |
| 169 | + else: |
| 170 | + print(f"❌ Failed to retrieve project name for project '{project_id}': {response.status_code} - {response.text}") |
| 171 | + return None |
| 172 | + |
| 173 | +# Process groups and projects |
| 174 | +# --------------------------- |
| 175 | + |
| 176 | +def process_group(group_id): |
| 177 | + """ |
| 178 | + Process a group by retrieving its members, subgroups, and projects. |
| 179 | +
|
| 180 | + :param group_id: ID of the group to process. |
| 181 | + """ |
| 182 | + group_name = get_group_name(group_id) |
| 183 | + print(f"⏳ Processing group with name '{group_name}'") |
| 184 | + # List roles to change of the group |
| 185 | + group_roles_to_change = list_group_role_to_change(group_id) |
| 186 | + if group_roles_to_change: |
| 187 | + print(f"ℹ️ Roles to change in group '{group_name}' (ID: '{group_id}'): {[to_change['username'] for to_change in group_roles_to_change]}") |
| 188 | + for to_change in group_roles_to_change: |
| 189 | + if to_change['username'] not in PROTECTED_USERS: |
| 190 | + if change_group_role(group_id, to_change['id']): |
| 191 | + print(f"✅ Changed role for '{to_change['username']}' in group '{group_name}'") |
| 192 | + else: |
| 193 | + print(f"😱 Failed to change role for '{to_change['username']}' in group '{group_name}'") |
| 194 | + else: |
| 195 | + print(f"🛑 '{to_change['username']}' is a protected user and will not be changed.") |
| 196 | + |
| 197 | + # Retrieve subgroups |
| 198 | + subgroups_url = f"{GITLAB_URL}/api/v4/groups/{group_id}/subgroups" |
| 199 | + subgroups = requests.get(subgroups_url, headers=headers).json() |
| 200 | + |
| 201 | + if isinstance(subgroups, list): |
| 202 | + for subgroup in subgroups: |
| 203 | + process_group(subgroup['id']) |
| 204 | + |
| 205 | + # Retrieve projects in the group |
| 206 | + projects_url = f"{GITLAB_URL}/api/v4/groups/{group_id}/projects" |
| 207 | + projects = requests.get(projects_url, headers=headers).json() |
| 208 | + |
| 209 | + if isinstance(projects, list): |
| 210 | + for project in projects: |
| 211 | + project_name = get_project_name(project['id']) |
| 212 | + print(f"⏳ Processing project with name '{project_name}'") |
| 213 | + project_role_to_change = list_project_role_to_change(project['id']) |
| 214 | + if project_role_to_change: |
| 215 | + print(f"ℹ️ Roles to change in project '{project_name}' (ID: '{project['id']}'): {[to_change['username'] for to_change in project_role_to_change]}") |
| 216 | + for to_change in project_role_to_change: |
| 217 | + if to_change['username'] not in PROTECTED_USERS: |
| 218 | + if change_role(project['id'], to_change['id']): |
| 219 | + print(f"✅ Changed role for '{to_change['username']}' in project '{project_name}'") |
| 220 | + else: |
| 221 | + print(f"😱 Failed to change role for '{to_change['username']}' in project '{project_name}'") |
| 222 | + else: |
| 223 | + print(f"🛑 '{to_change['username']}' is a protected user and will not be changed.") |
| 224 | + else: |
| 225 | + print(f"❌ Unexpected response format for projects in group '{group_name}': {projects}") |
| 226 | + |
| 227 | +# Main |
| 228 | +# ----- |
| 229 | + |
| 230 | +def main(): |
| 231 | + """ |
| 232 | + Main function that handles command-line arguments and initiates processing. |
| 233 | + """ |
| 234 | + start_time = time.time() # Record the start time |
| 235 | + |
| 236 | + print(f"❗In organization '{ORG_NAME}' on '{GITLAB_URL}' will change all roles with identifier ROLE_ID_TO_CHANGE '{ROLE_ID_TO_CHANGE}' to CUSTOM_ROLE_ID_TO_APPLY '{CUSTOM_ROLE_ID_TO_APPLY}'") |
| 237 | + # Wait for user input before exiting |
| 238 | + input("👉 Press any key to continue...") |
| 239 | + |
| 240 | + # Retrieve the ID of the organization |
| 241 | + groups_url = f"{GITLAB_URL}/api/v4/groups?search={ORG_NAME}" |
| 242 | + groups = requests.get(groups_url, headers=headers).json() |
| 243 | + |
| 244 | + if isinstance(groups, list): |
| 245 | + for group in groups: |
| 246 | + process_group(group['id']) |
| 247 | + else: |
| 248 | + print(f"❌ Unexpected response format for groups: {groups}") |
| 249 | + |
| 250 | + end_time = time.time() |
| 251 | + elapsed_time = end_time - start_time |
| 252 | + print(f"⌛ Elapsed time: {elapsed_time:.2f} seconds") |
| 253 | + print("🧡 If you spotted a bug or have idea to improve the script, go there: https://github.com/Orange-OpenSource/floss-toolbox/issues/new/choose") |
| 254 | + print("👋 Bye!") |
| 255 | + |
| 256 | +if __name__ == "__main__": |
| 257 | + main() |
0 commit comments