Skip to content

Commit 1643392

Browse files
committed
feat: add script to change role for GitLab organizations
Add Pythons cript with, using a .env file besides, will change roles of organization groups and projects members. Still need to have the suitable permission to prevent 403 errors. Assisted-by: GPT-4o-mini (Dinootoo) Signed-off-by: Pierre-Yves Lapersonne <pierreyves.lapersonne@orange.com>
1 parent ee6ca79 commit 1643392

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed
+257
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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

Comments
 (0)