一、功能概述
本脚本提供了一种高效的方式来批量管理 GitLab 用户的仓库权限,主要功能包括:
• 🚀 批量操作:一次性为多个用户添加/更新多个项目的权限
• 🔍 智能识别:支持混合使用用户ID/用户名和项目ID/项目名
• ⚙️ 权限分级:可设置从访客(Guest)到所有者(Owner)的不同权限级别
• ✅ 智能处理:自动跳过已有权限,仅更新需要变更的权限
• 📊 操作统计:提供成功/跳过/失败的详细统计信息
二、环境要求
• Python 3.6 或更高版本
• python-gitlab
包 (通过 pip install python-gitlab
安装)
• 具有足够权限的 GitLab API 访问令牌
三、使用说明
3.1 基本用法
python 脚本名.py -t <你的GitLab令牌> --users <用户列表> --projects <项目列表> [其他参数]
3.2 参数说明
必填参数
参数 说明 示例 -t
或--token
GitLab 个人访问令牌(需有管理员权限) -t "abc123xyz"
--users
用户列表(支持 空格 或 逗号 分隔)
• 可以是用户名或用户ID--users "user1,user2"
--users "100 101"
--projects
项目列表(支持 空格 或 逗号 分隔)
• 可以是 项目ID 或 完整路径(如组名/项目名
)--projects "42"
--projects "test/project1 project2"
可选参数
参数 说明 示例 --url
GitLab 地址(默认公司内网地址) --url "https://gitlab.example.com"
--action
操作类型: add
(添加权限,默认)或remove
(移除权限)--action remove
--access-level
权限等级(仅 add
时有效):
•5
: 最小权限
•10
: Guest
•20
: Reporter
•30
: Developer(默认)
•40
: Maintainer
•50
: Owner--access-level 40
3.3 使用示例
添加权限
# 添加用户 user1 和 user2 到项目 42 和 test Developer(默认) python script.py -t "abc123" --users "user1,user2" --projects "42 test" # 添加用户 ID 100 到项目 test/project1 权限为 Maintainer python script.py -t "abc123" --users "100" --projects "test/project1" --access-level 40
移除权限
# 从项目 42 中移除用户 user1 python script.py -t "abc123" --users "user1" --projects "42" --action remove
3.4 输出示例
🔄 正在处理项目: project_name (ID: 123)
✅ 添加: 张三 (30级权限)
⏩ 跳过: 李四 已有相同权限 (30)
❌ 操作失败: 未知用户 - 用户未找到
📊 操作完成: 成功 1 次 | 跳过 1 次 | 失败 1 次
四、安全注意事项
• 🔒 请妥善保管您的私有令牌(切勿提交到版本控制系统)
• ⚠️ 脚本默认禁用SSL验证(生产环境建议添加 ssl_verify=True
)
• 👥 运行前请确保您的令牌具有足够的权限
五、源代码
import gitlab
import argparse
from typing import Union, List
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def parse_arguments():
"""Parse command line arguments and return namespace object"""
parser = argparse.ArgumentParser(
description="Batch add/remove GitLab user repository permissions tool",
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
# Define argument groups for better readability
auth_group = parser.add_argument_group("Authentication Parameters")
auth_group.add_argument(
'-t', '--token',
required=True,
help="GitLab private token (required)"
)
auth_group.add_argument(
'--url',
default='https://gitlab.com',
help="GitLab instance URL"
)
input_group = parser.add_argument_group("Input Parameters")
input_group.add_argument(
'--users',
required=True,
help="List of user IDs or usernames (comma or space separated)"
)
input_group.add_argument(
'--projects',
required=True,
help="List of project IDs or project paths (comma or space separated)"
)
# Action parameter
parser.add_argument(
'--action',
choices=['add', 'remove'],
default='add',
help="Action to perform: add or remove permissions"
)
# Access level parameter (only relevant for add action)
parser.add_argument(
'--access-level',
type=int,
choices=[5, 10, 20, 30, 40, 50],
default=30,
help="Access level (5: Minimal, 10: Guest, 20: Reporter, 30: Developer, 40: Maintainer, 50: Owner)"
)
return parser.parse_args()
def resolve_entity(gl, entity_str: str, is_user: bool = True) -> int:
"""
Resolve entity ID from string (supports ID/username/project path)
Args:
gl: GitLab instance
entity_str: Entity identifier to resolve
is_user: Whether to resolve as user (True: user, False: project)
Returns:
Resolved entity ID
Raises:
ValueError: When unable to resolve entity
"""
try:
# First try to convert to integer (ID)
return int(entity_str)
except ValueError:
try:
if is_user:
# Exact match for username
users = gl.users.list(username=entity_str, all=True)
if users:
return users[0].id
raise ValueError(f"No user found with username: {entity_str}")
else:
# Exact match for project path
project = gl.projects.get(entity_str)
return project.id
except gitlab.exceptions.GitlabGetError:
raise ValueError(f"No {'user' if is_user else 'project'} found: {entity_str}")
except gitlab.exceptions.GitlabError as e:
raise ValueError(f"GitLab API error: {str(e)}")
except Exception as e:
raise ValueError(f"Unexpected error during resolution: {str(e)}")
def process_user_list(user_input: str) -> List[str]:
"""Process user input string into a list of user identifiers"""
# First try splitting by comma
if ',' in user_input:
users = [u.strip() for u in user_input.split(',')]
else:
# Fall back to space splitting
users = user_input.split()
return users
def process_project_list(project_input: str) -> List[str]:
"""Process project input string into a list of project identifiers"""
# First try splitting by comma
if ',' in project_input:
projects = [p.strip() for p in project_input.split(',')]
else:
# Fall back to space splitting
projects = project_input.split()
return projects
def batch_process_permissions(gl, users: List[Union[int, str]], projects: List[Union[int, str]],
access_level: int, action: str):
"""Core logic for batch adding/removing permissions"""
success_count = 0
skip_count = 0
fail_count = 0
for project_identifier in projects:
try:
project_id = resolve_entity(gl, project_identifier, is_user=False)
project = gl.projects.get(project_id)
print(f"\n🔄 Processing project: {project.path_with_namespace} (ID: {project.id})")
# Get existing members (to avoid duplicate additions or non-existent removals)
existing_members = {m.username: m for m in project.members.list(all=True)}
except ValueError as e:
print(f"❌ Project resolution failed: {project_identifier} - {str(e)}")
fail_count += 1
continue
for user_identifier in users:
try:
user_id = resolve_entity(gl, user_identifier, is_user=True)
user = gl.users.get(user_id)
if action == 'add':
# Check if already exists
if user.username in existing_members:
current_level = existing_members[user.username].access_level
if current_level == access_level:
print(f"⏩ Skipped: {user.username} already has same permission ({access_level})")
skip_count += 1
else:
# Update existing permission
member = project.members.get(user.id)
member.access_level = access_level
member.save()
print(f"🔄 Updated: {user.username} permission {current_level}→{access_level}")
success_count += 1
else:
# Add new member
project.members.create({
'user_id': user.id,
'access_level': access_level
})
print(f"✅ Added: {user.username} ({access_level} level permission)")
success_count += 1
else: # action == 'remove'
if user.username in existing_members:
# Remove existing member
member = project.members.get(user.id)
member.delete()
print(f"✅ Removed: {user.username} from project")
success_count += 1
else:
print(f"⏩ Skipped: {user.username} not in project members")
skip_count += 1
except ValueError as e:
print(f"❌ User resolution failed: {user_identifier} - {str(e)}")
fail_count += 1
except gitlab.exceptions.GitlabError as e:
print(f"❌ Operation failed: {user_identifier} - {str(e)}")
fail_count += 1
print(f"\n📊 Operation completed: Success {success_count} | Skipped {skip_count} | Failed {fail_count}")
def main():
args = parse_arguments()
# Process user and project lists
users = process_user_list(args.users)
projects = process_project_list(args.projects)
# Initialize GitLab connection
try:
gl = gitlab.Gitlab(
args.url,
private_token=args.token,
ssl_verify=False
)
gl.auth() # Test connection validity
except Exception as e:
print(f"❌ GitLab connection failed: {str(e)}")
return
# Execute permission addition/removal
batch_process_permissions(
gl=gl,
users=users,
projects=projects,
access_level=args.access_level,
action=args.action
)
if __name__ == "__main__":
main()