一、功能概述

本脚本提供了一种高效的方式来批量管理 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 参数说明

  1. 必填参数

    参数说明示例
    -t--tokenGitLab 个人访问令牌(需有管理员权限)-t "abc123xyz"
    --users用户列表(支持 空格 或 逗号 分隔)
    • 可以是用户名或用户ID
    --users "user1,user2"
    --users "100 101"
    --projects项目列表(支持 空格 或 逗号 分隔)
    • 可以是 项目ID 或 完整路径(如 组名/项目名
    --projects "42"
    --projects "test/project1 project2"

  1. 可选参数

    参数说明示例
    --urlGitLab 地址(默认公司内网地址)--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 使用示例

  1. 添加权限

    # 添加用户 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
  2. 移除权限

    # 从项目 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()
END
本文作者:
文章标题: GitLab 权限批量管理工具
本文地址: https://blog.imwlw.com/archives/40/
版权说明:若无注明,本文皆 ITShare Studio 原创,转载请保留文章出处。
最后修改:2025 年 04 月 22 日
如果觉得我的文章对你有用,请随意赞赏