commitguardian 0.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.
- commitguard/__init__.py +26 -0
- commitguard/__main__.py +6 -0
- commitguard/api/__init__.py +18 -0
- commitguard/api/app.py +1376 -0
- commitguard/api/governance.py +1085 -0
- commitguard/api/hosting.py +196 -0
- commitguard/api/http.py +252 -0
- commitguard/api/settings.py +169 -0
- commitguard/audit/__init__.py +13 -0
- commitguard/audit/logger.py +34 -0
- commitguard/audit/models.py +222 -0
- commitguard/audit/storage.py +59 -0
- commitguard/ci/__init__.py +7 -0
- commitguard/ci/context.py +60 -0
- commitguard/cli/__init__.py +6 -0
- commitguard/cli/app.py +74 -0
- commitguard/cli/commands/__init__.py +1 -0
- commitguard/cli/commands/benchmark.py +441 -0
- commitguard/cli/commands/check.py +100 -0
- commitguard/cli/commands/ci.py +165 -0
- commitguard/cli/commands/dashboard.py +141 -0
- commitguard/cli/commands/doctor.py +533 -0
- commitguard/cli/commands/github.py +449 -0
- commitguard/cli/commands/hook.py +156 -0
- commitguard/cli/commands/init.py +137 -0
- commitguard/cli/commands/install.py +152 -0
- commitguard/cli/commands/policy.py +36 -0
- commitguard/cli/commands/report.py +39 -0
- commitguard/cli/commands/reproduce.py +123 -0
- commitguard/cli/commands/scan.py +47 -0
- commitguard/cli/common.py +44 -0
- commitguard/cli/output.py +89 -0
- commitguard/cli/render.py +367 -0
- commitguard/config/__init__.py +6 -0
- commitguard/config/defaults.py +53 -0
- commitguard/config/enforcement.py +53 -0
- commitguard/config/loader.py +174 -0
- commitguard/config/schema.py +105 -0
- commitguard/config/sources.py +183 -0
- commitguard/controlplane/__init__.py +24 -0
- commitguard/controlplane/access.py +231 -0
- commitguard/controlplane/commands.py +393 -0
- commitguard/controlplane/errors.py +88 -0
- commitguard/controlplane/identity.py +478 -0
- commitguard/controlplane/members.py +219 -0
- commitguard/controlplane/notifications.py +787 -0
- commitguard/controlplane/pagination.py +146 -0
- commitguard/controlplane/policies.py +1204 -0
- commitguard/controlplane/queries.py +1814 -0
- commitguard/controlplane/results.py +909 -0
- commitguard/controlplane/rules.py +184 -0
- commitguard/controlplane/views.py +799 -0
- commitguard/core/__init__.py +6 -0
- commitguard/core/context.py +31 -0
- commitguard/core/decision.py +58 -0
- commitguard/core/engine.py +82 -0
- commitguard/core/result.py +177 -0
- commitguard/detectors/__init__.py +6 -0
- commitguard/detectors/base.py +58 -0
- commitguard/detectors/bot.py +87 -0
- commitguard/detectors/coauthor.py +86 -0
- commitguard/detectors/identity.py +76 -0
- commitguard/detectors/registry.py +72 -0
- commitguard/detectors/trailer.py +211 -0
- commitguard/exceptions/__init__.py +33 -0
- commitguard/exceptions/base.py +9 -0
- commitguard/exceptions/configuration.py +22 -0
- commitguard/exceptions/detection.py +11 -0
- commitguard/exceptions/git.py +41 -0
- commitguard/exceptions/service.py +25 -0
- commitguard/git/__init__.py +12 -0
- commitguard/git/commands.py +101 -0
- commitguard/git/commit.py +97 -0
- commitguard/git/diff.py +36 -0
- commitguard/git/hooks.py +527 -0
- commitguard/git/push.py +93 -0
- commitguard/git/ranges.py +71 -0
- commitguard/git/repository.py +447 -0
- commitguard/github/__init__.py +34 -0
- commitguard/github/actions.py +163 -0
- commitguard/github/app.py +935 -0
- commitguard/github/auth.py +217 -0
- commitguard/github/check_runs.py +172 -0
- commitguard/github/checks.py +210 -0
- commitguard/github/client.py +844 -0
- commitguard/github/enforcement_status.py +209 -0
- commitguard/github/errors.py +129 -0
- commitguard/github/events.py +563 -0
- commitguard/github/identifiers.py +90 -0
- commitguard/github/installations.py +566 -0
- commitguard/github/markdown.py +19 -0
- commitguard/github/permissions.py +70 -0
- commitguard/github/pull_requests.py +53 -0
- commitguard/github/queue.py +47 -0
- commitguard/github/recovery.py +124 -0
- commitguard/github/repositories.py +305 -0
- commitguard/github/server.py +52 -0
- commitguard/github/settings.py +174 -0
- commitguard/github/storage.py +2315 -0
- commitguard/github/webhooks.py +129 -0
- commitguard/github/worker.py +628 -0
- commitguard/github/workflow.py +286 -0
- commitguard/governance/__init__.py +26 -0
- commitguard/governance/bulk.py +765 -0
- commitguard/governance/cache.py +88 -0
- commitguard/governance/common.py +216 -0
- commitguard/governance/exceptions.py +861 -0
- commitguard/governance/groups.py +448 -0
- commitguard/governance/inventory.py +386 -0
- commitguard/governance/posture.py +1272 -0
- commitguard/governance/resolver.py +632 -0
- commitguard/governance/rollouts.py +760 -0
- commitguard/governance/rules.py +371 -0
- commitguard/governance/schedules.py +663 -0
- commitguard/governance/service.py +120 -0
- commitguard/governance/settings.py +365 -0
- commitguard/governance/simulation.py +618 -0
- commitguard/governance/workflow.py +734 -0
- commitguard/notifications/__init__.py +2 -0
- commitguard/notifications/channels/__init__.py +1 -0
- commitguard/notifications/channels/base.py +22 -0
- commitguard/notifications/channels/email.py +110 -0
- commitguard/notifications/channels/in_app.py +74 -0
- commitguard/notifications/channels/sink.py +58 -0
- commitguard/notifications/channels/webhook.py +233 -0
- commitguard/notifications/deduplication.py +57 -0
- commitguard/notifications/dispatcher.py +201 -0
- commitguard/notifications/models.py +439 -0
- commitguard/notifications/outbox.py +106 -0
- commitguard/notifications/preferences.py +224 -0
- commitguard/notifications/retry.py +282 -0
- commitguard/notifications/service.py +128 -0
- commitguard/notifications/settings.py +167 -0
- commitguard/notifications/templates.py +108 -0
- commitguard/observability/__init__.py +5 -0
- commitguard/observability/logging.py +161 -0
- commitguard/observability/metrics.py +105 -0
- commitguard/policies/__init__.py +6 -0
- commitguard/policies/defaults.py +48 -0
- commitguard/policies/evaluator.py +66 -0
- commitguard/policies/governance.py +498 -0
- commitguard/policies/loader.py +23 -0
- commitguard/policies/mandatory.py +52 -0
- commitguard/policies/model.py +46 -0
- commitguard/provenance/__init__.py +9 -0
- commitguard/provenance/author.py +146 -0
- commitguard/provenance/committer.py +16 -0
- commitguard/provenance/normalization.py +158 -0
- commitguard/provenance/signatures.py +34 -0
- commitguard/provenance/trailers.py +256 -0
- commitguard/research/__init__.py +26 -0
- commitguard/research/compare.py +231 -0
- commitguard/research/datasets.py +1484 -0
- commitguard/research/detection.py +183 -0
- commitguard/research/environment.py +185 -0
- commitguard/research/gitenv.py +108 -0
- commitguard/research/hooks.py +247 -0
- commitguard/research/metrics.py +85 -0
- commitguard/research/performance.py +194 -0
- commitguard/research/platform.py +288 -0
- commitguard/research/report.py +372 -0
- commitguard/research/repository.py +111 -0
- commitguard/research/reproduction.py +297 -0
- commitguard/research/results.py +94 -0
- commitguard/rules/__init__.py +11 -0
- commitguard/rules/data/ai-domains.yaml +51 -0
- commitguard/rules/data/ai-identities.yaml +131 -0
- commitguard/rules/data/bot-identities.yaml +53 -0
- commitguard/rules/data/patterns.yaml +52 -0
- commitguard/rules/loader.py +102 -0
- commitguard/rules/matcher.py +212 -0
- commitguard/rules/models.py +269 -0
- commitguard/security/__init__.py +5 -0
- commitguard/security/hashing.py +30 -0
- commitguard/security/rate_limit.py +33 -0
- commitguard/security/safe_yaml.py +69 -0
- commitguard/security/sanitization.py +85 -0
- commitguard/security/secrets.py +169 -0
- commitguard/security/validation.py +89 -0
- commitguard/services/__init__.py +15 -0
- commitguard/services/analysis.py +119 -0
- commitguard/services/audit.py +95 -0
- commitguard/services/ci.py +383 -0
- commitguard/services/enforcement.py +102 -0
- commitguard/services/hooks.py +254 -0
- commitguard/services/remediation.py +99 -0
- commitguard/services/reports.py +146 -0
- commitguard/services/scan.py +172 -0
- commitguard/utils/__init__.py +1 -0
- commitguard/utils/filesystem.py +72 -0
- commitguard/utils/platform.py +35 -0
- commitguard/utils/subprocess.py +84 -0
- commitguardian-0.1.0.dist-info/METADATA +694 -0
- commitguardian-0.1.0.dist-info/RECORD +197 -0
- commitguardian-0.1.0.dist-info/WHEEL +4 -0
- commitguardian-0.1.0.dist-info/entry_points.txt +2 -0
- commitguardian-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""``commitguard ci github``: server-side enforcement inside GitHub Actions.
|
|
2
|
+
|
|
3
|
+
Reads ``GITHUB_EVENT_NAME`` / ``GITHUB_EVENT_PATH``, analyses the commits the
|
|
4
|
+
event introduces with the policy from a trusted commit, and exits:
|
|
5
|
+
|
|
6
|
+
* 0 - allowed (warnings are reported but pass unless ``--fail-on warn``);
|
|
7
|
+
* 1 - blocked by policy;
|
|
8
|
+
* 2 - CommitGuard could not complete the check (fails the job: fail closed).
|
|
9
|
+
|
|
10
|
+
No token, secret, network access or write permission is used.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Annotated
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
from commitguard.cli.output import ExitCode, OutputFormat, error
|
|
21
|
+
from commitguard.cli.render import render_ci_text, render_json
|
|
22
|
+
from commitguard.core.decision import Action
|
|
23
|
+
from commitguard.git.repository import Repository
|
|
24
|
+
from commitguard.github.actions import (
|
|
25
|
+
annotation_commands,
|
|
26
|
+
append_file,
|
|
27
|
+
commands_stopped,
|
|
28
|
+
render_step_summary,
|
|
29
|
+
running_in_github_actions,
|
|
30
|
+
step_outputs,
|
|
31
|
+
workflow_command,
|
|
32
|
+
)
|
|
33
|
+
from commitguard.github.checks import build_check_output
|
|
34
|
+
from commitguard.github.events import load_github_event
|
|
35
|
+
from commitguard.security.sanitization import sanitize_block
|
|
36
|
+
from commitguard.security.validation import validate_repository_path
|
|
37
|
+
from commitguard.services.ci import DEFAULT_CI_MAX_COMMITS
|
|
38
|
+
from commitguard.services.scan import ScanRequest, ScanService
|
|
39
|
+
from commitguard.utils.filesystem import atomic_write_text
|
|
40
|
+
|
|
41
|
+
ci_app = typer.Typer(help="Server-side enforcement in CI systems.", no_args_is_help=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _emit(line: str) -> None:
|
|
45
|
+
sys.stdout.write(line + "\n")
|
|
46
|
+
sys.stdout.flush()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _env_path(name: str) -> Path | None:
|
|
50
|
+
value = os.environ.get(name)
|
|
51
|
+
return Path(value) if value else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@ci_app.command("github")
|
|
55
|
+
def github_command(
|
|
56
|
+
event_name: Annotated[
|
|
57
|
+
str | None,
|
|
58
|
+
typer.Option("--event-name", help="Event name. Default: $GITHUB_EVENT_NAME."),
|
|
59
|
+
] = None,
|
|
60
|
+
event_path: Annotated[
|
|
61
|
+
Path | None,
|
|
62
|
+
typer.Option("--event-path", help="Event payload JSON. Default: $GITHUB_EVENT_PATH."),
|
|
63
|
+
] = None,
|
|
64
|
+
config: Annotated[
|
|
65
|
+
str | None,
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--config",
|
|
68
|
+
help="Repository-relative policy file, read from the TRUSTED commit "
|
|
69
|
+
"(never from the checked-out change). Default: .commitguard.yaml/.yml.",
|
|
70
|
+
),
|
|
71
|
+
] = None,
|
|
72
|
+
fail_on: Annotated[
|
|
73
|
+
Action,
|
|
74
|
+
typer.Option("--fail-on", help="Fail the check at this action or above: block or warn."),
|
|
75
|
+
] = Action.BLOCK,
|
|
76
|
+
max_commits: Annotated[
|
|
77
|
+
int, typer.Option("--max-commits", min=1, help="Fail if more commits would be analysed.")
|
|
78
|
+
] = DEFAULT_CI_MAX_COMMITS,
|
|
79
|
+
output_format: Annotated[
|
|
80
|
+
OutputFormat, typer.Option("--format", "-f", case_sensitive=False)
|
|
81
|
+
] = OutputFormat.TEXT,
|
|
82
|
+
report_file: Annotated[
|
|
83
|
+
Path | None, typer.Option("--report-file", help="Also write the JSON report here.")
|
|
84
|
+
] = None,
|
|
85
|
+
github_output: Annotated[
|
|
86
|
+
bool,
|
|
87
|
+
typer.Option(
|
|
88
|
+
"--github-output/--no-github-output",
|
|
89
|
+
help="Write annotations, job summary and step outputs when running in GitHub Actions.",
|
|
90
|
+
),
|
|
91
|
+
] = True,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Check the commits a pull request, merge group or push introduces."""
|
|
94
|
+
in_actions = github_output and running_in_github_actions()
|
|
95
|
+
try:
|
|
96
|
+
if fail_on is Action.ALLOW:
|
|
97
|
+
raise ValueError("--fail-on must be block or warn")
|
|
98
|
+
if config is not None:
|
|
99
|
+
validate_repository_path(config)
|
|
100
|
+
context = load_github_event(
|
|
101
|
+
event_name or os.environ.get("GITHUB_EVENT_NAME"),
|
|
102
|
+
event_path or _env_path("GITHUB_EVENT_PATH"),
|
|
103
|
+
)
|
|
104
|
+
result = ScanService().run(
|
|
105
|
+
ScanRequest(
|
|
106
|
+
repository=Repository.discover(),
|
|
107
|
+
context=context,
|
|
108
|
+
config_path=config,
|
|
109
|
+
max_commits=max_commits,
|
|
110
|
+
fail_on=fail_on,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
except Exception as exc: # noqa: BLE001 - any failure must fail the check
|
|
114
|
+
message = f"{type(exc).__name__}: {exc}" if not str(exc) else str(exc)
|
|
115
|
+
if in_actions:
|
|
116
|
+
_emit(
|
|
117
|
+
workflow_command(
|
|
118
|
+
"error",
|
|
119
|
+
f"Security validation could not be completed: {message}",
|
|
120
|
+
title="CommitGuard could not verify repository policy",
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
text = (
|
|
124
|
+
"CommitGuard could not verify repository policy.\n"
|
|
125
|
+
f"Reason: {sanitize_block(message, max_length=4000)}\n"
|
|
126
|
+
"Security validation could not be completed.\n"
|
|
127
|
+
"Result: FAILED"
|
|
128
|
+
)
|
|
129
|
+
if in_actions:
|
|
130
|
+
# The reason can quote untrusted YAML or payload text: print it to stdout
|
|
131
|
+
# with workflow commands disabled (stdout/stderr ordering is not guaranteed).
|
|
132
|
+
with commands_stopped(_emit):
|
|
133
|
+
_emit(text)
|
|
134
|
+
else:
|
|
135
|
+
error(text)
|
|
136
|
+
raise typer.Exit(code=int(ExitCode.ERROR)) from None
|
|
137
|
+
|
|
138
|
+
report = result.report
|
|
139
|
+
check = build_check_output(report, fail_on=fail_on)
|
|
140
|
+
failed = not result.enforcement.allowed
|
|
141
|
+
|
|
142
|
+
if output_format is OutputFormat.JSON:
|
|
143
|
+
_emit(render_json(report))
|
|
144
|
+
elif in_actions:
|
|
145
|
+
with commands_stopped(_emit):
|
|
146
|
+
_emit(render_ci_text(report, failed=failed))
|
|
147
|
+
else:
|
|
148
|
+
_emit(render_ci_text(report, failed=failed))
|
|
149
|
+
|
|
150
|
+
if report_file is not None:
|
|
151
|
+
atomic_write_text(report_file, render_json(report) + "\n", overwrite=True)
|
|
152
|
+
|
|
153
|
+
if in_actions:
|
|
154
|
+
if output_format is not OutputFormat.JSON:
|
|
155
|
+
for line in annotation_commands(check):
|
|
156
|
+
_emit(line)
|
|
157
|
+
summary = _env_path("GITHUB_STEP_SUMMARY")
|
|
158
|
+
if summary is not None:
|
|
159
|
+
append_file(summary, render_step_summary(report, check))
|
|
160
|
+
outputs = _env_path("GITHUB_OUTPUT")
|
|
161
|
+
if outputs is not None:
|
|
162
|
+
append_file(outputs, step_outputs(report, check))
|
|
163
|
+
|
|
164
|
+
if failed:
|
|
165
|
+
raise typer.Exit(code=int(ExitCode.BLOCKED))
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""``commitguard dashboard``: operator tasks for the dashboard control plane.
|
|
2
|
+
|
|
3
|
+
* ``members list`` - roles in an organization;
|
|
4
|
+
* ``members grant`` - grant or change a role (bootstraps the first owner);
|
|
5
|
+
* ``members revoke`` - remove a role.
|
|
6
|
+
|
|
7
|
+
The commands work on the service's state database (``COMMITGUARD_APP_DATA_DIR``)
|
|
8
|
+
and need no network access. Users are named by numeric GitHub user ID (find it
|
|
9
|
+
with ``gh api users/<login> --jq .id``): logins can be renamed and re-registered.
|
|
10
|
+
Every change is recorded as an audit event with the actor ``commitguard-cli``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Annotated
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from commitguard.cli.output import fail, handled_errors, info
|
|
20
|
+
from commitguard.exceptions.base import CommitGuardError
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING: # imported lazily: the CLI must not load sqlite3 for unrelated commands
|
|
23
|
+
from commitguard.controlplane.members import MembershipService
|
|
24
|
+
from commitguard.github.storage import SqliteStateStore
|
|
25
|
+
|
|
26
|
+
dashboard_app = typer.Typer(
|
|
27
|
+
help="Dashboard control plane: organization members and roles.", no_args_is_help=True
|
|
28
|
+
)
|
|
29
|
+
members_app = typer.Typer(help="Organization members and their roles.", no_args_is_help=True)
|
|
30
|
+
dashboard_app.add_typer(members_app, name="members")
|
|
31
|
+
|
|
32
|
+
CLI_ACTOR_LOGIN = "commitguard-cli"
|
|
33
|
+
_ROLES = ("viewer", "security_manager", "admin", "owner")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _services() -> "tuple[SqliteStateStore, MembershipService]":
|
|
37
|
+
from commitguard.controlplane.members import MembershipService
|
|
38
|
+
from commitguard.github.settings import ENV_DATA_DIR
|
|
39
|
+
from commitguard.github.storage import DATABASE_FILENAME, SqliteStateStore
|
|
40
|
+
from commitguard.services.audit import AuditService
|
|
41
|
+
|
|
42
|
+
raw = os.environ.get(ENV_DATA_DIR)
|
|
43
|
+
if not raw or not Path(raw).is_absolute():
|
|
44
|
+
raise CommitGuardError(
|
|
45
|
+
f"{ENV_DATA_DIR} must be set to the service's absolute data directory"
|
|
46
|
+
)
|
|
47
|
+
database = Path(raw) / DATABASE_FILENAME
|
|
48
|
+
if not database.is_file():
|
|
49
|
+
raise CommitGuardError(f"no CommitGuard state database in {ENV_DATA_DIR}")
|
|
50
|
+
store = SqliteStateStore(database)
|
|
51
|
+
audit = AuditService([store])
|
|
52
|
+
return store, MembershipService(store, audit)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _organization(members: "MembershipService", value: str) -> tuple[int, str, str]:
|
|
56
|
+
account = (
|
|
57
|
+
members.account(int(value))
|
|
58
|
+
if value.isascii() and value.isdigit()
|
|
59
|
+
else members.account_by_login(value)
|
|
60
|
+
)
|
|
61
|
+
if account is None:
|
|
62
|
+
raise CommitGuardError(
|
|
63
|
+
f"no GitHub App installation is known for organization {value!r} "
|
|
64
|
+
"(install the App, or let the service receive its installation event first)"
|
|
65
|
+
)
|
|
66
|
+
return account
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
OrganizationOption = Annotated[
|
|
70
|
+
str, typer.Option("--organization", help="Organization login or numeric account ID.")
|
|
71
|
+
]
|
|
72
|
+
UserIdOption = Annotated[int, typer.Option("--user-id", min=1, help="Numeric GitHub user ID.")]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@members_app.command("list")
|
|
76
|
+
def list_command(organization: OrganizationOption) -> None:
|
|
77
|
+
"""List the members of an organization."""
|
|
78
|
+
with handled_errors():
|
|
79
|
+
store, members = _services()
|
|
80
|
+
try:
|
|
81
|
+
account = _organization(members, organization)
|
|
82
|
+
rows = members.list_members(account[0], limit=1000)
|
|
83
|
+
finally:
|
|
84
|
+
store.close()
|
|
85
|
+
info(f"{account[1]} ({account[2]}, account {account[0]})")
|
|
86
|
+
if not rows:
|
|
87
|
+
info(" no members")
|
|
88
|
+
for member in rows:
|
|
89
|
+
label = member.login or "(not signed in yet)"
|
|
90
|
+
suffix = " [implicit]" if member.implicit else ""
|
|
91
|
+
info(f" {member.user_id:<12} {label:<40} {member.role}{suffix}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@members_app.command("grant")
|
|
95
|
+
def grant_command(
|
|
96
|
+
organization: OrganizationOption,
|
|
97
|
+
user_id: UserIdOption,
|
|
98
|
+
role: Annotated[str, typer.Option("--role", help="viewer, security_manager, admin or owner.")],
|
|
99
|
+
login: Annotated[
|
|
100
|
+
str | None, typer.Option("--login", help="GitHub login, for display until they sign in.")
|
|
101
|
+
] = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Grant a role (or change an existing one)."""
|
|
104
|
+
from commitguard.audit.models import Actor, ActorType
|
|
105
|
+
from commitguard.controlplane.access import Role
|
|
106
|
+
|
|
107
|
+
if role not in _ROLES:
|
|
108
|
+
fail(f"--role must be one of: {', '.join(_ROLES)}")
|
|
109
|
+
with handled_errors():
|
|
110
|
+
store, members = _services()
|
|
111
|
+
try:
|
|
112
|
+
account = _organization(members, organization)
|
|
113
|
+
member = members.grant(
|
|
114
|
+
account_id=account[0],
|
|
115
|
+
user_id=user_id,
|
|
116
|
+
role=Role(role),
|
|
117
|
+
actor=Actor(type=ActorType.SYSTEM, login=CLI_ACTOR_LOGIN),
|
|
118
|
+
login=login,
|
|
119
|
+
)
|
|
120
|
+
finally:
|
|
121
|
+
store.close()
|
|
122
|
+
info(f"Granted {member.role} in {account[1]} to user {member.user_id}.")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@members_app.command("revoke")
|
|
126
|
+
def revoke_command(organization: OrganizationOption, user_id: UserIdOption) -> None:
|
|
127
|
+
"""Remove a member's role."""
|
|
128
|
+
from commitguard.audit.models import Actor, ActorType
|
|
129
|
+
|
|
130
|
+
with handled_errors():
|
|
131
|
+
store, members = _services()
|
|
132
|
+
try:
|
|
133
|
+
account = _organization(members, organization)
|
|
134
|
+
members.remove(
|
|
135
|
+
account_id=account[0],
|
|
136
|
+
user_id=user_id,
|
|
137
|
+
actor=Actor(type=ActorType.SYSTEM, login=CLI_ACTOR_LOGIN),
|
|
138
|
+
)
|
|
139
|
+
finally:
|
|
140
|
+
store.close()
|
|
141
|
+
info(f"Removed user {user_id} from {account[1]}.")
|