gitlabrat 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
labrat/cli/agents.py ADDED
@@ -0,0 +1,60 @@
1
+ from labrat.cli import common
2
+ from labrat.controllers.agents import Agents
3
+
4
+ def build_parser(parsers):
5
+ parser = parsers.add_parser("agents", help="Manage GitLab agents")
6
+ subparsers = parser.add_subparsers(dest="command", required=True)
7
+
8
+ list_parser = common.add_filtered_parser(subparsers, "list", handle_list_args, aliases=["ls"], help="List GitLab servers", filter_required=False)
9
+
10
+ delete_parser = common.add_filtered_parser(subparsers, "delete", handle_delete_args, aliases=["rm"], help="Delete GitLab server from config")
11
+
12
+ add_key_parser = common.add_filtered_parser(subparsers, "add-key", handle_add_key_args, help="Add SSH key to the user account", filter_required=False)
13
+ key_group = add_key_parser.add_mutually_exclusive_group(required=True)
14
+ key_group.add_argument("-k", "--key", required=False, help="Public SSH key to add")
15
+ key_group.add_argument("-K", "--key-file", required=False, help="Path to public SSH key file")
16
+
17
+ add_key_parser.add_argument("-t", "--title", required=False, help="Title for the SSH key", default="SSH Key")
18
+
19
+ parser.set_defaults(controller=Agents())
20
+ return parser
21
+
22
+ def handle_list_args(args):
23
+ # Prepare table
24
+ headers = ["Host", "ID", "Username", "Name", "Authenticated", "Is Admin", "Is Bot", "Password", "Private Token"]
25
+ data = []
26
+
27
+ for agent in args.controller.list(args.filter):
28
+ data.append([
29
+ agent.host,
30
+ agent.id,
31
+ agent.username,
32
+ agent.gitlab.user.name if agent.is_authenticated else "-",
33
+ agent.is_authenticated,
34
+ agent.is_admin,
35
+ agent.is_bot,
36
+ agent.password,
37
+ agent.private_token
38
+ ])
39
+
40
+ common.print_table(headers, data, "Host")
41
+
42
+ def handle_delete_args(args):
43
+ # Delete configs for filtered sections
44
+ for agent in args.controller.delete(args.filter):
45
+ print(f"[-] Deleted {agent.label} from config")
46
+
47
+ def handle_add_key_args(args):
48
+ # Prepare SSH key
49
+ if args.key:
50
+ key = args.key
51
+ elif args.key_file:
52
+ with open(args.key_file, "r") as f:
53
+ key = f.read().strip()
54
+
55
+ # Add SSH key to agents
56
+ for agent, err in args.controller.add_ssh_key(args.filter, args.title, key):
57
+ if err:
58
+ print(f"[-] Failed to add SSH key to {agent.label}: {err}")
59
+ else:
60
+ print(f"[+] Added SSH key to {agent.label}")
labrat/cli/auth.py ADDED
@@ -0,0 +1,60 @@
1
+ from labrat.controllers.auth import Auth
2
+ from labrat.core.utils import parse_host_range
3
+
4
+ def build_parser(parsers):
5
+ parser = parsers.add_parser("auth", help="Authenticate to GitLab server(s)")
6
+
7
+ parser.add_argument("-t", "--target", required=False, nargs="+", action="extend", help="GitLab server URL or pattern")
8
+ parser.add_argument("-T", "--target-file", required=False, help="Path to file containing GitLab server URLs or patterns")
9
+ parser.add_argument("-u", "--username", required=False, help="Username or e-mail for authentication")
10
+ parser.add_argument("-p", "--password", required=False, help="Password for authentication")
11
+ parser.add_argument("-C", "--combo-list", required=False, help="Path to the combo list file")
12
+ parser.add_argument("-l", "--use-ldap", action="store_true", help="Use LDAP for authentication")
13
+ parser.add_argument("-r", "--re-auth", action="store_true", help="Re-authenticate with stored credentials")
14
+ parser.add_argument("-n", "--token-name", required=False, help="Name for the access token", default="private token")
15
+ parser.add_argument("-s", "--scopes", required=False, help="Comma-separated list of scopes for the access token", default="api,read_repository,write_repository")
16
+
17
+ parser.set_defaults(func=handle_args, _parser=parser)
18
+ parser.set_defaults(controller=Auth())
19
+ return parser
20
+
21
+ def handle_args(args):
22
+ has_targets = args.target or args.target_file
23
+ has_credentials = (args.username and args.password) or args.combo_list
24
+ can_auth = has_targets and has_credentials
25
+
26
+ if args.re_auth or can_auth:
27
+ auth(args)
28
+ else:
29
+ getattr(args, "_parser", None).print_help()
30
+ return
31
+
32
+ def auth(args):
33
+ # Build the list of users
34
+ users = []
35
+ if args.username:
36
+ users = [(args.username, args.password)]
37
+ elif args.combo_list:
38
+ with open(args.combo_list, "r") as f:
39
+ for line in f:
40
+ username, password = line.strip().split(":")
41
+ users.append((username, password))
42
+
43
+ # Build the list of targets
44
+ targets = []
45
+ if args.target:
46
+ for target in args.target:
47
+ targets.extend(parse_host_range(target))
48
+ elif args.target_file:
49
+ with open(args.target_file, "r") as f:
50
+ for line in f:
51
+ targets.extend(parse_host_range(line.strip()))
52
+
53
+ scopes = args.scopes.split(",") if args.scopes else []
54
+
55
+ # Iterate over each user and target
56
+ for agent, err in args.controller.reauth(token_name=args.token_name, token_scopes=scopes, targets=targets, users=[user[0] for user in users]) if args.re_auth else args.controller.auth(targets, users, token_name=args.token_name, token_scopes=scopes, use_ldap=args.use_ldap):
57
+ if err:
58
+ print(f"[-] Authentication failed for {agent.label}: {err}")
59
+ else:
60
+ print(f"[+] Authenticated {agent.label}{' (admin)' if agent.is_admin else ''} with {agent.private_token}")
labrat/cli/common.py ADDED
@@ -0,0 +1,76 @@
1
+ import re
2
+
3
+ def add_filtered_parser(subparsers, name, handler, aliases=[], help=None, filter_required=True, all_help="All items", filter_help="Filter by substring"):
4
+ parser = subparsers.add_parser(name, aliases=aliases, help=help)
5
+
6
+ filter_group = parser.add_mutually_exclusive_group(required=filter_required)
7
+
8
+ if filter_required:
9
+ filter_group.add_argument("-a", "--all", action="store_true", help=all_help)
10
+
11
+ filter_group.add_argument("-f", "--filter", action="append", help=filter_help)
12
+
13
+ parser.set_defaults(func=handler, _parser=parser)
14
+ return parser
15
+
16
+ def print_table(headers, data, sort_key=None):
17
+ """
18
+ Print a table with the given data and headers.
19
+ """
20
+ column_widths = [max(len(re.sub(r'\x1b\[[0-9;]*m', '', str(x))) for x in col) for col in zip(*([headers] + data))]
21
+
22
+ # Print header
23
+ if headers:
24
+ print(" " + " ".join(h.ljust(w) for h, w in zip(headers, column_widths)) + " ")
25
+ print(" ".join("=" * (w + 1) for w in column_widths))
26
+
27
+ # Sort data by the specified key
28
+ if sort_key is not None:
29
+ data = sorted(data, key=lambda x: x[headers.index(sort_key)])
30
+
31
+ # Print data rows
32
+ for row in data:
33
+ print(" " + " ".join(str(x).ljust(w) for x, w in zip(row, column_widths)) + " ")
34
+
35
+ class Colors:
36
+ RESET = "\x1b[0m"
37
+ BRIGHT_RED = "\x1b[0;91m"
38
+ RED = "\x1b[0;31m"
39
+ GREEN = "\x1b[0;32m"
40
+ YELLOW = "\x1b[0;33m"
41
+ BLUE = "\x1b[0;34m"
42
+ MAGENTA = "\x1b[0;35m"
43
+ CYAN = "\x1b[0;36m"
44
+ WHITE = "\x1b[0;37m"
45
+
46
+ def color_access_level(level, text):
47
+ shades = [
48
+ Colors.RESET, # guest - none
49
+ Colors.RESET, # planner - none
50
+ Colors.RESET, # reporter - none
51
+ Colors.GREEN, # developer - green
52
+ Colors.YELLOW, # maintainer - yellow
53
+ Colors.BRIGHT_RED, # owner - bright red (highest)
54
+ ]
55
+ idx = min(len(shades)-1, max(0, (level // 10)))
56
+ return f"{shades[idx]}{text}{Colors.RESET}"
57
+
58
+ def color_diff(diff):
59
+ lines = []
60
+ for line in diff.splitlines():
61
+ if line.startswith("@@"):
62
+ match = re.match(r"(@@[^@]*@@)(.*)", line)
63
+ if match:
64
+ header = match.group(1)
65
+ tail = match.group(2)
66
+ lines.append(f"{Colors.CYAN}{header}{Colors.RESET}{tail}")
67
+ else:
68
+ lines.append(line)
69
+ elif line.startswith("+"):
70
+ lines.append(f"{Colors.GREEN}{line}{Colors.RESET}") # Green for additions
71
+ elif line.startswith("-"):
72
+ lines.append(f"{Colors.RED}{line}{Colors.RESET}") # Red for deletions
73
+ else:
74
+ lines.append(line)
75
+
76
+ return "\n".join(lines)
labrat/cli/projects.py ADDED
@@ -0,0 +1,95 @@
1
+ import datetime
2
+ import gitlab
3
+
4
+ from labrat.cli import common
5
+ from labrat.controllers.projects import Projects
6
+
7
+ def build_parser(parsers):
8
+ parser = parsers.add_parser("projects", help="Manage GitLab projects")
9
+ subparsers = parser.add_subparsers(dest="command", required=True)
10
+
11
+ list_parser = common.add_filtered_parser(subparsers, "list", handle_list_args, aliases=["ls"], help="List GitLab projects", filter_required=False)
12
+ list_parser.add_argument("-m", "--min-access-level", required=False, type=int, help="Minimum access level to filter projects", default=0)
13
+ list_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
14
+
15
+ clone_parser = common.add_filtered_parser(subparsers, "clone", handle_clone_args, help="Clone GitLab repositories")
16
+ clone_parser.add_argument("-o", "--output", required=False, help="Output location for cloned repositories", default='./')
17
+ clone_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
18
+
19
+ create_token_parser = common.add_filtered_parser(subparsers, "create_token", handle_create_token_args, help="Create an access token", filter_required=True)
20
+ create_token_parser.add_argument("-l", "--access-level", required=False, type=int, help="Access level for the access token", default=50)
21
+ create_token_parser.add_argument("-d", "--days", required=False, type=int, help="Number of days until the token expires", default=60)
22
+ create_token_parser.add_argument("-n", "--token-name", required=False, help="Name for the access token", default="project_bot")
23
+ create_token_parser.add_argument("-s", "--scopes", required=False, help="Comma-separated list of scopes for the access token", default="api,read_repository,write_repository")
24
+ create_token_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
25
+
26
+ update_parser = common.add_filtered_parser(subparsers, "update", handle_update_args, help="Update GitLab repositories procedurally", filter_required=True)
27
+ update_parser.add_argument("-F", "--file", required=True, help="Path to the remote file to update")
28
+
29
+ mechanism_group = update_parser.add_mutually_exclusive_group(required=True)
30
+ mechanism_group.add_argument("-c", "--content", required=False, help="Text content to replace the file with")
31
+ mechanism_group.add_argument("-C", "--content-file", required=False, help="Path to replacement file")
32
+ mechanism_group.add_argument("-p", "--pattern", required=False, help="String or regex pattern to find in the file")
33
+
34
+ update_parser.add_argument("-r", "--replace", required=False, help="String or pattern to replace the found text")
35
+
36
+ update_parser.add_argument("-m", "--commit-message", required=False, help="Commit message for the update", default="Update")
37
+ update_parser.add_argument("-b", "--branch", required=False, help="Branch to update", default="main")
38
+ update_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
39
+
40
+ parser.set_defaults(controller=Projects())
41
+ return subparsers
42
+
43
+ def handle_list_args(args):
44
+ headers = ["Host", "ID", "Path with Namespace", "Access Level", "Agents"]
45
+ data = []
46
+
47
+ for project in args.controller.list(args.filter, args.user):
48
+ # color each username by that agent's access level (higher -> brighter)
49
+ usernames = []
50
+ for agent in project.agents:
51
+ colored_username = common.color_access_level(agent.access_level, f"{agent.username} ({agent.access_level})")
52
+ usernames.append(colored_username)
53
+
54
+ # Add project information to data table
55
+ data.append([
56
+ project.host,
57
+ project.id,
58
+ project.path_with_namespace,
59
+ f"{project.access_level} ({gitlab.const.AccessLevel(project.access_level).name.lower()})",
60
+ ", ".join(usernames)
61
+ ])
62
+
63
+ common.print_table(headers, data)
64
+
65
+ def handle_clone_args(args):
66
+ for project, err in args.controller.clone(args.output, args.user, args.filter):
67
+ if err:
68
+ print(f"[!] Failed to clone {project.web_url}: {err}")
69
+ else:
70
+ print(f"[+] Successfully cloned {project.web_url} to {project.to_path}")
71
+
72
+ def handle_create_token_args(args):
73
+ expiration = (datetime.datetime.now() + datetime.timedelta(days=args.days)).strftime("%Y-%m-%d")
74
+ for project, agent, err in args.controller.create_token(args.token_name, args.access_level, args.scopes.split(","), expiration, args.user, args.filter):
75
+ if err:
76
+ print(f"[!] Failed to create access token for {project.web_url}: {err}")
77
+ else:
78
+ print(f"[+] Successfully created access token for {project.web_url}: {agent.private_token}")
79
+
80
+ def handle_update_args(args):
81
+ content = None
82
+ if args.content:
83
+ content = args.content
84
+ elif args.content_file:
85
+ with open(args.content_file, "r") as f:
86
+ content = f.read()
87
+
88
+ for project, commit, err in args.controller.update(file_path=args.file, content=content, pattern=args.pattern, replace=args.replace, branch=args.branch, commit_message=args.commit_message, agent_filter=args.user, filter=args.filter):
89
+ if err:
90
+ print(f"[!] Failed to update {project.web_url}: {err}")
91
+ else:
92
+ print(f"[+] Successfully updated {project.web_url}:")
93
+ diff = commit.diff()
94
+ if diff:
95
+ print(common.color_diff(diff[0].get("diff")))
labrat/cli/users.py ADDED
@@ -0,0 +1,40 @@
1
+ from labrat.cli import common
2
+ from labrat.controllers.users import Users
3
+
4
+ def build_parser(parsers):
5
+ parser = parsers.add_parser("users", help="Manage GitLab users")
6
+ subparsers = parser.add_subparsers(dest="command", required=True)
7
+
8
+ list_parser = common.add_filtered_parser(subparsers, "list", handle_list_args, aliases=["ls"], help="List GitLab users", filter_required=False)
9
+
10
+ create_token_parser = common.add_filtered_parser(subparsers, "create_token", handle_create_token_args, help="Create an access token", filter_required=True)
11
+ create_token_parser.add_argument("-n", "--token-name", required=False, help="Name for the access token", default="private token")
12
+ create_token_parser.add_argument("-s", "--scopes", required=False, help="Comma-separated list of scopes for the access token", default="api,read_repository,write_repository")
13
+
14
+
15
+ parser.set_defaults(controller=Users())
16
+ return parser
17
+
18
+ def handle_list_args(args):
19
+ headers = ["Host", "ID", "Username", "Name", "Is Agent", "Is Admin", "Is Bot"]
20
+ data = []
21
+
22
+ for user in args.controller.list(filter=args.filter):
23
+ data.append([
24
+ user.host,
25
+ user.id,
26
+ user.username,
27
+ user.name,
28
+ getattr(user, "is_agent", "-"),
29
+ getattr(user, "is_admin", "-"),
30
+ getattr(user, "bot", "-")
31
+ ])
32
+
33
+ common.print_table(headers, data, "Host")
34
+
35
+ def handle_create_token_args(args):
36
+ for agent, err in args.controller.create_token(args.token_name, args.scopes.split(","), filter=args.filter):
37
+ if err:
38
+ print(f"[-] Failed to create access token: {err}")
39
+ else:
40
+ print(f"[+] Created access token for {agent.label}: {agent.private_token}")
File without changes
@@ -0,0 +1,47 @@
1
+ from labrat.core.agent import Agent
2
+ from labrat.core.config import Config
3
+ from urllib.parse import urlparse
4
+
5
+ class Agents:
6
+ def __init__(self):
7
+ self.config = Config(preauth=True)
8
+
9
+ def list(self, filter=None):
10
+ """List Labrat agents in config.
11
+
12
+ Keyword arguments:
13
+ - filter: Regex filter and field selection passed to `utils.obj_filter()`
14
+ """
15
+
16
+ for section, agent in self.config.filter(filter):
17
+ yield agent
18
+
19
+ def delete(self, filter):
20
+ """Delete Labrat agents from config.
21
+
22
+ Keyword arguments:
23
+ - filter: Regex filter and field selection passed to `utils.obj_filter()`
24
+ """
25
+
26
+ for section, agent in self.config.filter(filter):
27
+ self.config.remove_section(section)
28
+ yield agent
29
+
30
+ def add_ssh_key(self, filter, title, key):
31
+ """Add an SSH key to each agent matching the filter.
32
+
33
+ Keyword arguments:
34
+ - filter: Regex filter and field selection passed to `utils.obj_filter()`
35
+ - title: Title of the SSH key.
36
+ - key: The SSH key to add.
37
+ """
38
+
39
+ for section, agent in self.config.filter(filter):
40
+ if not agent.is_authenticated:
41
+ continue
42
+
43
+ try:
44
+ agent.add_ssh_key(title, key)
45
+ yield agent, None
46
+ except Exception as e:
47
+ yield agent, e
@@ -0,0 +1,55 @@
1
+ from labrat.core.agent import Agent
2
+ from labrat.core.config import Config
3
+ from urllib.parse import urlparse
4
+
5
+ class Auth:
6
+ def __init__(self):
7
+ self.config = Config()
8
+
9
+ def auth(self, targets, users, token_name, token_scopes, use_ldap=False):
10
+ """Authenticate and create a PAT for each user on each target.
11
+
12
+ Keyword arguments:
13
+ - targets: List of target URLs to authenticate against.
14
+ - users: List of (username, password) tuples for authentication.
15
+ - token_name: Name for the created Access Token.
16
+ - token_scopes: Scopes for the created Access Token.
17
+ - use_ldap: Whether to use LDAP for authentication (default: False).
18
+ """
19
+
20
+ for username, password in users:
21
+ for target in targets:
22
+ agent = Agent(target, use_ldap, username, password)
23
+ try:
24
+ agent.login()
25
+ agent.auth(private_token=agent.create_pat(token_name, token_scopes))
26
+ self.config[agent.section] = agent.to_dict()
27
+ yield agent, None
28
+ except Exception as e:
29
+ yield agent, e
30
+
31
+ def reauth(self, token_name, token_scopes, targets=None, users=None):
32
+ """Re-authenticate existing agents.
33
+
34
+ Keyword arguments:
35
+ - targets: List of target URLs to re-authenticate against.
36
+ - users: List of usernames to re-authenticate.
37
+ """
38
+
39
+ for section, agent in self.config:
40
+ # Filter by target if specified
41
+ if targets and agent.url not in targets:
42
+ continue
43
+
44
+ # Filter by username if specified
45
+ if users and agent.username not in users:
46
+ continue
47
+
48
+ try:
49
+ if not agent.is_authenticated:
50
+ agent.login()
51
+ agent.auth(private_token=agent.create_pat(token_name, token_scopes))
52
+ self.config[section] = agent.to_dict()
53
+ yield agent, None
54
+ except Exception as e:
55
+ yield agent, e
@@ -0,0 +1,164 @@
1
+ import copy
2
+ import os
3
+ import git
4
+ import re
5
+
6
+ from labrat.core.agent import Agent
7
+ from labrat.core.config import Config
8
+ from labrat.core.utils import obj_filter
9
+ from urllib.parse import urlparse
10
+
11
+ class Projects:
12
+ def __init__(self):
13
+ self.config = Config(authed_only=True)
14
+
15
+ def list(self, filter=None, agent_filter=None, min_level=0):
16
+ """List projects accessible by agents.
17
+
18
+ Keyword arguments:
19
+ - filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects
20
+ - agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects
21
+ - min_level: Minimum desired access level for agent to return a project
22
+ """
23
+
24
+ repo_map = {}
25
+ for section, agent in self.config.filter(agent_filter):
26
+ projects = agent.gitlab.projects.list(all=True)
27
+ for project in projects:
28
+ access_level = self.get_project_access_level(agent, project)
29
+ if access_level < min_level:
30
+ continue
31
+
32
+ # Agent enrichment with shallow copy
33
+ agent_copy = copy.copy(agent)
34
+ agent_copy.access_level = access_level
35
+
36
+ # Project enrichment
37
+ project.host = agent.host
38
+
39
+ # Add project and agent to map
40
+ if project.web_url not in repo_map:
41
+ project.agents = [agent_copy]
42
+ project.access_level = access_level
43
+ repo_map[project.web_url] = project
44
+ else:
45
+ repo_map[project.web_url].agents.append(agent_copy)
46
+ repo_map[project.web_url].access_level = max(repo_map[project.web_url].access_level, access_level)
47
+
48
+ return [proj for proj in repo_map.values() if not filter or obj_filter(proj, filter)]
49
+
50
+ def clone(self, output, agent_filter=None, filter=None):
51
+ """Clone projects accessible by agents.
52
+
53
+ Keyword arguments:
54
+ - output: Output directory for cloned projects. A nested structure of `<hostname>/<namespace>/<project>` will be created inside.
55
+ - agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects.
56
+ - filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects.
57
+ """
58
+
59
+ projects = self.list(filter=filter, agent_filter=agent_filter, min_level=15)
60
+ for project in projects:
61
+ agent = project.agents[0]
62
+
63
+ url = urlparse(agent.url)
64
+ clone_url = f"{url.scheme}://{agent.username}:{agent.private_token}@{url.netloc}/{project.path_with_namespace}.git"
65
+ to_path = f"{output}{'/' if output[-1] != '/' else ''}{url.hostname}/{project.path_with_namespace}"
66
+ project.clone_url = clone_url
67
+ project.to_path = to_path
68
+
69
+ if os.path.isdir(to_path):
70
+ yield project, f"destination path '{to_path}' already exists"
71
+ continue
72
+
73
+ try:
74
+ git.Repo.clone_from(clone_url, to_path)
75
+ yield project, None
76
+ except Exception as e:
77
+ yield project, e
78
+
79
+ def create_token(self, name, access_level, scopes, expires_at, agent_filter=None, filter=None):
80
+ """ Create Project Access Tokens.
81
+
82
+ Keyword arguments:
83
+ - name: Name of the access token.
84
+ - access_level: Access level of the created PAT.
85
+ - scopes: List of scopes for the created PAT. (e.g. `['read_repository', 'write_repository']`)
86
+ - expires_at: ISO format expiration date for the created PAT.
87
+ - agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects.
88
+ - filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects.
89
+ """
90
+
91
+ for project in self.list(filter=filter, agent_filter=agent_filter, min_level=40):
92
+ agent = project.agents[0]
93
+
94
+ try:
95
+ req = project.access_tokens.create({
96
+ 'name': name,
97
+ 'access_level': access_level,
98
+ 'scopes': scopes,
99
+ 'expires_at': expires_at
100
+ })
101
+
102
+ section = f"{req.user_id}_{project.id}@{agent.host}"
103
+ project_agent = Agent(agent.url, username=name, private_token=req.token, section=section)
104
+ self.config[section] = project_agent.to_dict()
105
+
106
+ yield project, project_agent, None
107
+ except Exception as e:
108
+ yield project, None, e
109
+
110
+ def update(self, file_path, content=None, pattern=None, replace=None, branch=None, commit_message=None, agent_filter=None, filter=None):
111
+ """Update file contents.
112
+
113
+ Keyword arguments:
114
+ - file_path: Path to the file in project to update.
115
+ - content: New content for the file.
116
+ - pattern: Regex pattern to match for replacement.
117
+ - replace: Replacement string for the regex pattern.
118
+ - branch: Branch to commit the changes to.
119
+ - commit_message: Commit message for the update.
120
+ - agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects.
121
+ - filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects.
122
+ """
123
+
124
+ for project in self.list(filter=filter, agent_filter=agent_filter, min_level=30):
125
+ agent = project.agents[0]
126
+ try:
127
+ file = project.files.get(file_path=file_path, ref="main")
128
+ file_content = file.decode().decode("utf-8")
129
+
130
+ if content is not None:
131
+ new_content = content
132
+ elif pattern and replace is not None:
133
+ new_content = re.sub(pattern, replace, file_content, flags=re.MULTILINE)
134
+ else:
135
+ yield project, None, "No update method specified"
136
+ continue
137
+
138
+ if new_content == file_content:
139
+ yield project, None, "Content is the same, skipping update"
140
+ continue
141
+
142
+ commit = project.commits.create({
143
+ "branch": branch,
144
+ "commit_message": commit_message,
145
+ "actions": [
146
+ {
147
+ "action": "update",
148
+ "file_path": file_path,
149
+ "content": new_content
150
+ }
151
+ ]
152
+ })
153
+ yield project, commit, None
154
+ except Exception as e:
155
+ yield project, None, e
156
+
157
+ def get_project_access_level(self, agent, project):
158
+ if project.permissions['project_access']:
159
+ access_level = int(project.permissions['project_access']['access_level'])
160
+ elif project.permissions['group_access']:
161
+ access_level = int(project.permissions['group_access']['access_level'])
162
+ else:
163
+ access_level = 60 if agent.is_admin else 15
164
+ return access_level
@@ -0,0 +1,68 @@
1
+ from labrat.core.agent import Agent
2
+ from labrat.core.config import Config
3
+ from labrat.core.utils import obj_filter
4
+ from urllib.parse import urlparse
5
+
6
+ class Users:
7
+ def __init__(self):
8
+ self.config = Config(authed_only=True)
9
+
10
+ def list(self, filter=None):
11
+ """List users and additional information.
12
+
13
+ Keyword arguments:
14
+ - filter: Regex filter and field selection passed to `utils.obj_filter()`
15
+ """
16
+
17
+ data = dict()
18
+ for section, agent in self.config:
19
+ users = agent.gitlab.users.list(all=True)
20
+ for user in users:
21
+ self._user_enrichment(agent, user)
22
+
23
+ if user.section not in data.keys() or agent.is_admin:
24
+ data[user.section] = user
25
+
26
+ data = [user for user in data.values() if not filter or obj_filter(user, filter)]
27
+
28
+ return data
29
+
30
+ def create_token(self, name, scopes, filter=None):
31
+ """Create Personal Access Tokens for other users.
32
+
33
+ Keyword arguments:
34
+ - name: Name of the access token.
35
+ - scopes: List of scopes for the access token.
36
+ - filter: Regex filter and field selection passed to `utils.obj_filter()`
37
+ """
38
+
39
+ for section, agent in self.config:
40
+ if not agent.is_admin:
41
+ continue
42
+
43
+ for user in agent.gitlab.users.list(all=True):
44
+ self._user_enrichment(agent, user)
45
+ if user.is_agent:
46
+ continue
47
+
48
+ if filter and not obj_filter(user, filter):
49
+ continue
50
+
51
+ try:
52
+ req = user.personal_access_tokens.create({
53
+ "name": name,
54
+ "scopes": scopes
55
+ })
56
+ agent_user = Agent(agent.url, username=user.username, private_token=req.token)
57
+ self.config[user.section] = agent_user.to_dict()
58
+ yield agent_user, None
59
+ except Exception as e:
60
+ yield None, e
61
+
62
+ def _user_enrichment(self, agent, user):
63
+ user.url = agent.url
64
+ user.host = agent.host
65
+ user.section = f"{user.id}@{agent.host}"
66
+ user.label = f"{user.username}@{agent.host}"
67
+ user.is_agent = self.config.has_section(user.section)
68
+ user.is_bot = getattr(user, "bot", None)
File without changes