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,1085 @@
|
|
|
1
|
+
"""The ``/api/v1`` routes of organization governance.
|
|
2
|
+
|
|
3
|
+
Every route is authenticated and CSRF-checked by :class:`~commitguard.api.app.DashboardApi`.
|
|
4
|
+
Authorization happens in the governance services, per resource: the caller must
|
|
5
|
+
be a member of the resource's organization (otherwise the answer is 404, exactly
|
|
6
|
+
like a missing resource) and hold the permission the operation needs
|
|
7
|
+
(otherwise 403). Nested resources are resolved from the stored row - a group's,
|
|
8
|
+
draft's or exception's organization is never taken from the URL or the body.
|
|
9
|
+
|
|
10
|
+
====================================================== ==================================
|
|
11
|
+
Route Permission (checked in services)
|
|
12
|
+
====================================================== ==================================
|
|
13
|
+
GET /organizations/{id} organization:read / security:read
|
|
14
|
+
GET PUT /organizations/{id}/settings organization:read / :manage
|
|
15
|
+
GET /organizations/{id}/security/overview|repositories security:read
|
|
16
|
+
GET /organizations/{id}/security/policies|exceptions policies:read / exceptions:read
|
|
17
|
+
GET /organizations/{id}/security/trends|events security:read
|
|
18
|
+
POST /organizations/{id}/security/events/{e}/acknowledge violations:manage
|
|
19
|
+
GET /organizations/{id}/reports/{kind} security:read (policy_changes: audit)
|
|
20
|
+
GET /organizations/{id}/search organization:read (+ per kind)
|
|
21
|
+
POST /organizations/{id}/repositories/onboard|mode repositories:manage
|
|
22
|
+
GET /repositories/{r}/effective-policy policies:read
|
|
23
|
+
GET POST /organizations/{id}/repository-groups repositories:read / :manage
|
|
24
|
+
GET PATCH DELETE /repository-groups/{g} repositories:read / :manage
|
|
25
|
+
POST /repository-groups/{g}/repositories[/remove] repositories:manage
|
|
26
|
+
GET /organizations/{id}/policies policies:read
|
|
27
|
+
GET /policy-targets/{type}/{id}/versions policies:read
|
|
28
|
+
POST /policy-targets/{type}/{id}/rollback policies:rollback
|
|
29
|
+
GET POST /organizations/{id}/policy-drafts policies:read / :write
|
|
30
|
+
GET PATCH /policy-drafts/{d} policies:read / :write
|
|
31
|
+
POST /policy-drafts/{d}/submit|cancel policies:write
|
|
32
|
+
POST /policy-drafts/{d}/approve|reject policies:approve (not the author)
|
|
33
|
+
POST /policy-drafts/{d}/publish policies:publish
|
|
34
|
+
POST /policy-drafts/{d}/emergency-publish policies:emergency
|
|
35
|
+
POST /policy-drafts/{d}/simulations policies:write
|
|
36
|
+
GET /organizations/{id}/simulations, /simulations/{s} policies:read
|
|
37
|
+
GET /organizations/{id}/rollouts, /rollouts/{r} policies:read
|
|
38
|
+
POST /rollouts/{r}/advance|pause|resume policies:publish
|
|
39
|
+
POST /rollouts/{r}/rollback policies:rollback
|
|
40
|
+
GET /organizations/{id}/policy-propagation policies:read
|
|
41
|
+
GET POST /organizations/{id}/exceptions exceptions:read / :create
|
|
42
|
+
GET /exceptions/{x} exceptions:read
|
|
43
|
+
POST /exceptions/{x}/approve|reject exceptions:approve (not the requester)
|
|
44
|
+
POST /exceptions/{x}/revoke exceptions:revoke
|
|
45
|
+
POST /exceptions/{x}/cancel requester or exceptions:revoke
|
|
46
|
+
GET PUT /organizations/{id}/rules, .../rules/history rules:read / rules:manage
|
|
47
|
+
GET POST /organizations/{id}/bulk-operations repositories:read / :manage
|
|
48
|
+
GET /bulk-operations/{b}; POST .../cancel|retry repositories:read / :manage
|
|
49
|
+
GET POST /organizations/{id}/scan-schedules security:read / :manage
|
|
50
|
+
GET PATCH /scan-schedules/{s}; POST .../disable security:read / :manage
|
|
51
|
+
====================================================== ==================================
|
|
52
|
+
|
|
53
|
+
Security exceptions are never deleted through the API: they are revoked,
|
|
54
|
+
rejected, cancelled or expire, and the history is kept. Groups are archived.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from collections.abc import Callable
|
|
58
|
+
from typing import Any
|
|
59
|
+
|
|
60
|
+
from commitguard.api.http import ApiError, Request, Response, bad_request, ok
|
|
61
|
+
from commitguard.audit.models import Actor
|
|
62
|
+
from commitguard.controlplane.access import Permission, Principal
|
|
63
|
+
from commitguard.controlplane.errors import NotFoundError
|
|
64
|
+
from commitguard.controlplane.pagination import encode_cursor, offset_cursor, parse_limit
|
|
65
|
+
from commitguard.controlplane.policies import (
|
|
66
|
+
ORGANIZATION_TARGET,
|
|
67
|
+
PolicyTarget,
|
|
68
|
+
PolicyTargetType,
|
|
69
|
+
canonical_document,
|
|
70
|
+
)
|
|
71
|
+
from commitguard.controlplane.views import OrganizationRef
|
|
72
|
+
from commitguard.governance.common import (
|
|
73
|
+
account_repository,
|
|
74
|
+
repository_ids,
|
|
75
|
+
require,
|
|
76
|
+
visible_repository_ids,
|
|
77
|
+
)
|
|
78
|
+
from commitguard.governance.rollouts import parse_stages
|
|
79
|
+
from commitguard.governance.service import GovernanceServices
|
|
80
|
+
from commitguard.governance.workflow import parse_rules, parse_target
|
|
81
|
+
|
|
82
|
+
type RouteRow = tuple[str, str, Callable[[Request], Response], Permission | None, bool, str]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _principal(request: Request) -> Principal:
|
|
86
|
+
if request.principal is None: # pragma: no cover - guaranteed by the dispatcher
|
|
87
|
+
raise ApiError(401, "UNAUTHENTICATED", "Sign in to continue.")
|
|
88
|
+
return request.principal
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _flag(value: object, field: str, default: bool = False) -> bool:
|
|
92
|
+
if value is None:
|
|
93
|
+
return default
|
|
94
|
+
if not isinstance(value, bool):
|
|
95
|
+
raise bad_request(f"{field} must be true or false", field)
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _optional_ids(value: object, field: str) -> list[int] | None:
|
|
100
|
+
return None if value is None else repository_ids(value, field)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class GovernanceRoutes:
|
|
104
|
+
def __init__(self, governance: GovernanceServices) -> None:
|
|
105
|
+
self.g = governance
|
|
106
|
+
|
|
107
|
+
# -- organization ----------------------------------------------------- #
|
|
108
|
+
def organization(self, request: Request) -> Response:
|
|
109
|
+
principal = _principal(request)
|
|
110
|
+
account_id = request.params["organization_id"]
|
|
111
|
+
require(principal, Permission.ORGANIZATION_READ, account_id)
|
|
112
|
+
overview = self.g.posture.overview(principal, account_id)
|
|
113
|
+
settings = self.g.settings.view(principal, account_id)
|
|
114
|
+
return ok({"organization": overview, "settings": settings})
|
|
115
|
+
|
|
116
|
+
def get_settings(self, request: Request) -> Response:
|
|
117
|
+
principal = _principal(request)
|
|
118
|
+
return ok(self.g.settings.view(principal, request.params["organization_id"]))
|
|
119
|
+
|
|
120
|
+
def put_settings(self, request: Request) -> Response:
|
|
121
|
+
principal = _principal(request)
|
|
122
|
+
body = request.json()
|
|
123
|
+
view = self.g.settings.update(
|
|
124
|
+
principal,
|
|
125
|
+
request.params["organization_id"],
|
|
126
|
+
expected_version=body.get("expected_version"),
|
|
127
|
+
changes=body.get("settings"),
|
|
128
|
+
reason=body.get("reason"),
|
|
129
|
+
confirm=body.get("confirm", False),
|
|
130
|
+
)
|
|
131
|
+
return ok(view)
|
|
132
|
+
|
|
133
|
+
def security_overview(self, request: Request) -> Response:
|
|
134
|
+
return ok(self.g.posture.overview(_principal(request), request.params["organization_id"]))
|
|
135
|
+
|
|
136
|
+
def security_repositories(self, request: Request) -> Response:
|
|
137
|
+
principal = _principal(request)
|
|
138
|
+
offset = offset_cursor(request.arg("cursor"))
|
|
139
|
+
limit = parse_limit(request.arg("limit"))
|
|
140
|
+
names = (
|
|
141
|
+
"q",
|
|
142
|
+
"group",
|
|
143
|
+
"posture",
|
|
144
|
+
"protection",
|
|
145
|
+
"mode",
|
|
146
|
+
"onboarding",
|
|
147
|
+
"policy_state",
|
|
148
|
+
"drift",
|
|
149
|
+
"exceptions",
|
|
150
|
+
"severity",
|
|
151
|
+
"last_scan",
|
|
152
|
+
"sort",
|
|
153
|
+
)
|
|
154
|
+
page = self.g.posture.matrix(
|
|
155
|
+
principal,
|
|
156
|
+
request.params["organization_id"],
|
|
157
|
+
filters={name: request.arg(name) for name in names},
|
|
158
|
+
offset=offset,
|
|
159
|
+
limit=limit,
|
|
160
|
+
)
|
|
161
|
+
return ok(
|
|
162
|
+
list(page.items),
|
|
163
|
+
{
|
|
164
|
+
"next_cursor": page.next_cursor,
|
|
165
|
+
"limit": limit,
|
|
166
|
+
"total": page.total,
|
|
167
|
+
"computed_at": page.computed_at.isoformat(),
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def security_policies(self, request: Request) -> Response:
|
|
172
|
+
principal = _principal(request)
|
|
173
|
+
account_id = request.params["organization_id"]
|
|
174
|
+
require(principal, Permission.POLICIES_READ, account_id)
|
|
175
|
+
return ok(
|
|
176
|
+
{
|
|
177
|
+
"targets": self._policy_targets(principal, account_id),
|
|
178
|
+
"drafts": self.g.workflow.list_drafts(principal, account_id),
|
|
179
|
+
"rollouts": self.g.rollouts.list_rollouts(principal, account_id),
|
|
180
|
+
"propagation": self.g.resolver.propagation_status(principal, account_id),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def security_exceptions(self, request: Request) -> Response:
|
|
185
|
+
principal = _principal(request)
|
|
186
|
+
account_id = request.params["organization_id"]
|
|
187
|
+
items, _ = self.g.exceptions.list(principal, account_id, limit=500)
|
|
188
|
+
counts: dict[str, int] = {}
|
|
189
|
+
for item in items:
|
|
190
|
+
counts[item.status] = counts.get(item.status, 0) + 1
|
|
191
|
+
counts["expiring_soon"] = sum(1 for item in items if item.expiring_soon)
|
|
192
|
+
return ok({"counts": counts, "exceptions": items})
|
|
193
|
+
|
|
194
|
+
def security_trends(self, request: Request) -> Response:
|
|
195
|
+
raw = request.arg("days") or "30"
|
|
196
|
+
if not raw.isdigit() or len(raw) > 3:
|
|
197
|
+
raise bad_request("days must be a number", "days")
|
|
198
|
+
return ok(
|
|
199
|
+
self.g.posture.trends(
|
|
200
|
+
_principal(request), request.params["organization_id"], days=int(raw)
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def security_events(self, request: Request) -> Response:
|
|
205
|
+
return ok(
|
|
206
|
+
self.g.posture.security_events(_principal(request), request.params["organization_id"])
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def acknowledge_event(self, request: Request) -> Response:
|
|
210
|
+
body = request.json()
|
|
211
|
+
return ok(
|
|
212
|
+
self.g.posture.acknowledge_event(
|
|
213
|
+
_principal(request),
|
|
214
|
+
request.params["organization_id"],
|
|
215
|
+
request.params["event_id"],
|
|
216
|
+
body.get("note"),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def report(self, request: Request) -> Response:
|
|
221
|
+
principal = _principal(request)
|
|
222
|
+
content_type, body, filename = self.g.posture.report(
|
|
223
|
+
principal,
|
|
224
|
+
request.params["organization_id"],
|
|
225
|
+
kind=request.params["kind"],
|
|
226
|
+
fmt=request.arg("format") or "json",
|
|
227
|
+
)
|
|
228
|
+
return Response(
|
|
229
|
+
200,
|
|
230
|
+
body,
|
|
231
|
+
[
|
|
232
|
+
("Content-Type", content_type),
|
|
233
|
+
("Content-Disposition", f'attachment; filename="{filename}"'),
|
|
234
|
+
],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def search(self, request: Request) -> Response:
|
|
238
|
+
results = self.g.posture.search(
|
|
239
|
+
_principal(request), request.params["organization_id"], request.arg("q") or ""
|
|
240
|
+
)
|
|
241
|
+
return ok(results)
|
|
242
|
+
|
|
243
|
+
# -- repositories ----------------------------------------------------- #
|
|
244
|
+
def onboard(self, request: Request) -> Response:
|
|
245
|
+
require(
|
|
246
|
+
_principal(request), Permission.REPOSITORIES_MANAGE, request.params["organization_id"]
|
|
247
|
+
)
|
|
248
|
+
body = request.json()
|
|
249
|
+
changed = self.g.inventory.onboard(
|
|
250
|
+
_principal(request),
|
|
251
|
+
request.params["organization_id"],
|
|
252
|
+
repository_ids(body.get("repository_ids")),
|
|
253
|
+
mode=body.get("mode"),
|
|
254
|
+
confirm=body.get("confirm", False),
|
|
255
|
+
reason=body.get("reason"),
|
|
256
|
+
)
|
|
257
|
+
return ok({"changed": changed})
|
|
258
|
+
|
|
259
|
+
def set_mode(self, request: Request) -> Response:
|
|
260
|
+
require(
|
|
261
|
+
_principal(request), Permission.REPOSITORIES_MANAGE, request.params["organization_id"]
|
|
262
|
+
)
|
|
263
|
+
body = request.json()
|
|
264
|
+
changed = self.g.inventory.set_mode(
|
|
265
|
+
_principal(request),
|
|
266
|
+
request.params["organization_id"],
|
|
267
|
+
repository_ids(body.get("repository_ids")),
|
|
268
|
+
mode=body.get("mode"),
|
|
269
|
+
confirm=body.get("confirm", False),
|
|
270
|
+
reason=body.get("reason"),
|
|
271
|
+
)
|
|
272
|
+
return ok({"changed": changed})
|
|
273
|
+
|
|
274
|
+
def _repository_account(self, principal: Principal, repository_id: int) -> int:
|
|
275
|
+
"""The organization of a repository the caller can see (else 404)."""
|
|
276
|
+
for account_id in principal.accounts_with(Permission.REPOSITORIES_READ):
|
|
277
|
+
if repository_id in visible_repository_ids(
|
|
278
|
+
self.g.store, principal, account_id
|
|
279
|
+
) and account_repository(self.g.store, account_id, repository_id):
|
|
280
|
+
return account_id
|
|
281
|
+
raise NotFoundError()
|
|
282
|
+
|
|
283
|
+
def effective_policy(self, request: Request) -> Response:
|
|
284
|
+
principal = _principal(request)
|
|
285
|
+
repository_id = request.params["repository_id"]
|
|
286
|
+
account_id = self._repository_account(principal, repository_id)
|
|
287
|
+
view = self.g.resolver.effective_view(principal, account_id, repository_id)
|
|
288
|
+
active, soon = self.g.exceptions.counts(
|
|
289
|
+
account_id, repository_id, [g for g in view.versions.groups]
|
|
290
|
+
)
|
|
291
|
+
return ok(view, {"exceptions": {"active": active, "expiring_soon": soon}})
|
|
292
|
+
|
|
293
|
+
# -- groups ----------------------------------------------------------- #
|
|
294
|
+
def list_groups(self, request: Request) -> Response:
|
|
295
|
+
archived = request.arg("archived") == "true"
|
|
296
|
+
return ok(
|
|
297
|
+
self.g.groups.list_groups(
|
|
298
|
+
_principal(request), request.params["organization_id"], include_archived=archived
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def create_group(self, request: Request) -> Response:
|
|
303
|
+
body = request.json()
|
|
304
|
+
view = self.g.groups.create(
|
|
305
|
+
_principal(request),
|
|
306
|
+
request.params["organization_id"],
|
|
307
|
+
name=body.get("name"),
|
|
308
|
+
description=body.get("description"),
|
|
309
|
+
)
|
|
310
|
+
return ok(view, status=201)
|
|
311
|
+
|
|
312
|
+
def get_group(self, request: Request) -> Response:
|
|
313
|
+
return ok(self.g.groups.get(_principal(request), request.params["group_id"]))
|
|
314
|
+
|
|
315
|
+
def patch_group(self, request: Request) -> Response:
|
|
316
|
+
body = request.json()
|
|
317
|
+
return ok(
|
|
318
|
+
self.g.groups.update(
|
|
319
|
+
_principal(request),
|
|
320
|
+
request.params["group_id"],
|
|
321
|
+
name=body.get("name"),
|
|
322
|
+
description=body.get("description"),
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def archive_group(self, request: Request) -> Response:
|
|
327
|
+
confirm = request.arg("confirm") == "true"
|
|
328
|
+
self.g.groups.archive(_principal(request), request.params["group_id"], confirm=confirm)
|
|
329
|
+
return ok({"archived": True})
|
|
330
|
+
|
|
331
|
+
def add_group_members(self, request: Request) -> Response:
|
|
332
|
+
self.g.groups.account_of(
|
|
333
|
+
_principal(request), request.params["group_id"], Permission.REPOSITORIES_MANAGE
|
|
334
|
+
)
|
|
335
|
+
body = request.json()
|
|
336
|
+
return ok(
|
|
337
|
+
self.g.groups.add_members(
|
|
338
|
+
_principal(request),
|
|
339
|
+
request.params["group_id"],
|
|
340
|
+
repository_ids(body.get("repository_ids")),
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def remove_group_members(self, request: Request) -> Response:
|
|
345
|
+
self.g.groups.account_of(
|
|
346
|
+
_principal(request), request.params["group_id"], Permission.REPOSITORIES_MANAGE
|
|
347
|
+
)
|
|
348
|
+
body = request.json()
|
|
349
|
+
return ok(
|
|
350
|
+
self.g.groups.remove_members(
|
|
351
|
+
_principal(request),
|
|
352
|
+
request.params["group_id"],
|
|
353
|
+
repository_ids(body.get("repository_ids")),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# -- policies --------------------------------------------------------- #
|
|
358
|
+
def _organization_ref(self, principal: Principal, account_id: int) -> OrganizationRef:
|
|
359
|
+
membership = principal.memberships.get(account_id)
|
|
360
|
+
if membership is None:
|
|
361
|
+
raise NotFoundError()
|
|
362
|
+
return OrganizationRef(
|
|
363
|
+
id=account_id, login=membership.account_login, type=membership.account_type
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def _policy_targets(self, principal: Principal, account_id: int) -> dict[str, Any]:
|
|
367
|
+
organization = self._organization_ref(principal, account_id)
|
|
368
|
+
can_write = principal.can(Permission.POLICIES_WRITE, account_id)
|
|
369
|
+
groups = self.g.groups.list_groups(principal, account_id)
|
|
370
|
+
visible = visible_repository_ids(self.g.store, principal, account_id)
|
|
371
|
+
repository_targets = [
|
|
372
|
+
repository_id
|
|
373
|
+
for repository_id in self.g.policies.repository_targets(account_id)
|
|
374
|
+
if repository_id in visible
|
|
375
|
+
]
|
|
376
|
+
return {
|
|
377
|
+
"organization": self.g.policies.view(organization, can_write=can_write),
|
|
378
|
+
"groups": [
|
|
379
|
+
self.g.policies.scoped_view(
|
|
380
|
+
organization, PolicyTarget.group(group.id), can_write=can_write
|
|
381
|
+
)
|
|
382
|
+
for group in groups
|
|
383
|
+
],
|
|
384
|
+
"repositories": [
|
|
385
|
+
self.g.policies.scoped_view(
|
|
386
|
+
organization, PolicyTarget.repository(repository_id), can_write=can_write
|
|
387
|
+
)
|
|
388
|
+
for repository_id in repository_targets
|
|
389
|
+
],
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def list_policy_targets(self, request: Request) -> Response:
|
|
393
|
+
principal = _principal(request)
|
|
394
|
+
account_id = request.params["organization_id"]
|
|
395
|
+
require(principal, Permission.POLICIES_READ, account_id)
|
|
396
|
+
return ok(self._policy_targets(principal, account_id))
|
|
397
|
+
|
|
398
|
+
def _target(
|
|
399
|
+
self,
|
|
400
|
+
request: Request,
|
|
401
|
+
principal: Principal,
|
|
402
|
+
permission: Permission = Permission.POLICIES_READ,
|
|
403
|
+
) -> tuple[int, PolicyTarget]:
|
|
404
|
+
account_id = request.params["organization_id"]
|
|
405
|
+
require(principal, permission, account_id) # before parsing: no validation oracle
|
|
406
|
+
target = parse_target(request.params["target_type"], request.arg("target_id") or "")
|
|
407
|
+
if target.type is PolicyTargetType.REPOSITORY and int(target.id) not in (
|
|
408
|
+
visible_repository_ids(self.g.store, principal, account_id)
|
|
409
|
+
):
|
|
410
|
+
raise NotFoundError()
|
|
411
|
+
if self.g.policies.target_label(account_id, target) is None:
|
|
412
|
+
raise NotFoundError()
|
|
413
|
+
return account_id, target
|
|
414
|
+
|
|
415
|
+
def policy_target_versions(self, request: Request) -> Response:
|
|
416
|
+
principal = _principal(request)
|
|
417
|
+
account_id, target = self._target(request, principal)
|
|
418
|
+
offset = offset_cursor(request.arg("cursor"))
|
|
419
|
+
limit = parse_limit(request.arg("limit"))
|
|
420
|
+
versions = self.g.policies.versions(
|
|
421
|
+
account_id, offset=offset, limit=limit + 1, target=target
|
|
422
|
+
)
|
|
423
|
+
next_cursor = encode_cursor([offset + limit]) if len(versions) > limit else None
|
|
424
|
+
organization = self._organization_ref(principal, account_id)
|
|
425
|
+
view = (
|
|
426
|
+
self.g.policies.scoped_view(
|
|
427
|
+
organization, target, can_write=principal.can(Permission.POLICIES_WRITE, account_id)
|
|
428
|
+
)
|
|
429
|
+
if target.scoped
|
|
430
|
+
else self.g.policies.view(
|
|
431
|
+
organization, can_write=principal.can(Permission.POLICIES_WRITE, account_id)
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
return ok(
|
|
435
|
+
{"policy": view, "versions": versions[:limit]},
|
|
436
|
+
{"next_cursor": next_cursor, "limit": limit},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def policy_target_rollback(self, request: Request) -> Response:
|
|
440
|
+
principal = _principal(request)
|
|
441
|
+
account_id, target = self._target(request, principal, Permission.POLICIES_ROLLBACK)
|
|
442
|
+
body = request.json()
|
|
443
|
+
target_version = body.get("target_version")
|
|
444
|
+
expected = body.get("expected_current_version")
|
|
445
|
+
checks = (("target_version", target_version), ("expected_current_version", expected))
|
|
446
|
+
for name, value in checks:
|
|
447
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
448
|
+
raise bad_request(f"{name} must be a non-negative integer", name)
|
|
449
|
+
created, diff = self.g.policies.rollback(
|
|
450
|
+
account_id=account_id,
|
|
451
|
+
actor=Actor.user(principal.user_id, principal.login),
|
|
452
|
+
authenticated_at=principal.authenticated_at,
|
|
453
|
+
target_version=int(target_version), # type: ignore[arg-type]
|
|
454
|
+
expected_current_version=int(expected), # type: ignore[arg-type]
|
|
455
|
+
reason=body.get("reason") if isinstance(body.get("reason"), str) else None,
|
|
456
|
+
confirm=_flag(body.get("confirm"), "confirm"),
|
|
457
|
+
target=target,
|
|
458
|
+
)
|
|
459
|
+
return ok({"version": created.version, "restored_version": target_version, "diff": diff})
|
|
460
|
+
|
|
461
|
+
# -- drafts ----------------------------------------------------------- #
|
|
462
|
+
def list_drafts(self, request: Request) -> Response:
|
|
463
|
+
return ok(
|
|
464
|
+
self.g.workflow.list_drafts(
|
|
465
|
+
_principal(request), request.params["organization_id"], state=request.arg("state")
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def create_draft(self, request: Request) -> Response:
|
|
470
|
+
require(_principal(request), Permission.POLICIES_WRITE, request.params["organization_id"])
|
|
471
|
+
body = request.json()
|
|
472
|
+
floors, defaults = parse_rules(body)
|
|
473
|
+
view = self.g.workflow.create(
|
|
474
|
+
_principal(request),
|
|
475
|
+
request.params["organization_id"],
|
|
476
|
+
target_type=body.get("target_type"),
|
|
477
|
+
target_id=body.get("target_id"),
|
|
478
|
+
floors=floors,
|
|
479
|
+
defaults=defaults,
|
|
480
|
+
title=body.get("title"),
|
|
481
|
+
reason=body.get("reason"),
|
|
482
|
+
)
|
|
483
|
+
return ok(view, status=201)
|
|
484
|
+
|
|
485
|
+
def get_draft(self, request: Request) -> Response:
|
|
486
|
+
return ok(self.g.workflow.get(_principal(request), request.params["draft_id"]))
|
|
487
|
+
|
|
488
|
+
def patch_draft(self, request: Request) -> Response:
|
|
489
|
+
self.g.workflow.authorize(
|
|
490
|
+
_principal(request), request.params["draft_id"], Permission.POLICIES_WRITE
|
|
491
|
+
)
|
|
492
|
+
body = request.json()
|
|
493
|
+
floors = defaults = None
|
|
494
|
+
if "floors" in body or "defaults" in body:
|
|
495
|
+
floors, defaults = parse_rules(body)
|
|
496
|
+
return ok(
|
|
497
|
+
self.g.workflow.update(
|
|
498
|
+
_principal(request),
|
|
499
|
+
request.params["draft_id"],
|
|
500
|
+
expected_revision=body.get("expected_revision"),
|
|
501
|
+
floors=floors,
|
|
502
|
+
defaults=defaults,
|
|
503
|
+
title=body.get("title"),
|
|
504
|
+
reason=body.get("reason"),
|
|
505
|
+
rebase=_flag(body.get("rebase"), "rebase"),
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def submit_draft(self, request: Request) -> Response:
|
|
510
|
+
return ok(self.g.workflow.submit(_principal(request), request.params["draft_id"]))
|
|
511
|
+
|
|
512
|
+
def approve_draft(self, request: Request) -> Response:
|
|
513
|
+
body = request.json()
|
|
514
|
+
return ok(
|
|
515
|
+
self.g.workflow.decide(
|
|
516
|
+
_principal(request),
|
|
517
|
+
request.params["draft_id"],
|
|
518
|
+
approve=True,
|
|
519
|
+
reason=body.get("reason"),
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def reject_draft(self, request: Request) -> Response:
|
|
524
|
+
body = request.json()
|
|
525
|
+
return ok(
|
|
526
|
+
self.g.workflow.decide(
|
|
527
|
+
_principal(request),
|
|
528
|
+
request.params["draft_id"],
|
|
529
|
+
approve=False,
|
|
530
|
+
reason=body.get("reason"),
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
def cancel_draft(self, request: Request) -> Response:
|
|
535
|
+
return ok(self.g.workflow.cancel(_principal(request), request.params["draft_id"]))
|
|
536
|
+
|
|
537
|
+
def _rollout_hook(self, principal: Principal, raw: object): # type: ignore[no-untyped-def]
|
|
538
|
+
if raw is None:
|
|
539
|
+
return None
|
|
540
|
+
if not isinstance(raw, dict):
|
|
541
|
+
raise bad_request("rollout must be an object", "rollout")
|
|
542
|
+
thresholds_raw = raw.get("thresholds")
|
|
543
|
+
thresholds: dict[str, float] | None = None
|
|
544
|
+
if thresholds_raw is not None:
|
|
545
|
+
if not isinstance(thresholds_raw, dict):
|
|
546
|
+
raise bad_request("rollout.thresholds must be an object", "rollout.thresholds")
|
|
547
|
+
thresholds = {}
|
|
548
|
+
for key in ("max_error_rate", "max_block_rate", "min_scans"):
|
|
549
|
+
value = thresholds_raw.get(key)
|
|
550
|
+
if value is None:
|
|
551
|
+
continue
|
|
552
|
+
if isinstance(value, bool) or not isinstance(value, int | float) or value < 0:
|
|
553
|
+
raise bad_request(f"rollout.thresholds.{key} must be a number", key)
|
|
554
|
+
if key != "min_scans" and value > 1:
|
|
555
|
+
raise bad_request(f"rollout.thresholds.{key} must be at most 1", key)
|
|
556
|
+
thresholds[key] = float(value)
|
|
557
|
+
return self.g.rollouts.creator(
|
|
558
|
+
principal,
|
|
559
|
+
stages=parse_stages(raw.get("stages")),
|
|
560
|
+
thresholds=thresholds,
|
|
561
|
+
auto_pause=_flag(raw.get("auto_pause"), "rollout.auto_pause", True)
|
|
562
|
+
if raw.get("auto_pause") is not None
|
|
563
|
+
else None,
|
|
564
|
+
auto_rollback=_flag(raw.get("auto_rollback"), "rollout.auto_rollback")
|
|
565
|
+
if raw.get("auto_rollback") is not None
|
|
566
|
+
else None,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def publish_draft(self, request: Request) -> Response:
|
|
570
|
+
principal = _principal(request)
|
|
571
|
+
body = request.json()
|
|
572
|
+
view = self.g.workflow.publish(
|
|
573
|
+
principal,
|
|
574
|
+
request.params["draft_id"],
|
|
575
|
+
confirm_weakening=_flag(body.get("confirm_weakening"), "confirm_weakening"),
|
|
576
|
+
reason=body.get("reason"),
|
|
577
|
+
rollout=self._rollout_hook(principal, body.get("rollout")),
|
|
578
|
+
)
|
|
579
|
+
return ok(view)
|
|
580
|
+
|
|
581
|
+
def emergency_publish_draft(self, request: Request) -> Response:
|
|
582
|
+
principal = _principal(request)
|
|
583
|
+
body = request.json()
|
|
584
|
+
view = self.g.workflow.publish(
|
|
585
|
+
principal,
|
|
586
|
+
request.params["draft_id"],
|
|
587
|
+
confirm_weakening=_flag(body.get("confirm_weakening"), "confirm_weakening"),
|
|
588
|
+
emergency=True,
|
|
589
|
+
reason=body.get("reason"),
|
|
590
|
+
)
|
|
591
|
+
return ok(view)
|
|
592
|
+
|
|
593
|
+
# -- simulations ------------------------------------------------------ #
|
|
594
|
+
def simulate_draft(self, request: Request) -> Response:
|
|
595
|
+
principal = _principal(request)
|
|
596
|
+
self.g.workflow.authorize(principal, request.params["draft_id"], Permission.POLICIES_WRITE)
|
|
597
|
+
draft = self.g.workflow.get(principal, request.params["draft_id"])
|
|
598
|
+
body = request.json()
|
|
599
|
+
target = (
|
|
600
|
+
ORGANIZATION_TARGET
|
|
601
|
+
if draft.target.type == "organization"
|
|
602
|
+
else PolicyTarget(PolicyTargetType(draft.target.type), draft.target.id)
|
|
603
|
+
)
|
|
604
|
+
view = self.g.simulations.create(
|
|
605
|
+
principal,
|
|
606
|
+
draft.organization_id,
|
|
607
|
+
target=target,
|
|
608
|
+
document=canonical_document(draft.floors, draft.defaults),
|
|
609
|
+
draft_id=draft.id,
|
|
610
|
+
period_days=body.get("period_days"),
|
|
611
|
+
repository_ids=_optional_ids(body.get("repository_ids"), "repository_ids"),
|
|
612
|
+
)
|
|
613
|
+
return ok(view, status=202)
|
|
614
|
+
|
|
615
|
+
def list_simulations(self, request: Request) -> Response:
|
|
616
|
+
return ok(
|
|
617
|
+
self.g.simulations.list_simulations(
|
|
618
|
+
_principal(request),
|
|
619
|
+
request.params["organization_id"],
|
|
620
|
+
draft_id=request.arg("draft"),
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def get_simulation(self, request: Request) -> Response:
|
|
625
|
+
return ok(self.g.simulations.get(_principal(request), request.params["simulation_id"]))
|
|
626
|
+
|
|
627
|
+
# -- rollouts --------------------------------------------------------- #
|
|
628
|
+
def list_rollouts(self, request: Request) -> Response:
|
|
629
|
+
return ok(
|
|
630
|
+
self.g.rollouts.list_rollouts(
|
|
631
|
+
_principal(request),
|
|
632
|
+
request.params["organization_id"],
|
|
633
|
+
active_only=request.arg("active") == "true",
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
def get_rollout(self, request: Request) -> Response:
|
|
638
|
+
return ok(self.g.rollouts.get(_principal(request), request.params["rollout_id"]))
|
|
639
|
+
|
|
640
|
+
def advance_rollout(self, request: Request) -> Response:
|
|
641
|
+
return ok(self.g.rollouts.advance(_principal(request), request.params["rollout_id"]))
|
|
642
|
+
|
|
643
|
+
def pause_rollout(self, request: Request) -> Response:
|
|
644
|
+
body = request.json()
|
|
645
|
+
return ok(
|
|
646
|
+
self.g.rollouts.pause(
|
|
647
|
+
_principal(request), request.params["rollout_id"], body.get("reason")
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def resume_rollout(self, request: Request) -> Response:
|
|
652
|
+
return ok(self.g.rollouts.resume(_principal(request), request.params["rollout_id"]))
|
|
653
|
+
|
|
654
|
+
def rollback_rollout(self, request: Request) -> Response:
|
|
655
|
+
body = request.json()
|
|
656
|
+
return ok(
|
|
657
|
+
self.g.rollouts.rollback(
|
|
658
|
+
_principal(request),
|
|
659
|
+
request.params["rollout_id"],
|
|
660
|
+
reason=body.get("reason"),
|
|
661
|
+
confirm=body.get("confirm"),
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def propagation(self, request: Request) -> Response:
|
|
666
|
+
return ok(
|
|
667
|
+
self.g.resolver.propagation_status(
|
|
668
|
+
_principal(request), request.params["organization_id"]
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# -- exceptions ------------------------------------------------------- #
|
|
673
|
+
def list_exceptions(self, request: Request) -> Response:
|
|
674
|
+
principal = _principal(request)
|
|
675
|
+
repository = request.arg("repository")
|
|
676
|
+
if repository is not None and (not repository.isdigit() or len(repository) > 16):
|
|
677
|
+
raise bad_request("repository must be a repository ID", "repository")
|
|
678
|
+
offset = offset_cursor(request.arg("cursor"))
|
|
679
|
+
limit = parse_limit(request.arg("limit"))
|
|
680
|
+
items, more = self.g.exceptions.list(
|
|
681
|
+
principal,
|
|
682
|
+
request.params["organization_id"],
|
|
683
|
+
status=request.arg("status"),
|
|
684
|
+
rule_id=request.arg("rule"),
|
|
685
|
+
repository_id=int(repository) if repository else None,
|
|
686
|
+
group_id=request.arg("group"),
|
|
687
|
+
limit=limit,
|
|
688
|
+
offset=offset,
|
|
689
|
+
)
|
|
690
|
+
return ok(
|
|
691
|
+
items,
|
|
692
|
+
{"next_cursor": encode_cursor([offset + limit]) if more else None, "limit": limit},
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
def create_exception(self, request: Request) -> Response:
|
|
696
|
+
body = request.json()
|
|
697
|
+
view = self.g.exceptions.request(
|
|
698
|
+
_principal(request),
|
|
699
|
+
request.params["organization_id"],
|
|
700
|
+
rule_id=body.get("rule_id"),
|
|
701
|
+
scope_type=body.get("scope_type"),
|
|
702
|
+
scope_id=body.get("scope_id"),
|
|
703
|
+
action=body.get("action"),
|
|
704
|
+
reason=body.get("reason"),
|
|
705
|
+
expires_at=body.get("expires_at"),
|
|
706
|
+
permanent=body.get("permanent", False),
|
|
707
|
+
)
|
|
708
|
+
return ok(view, status=201)
|
|
709
|
+
|
|
710
|
+
def get_exception(self, request: Request) -> Response:
|
|
711
|
+
return ok(self.g.exceptions.get(_principal(request), request.params["exception_id"]))
|
|
712
|
+
|
|
713
|
+
def approve_exception(self, request: Request) -> Response:
|
|
714
|
+
body = request.json()
|
|
715
|
+
return ok(
|
|
716
|
+
self.g.exceptions.approve(
|
|
717
|
+
_principal(request), request.params["exception_id"], body.get("note")
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def reject_exception(self, request: Request) -> Response:
|
|
722
|
+
body = request.json()
|
|
723
|
+
return ok(
|
|
724
|
+
self.g.exceptions.reject(
|
|
725
|
+
_principal(request), request.params["exception_id"], body.get("note")
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
def revoke_exception(self, request: Request) -> Response:
|
|
730
|
+
body = request.json()
|
|
731
|
+
return ok(
|
|
732
|
+
self.g.exceptions.revoke(
|
|
733
|
+
_principal(request), request.params["exception_id"], body.get("reason")
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def cancel_exception(self, request: Request) -> Response:
|
|
738
|
+
return ok(self.g.exceptions.cancel(_principal(request), request.params["exception_id"]))
|
|
739
|
+
|
|
740
|
+
# -- organization rules ----------------------------------------------- #
|
|
741
|
+
def get_rules(self, request: Request) -> Response:
|
|
742
|
+
return ok(self.g.rules.view(_principal(request), request.params["organization_id"]))
|
|
743
|
+
|
|
744
|
+
def put_rules(self, request: Request) -> Response:
|
|
745
|
+
body = request.json()
|
|
746
|
+
return ok(
|
|
747
|
+
self.g.rules.update(
|
|
748
|
+
_principal(request),
|
|
749
|
+
request.params["organization_id"],
|
|
750
|
+
expected_version=body.get("expected_version"),
|
|
751
|
+
rules=body.get("rules"),
|
|
752
|
+
reason=body.get("reason"),
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
def rules_history(self, request: Request) -> Response:
|
|
757
|
+
return ok(self.g.rules.history(_principal(request), request.params["organization_id"]))
|
|
758
|
+
|
|
759
|
+
# -- bulk operations -------------------------------------------------- #
|
|
760
|
+
def list_bulk(self, request: Request) -> Response:
|
|
761
|
+
return ok(
|
|
762
|
+
self.g.bulk.list_operations(_principal(request), request.params["organization_id"])
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
def create_bulk(self, request: Request) -> Response:
|
|
766
|
+
body = request.json()
|
|
767
|
+
require(
|
|
768
|
+
_principal(request),
|
|
769
|
+
Permission.SCANS_TRIGGER
|
|
770
|
+
if body.get("type") == "schedule_scan"
|
|
771
|
+
else Permission.REPOSITORIES_MANAGE,
|
|
772
|
+
request.params["organization_id"],
|
|
773
|
+
)
|
|
774
|
+
view = self.g.bulk.create(
|
|
775
|
+
_principal(request),
|
|
776
|
+
request.params["organization_id"],
|
|
777
|
+
operation_type=body.get("type"),
|
|
778
|
+
repository_ids=repository_ids(body.get("repository_ids")),
|
|
779
|
+
parameters=body.get("parameters"),
|
|
780
|
+
idempotency_key=body.get("idempotency_key"),
|
|
781
|
+
confirm=body.get("confirm", False),
|
|
782
|
+
)
|
|
783
|
+
return ok(view, status=202)
|
|
784
|
+
|
|
785
|
+
def get_bulk(self, request: Request) -> Response:
|
|
786
|
+
return ok(self.g.bulk.get(_principal(request), request.params["operation_id"]))
|
|
787
|
+
|
|
788
|
+
def cancel_bulk(self, request: Request) -> Response:
|
|
789
|
+
return ok(self.g.bulk.cancel(_principal(request), request.params["operation_id"]))
|
|
790
|
+
|
|
791
|
+
def retry_bulk(self, request: Request) -> Response:
|
|
792
|
+
return ok(self.g.bulk.retry_failed(_principal(request), request.params["operation_id"]))
|
|
793
|
+
|
|
794
|
+
# -- scan schedules --------------------------------------------------- #
|
|
795
|
+
def list_schedules(self, request: Request) -> Response:
|
|
796
|
+
return ok(
|
|
797
|
+
self.g.schedules.list_schedules(_principal(request), request.params["organization_id"])
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
def create_schedule(self, request: Request) -> Response:
|
|
801
|
+
view = self.g.schedules.create(
|
|
802
|
+
_principal(request), request.params["organization_id"], request.json()
|
|
803
|
+
)
|
|
804
|
+
return ok(view, status=201)
|
|
805
|
+
|
|
806
|
+
def get_schedule(self, request: Request) -> Response:
|
|
807
|
+
view, runs = self.g.schedules.get(_principal(request), request.params["schedule_id"])
|
|
808
|
+
return ok({"schedule": view, "runs": runs})
|
|
809
|
+
|
|
810
|
+
def patch_schedule(self, request: Request) -> Response:
|
|
811
|
+
return ok(
|
|
812
|
+
self.g.schedules.update(
|
|
813
|
+
_principal(request), request.params["schedule_id"], request.json()
|
|
814
|
+
)
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
def disable_schedule(self, request: Request) -> Response:
|
|
818
|
+
return ok(self.g.schedules.disable(_principal(request), request.params["schedule_id"]))
|
|
819
|
+
|
|
820
|
+
# -- table ------------------------------------------------------------ #
|
|
821
|
+
def routes(self) -> list[RouteRow]:
|
|
822
|
+
org = "/organizations/{organization_id:int}"
|
|
823
|
+
return [
|
|
824
|
+
("GET", org, self.organization, None, False, "read"),
|
|
825
|
+
("GET", f"{org}/settings", self.get_settings, None, False, "read"),
|
|
826
|
+
("PUT", f"{org}/settings", self.put_settings, None, False, "sensitive"),
|
|
827
|
+
("GET", f"{org}/security/overview", self.security_overview, None, False, "read"),
|
|
828
|
+
(
|
|
829
|
+
"GET",
|
|
830
|
+
f"{org}/security/repositories",
|
|
831
|
+
self.security_repositories,
|
|
832
|
+
None,
|
|
833
|
+
False,
|
|
834
|
+
"read",
|
|
835
|
+
),
|
|
836
|
+
("GET", f"{org}/security/policies", self.security_policies, None, False, "read"),
|
|
837
|
+
("GET", f"{org}/security/exceptions", self.security_exceptions, None, False, "read"),
|
|
838
|
+
("GET", f"{org}/security/trends", self.security_trends, None, False, "read"),
|
|
839
|
+
("GET", f"{org}/security/events", self.security_events, None, False, "read"),
|
|
840
|
+
(
|
|
841
|
+
"POST",
|
|
842
|
+
f"{org}/security/events/{{event_id:hex}}/acknowledge",
|
|
843
|
+
self.acknowledge_event,
|
|
844
|
+
None,
|
|
845
|
+
False,
|
|
846
|
+
"write",
|
|
847
|
+
),
|
|
848
|
+
("GET", f"{org}/reports/{{kind:ident}}", self.report, None, False, "search"),
|
|
849
|
+
("GET", f"{org}/search", self.search, None, False, "search"),
|
|
850
|
+
("POST", f"{org}/repositories/onboard", self.onboard, None, False, "write"),
|
|
851
|
+
("POST", f"{org}/repositories/mode", self.set_mode, None, False, "sensitive"),
|
|
852
|
+
(
|
|
853
|
+
"GET",
|
|
854
|
+
"/repositories/{repository_id:int}/effective-policy",
|
|
855
|
+
self.effective_policy,
|
|
856
|
+
None,
|
|
857
|
+
False,
|
|
858
|
+
"read",
|
|
859
|
+
),
|
|
860
|
+
("GET", f"{org}/repository-groups", self.list_groups, None, False, "read"),
|
|
861
|
+
("POST", f"{org}/repository-groups", self.create_group, None, False, "write"),
|
|
862
|
+
("GET", "/repository-groups/{group_id:hex}", self.get_group, None, False, "read"),
|
|
863
|
+
("PATCH", "/repository-groups/{group_id:hex}", self.patch_group, None, False, "write"),
|
|
864
|
+
(
|
|
865
|
+
"DELETE",
|
|
866
|
+
"/repository-groups/{group_id:hex}",
|
|
867
|
+
self.archive_group,
|
|
868
|
+
None,
|
|
869
|
+
False,
|
|
870
|
+
"sensitive",
|
|
871
|
+
),
|
|
872
|
+
(
|
|
873
|
+
"POST",
|
|
874
|
+
"/repository-groups/{group_id:hex}/repositories",
|
|
875
|
+
self.add_group_members,
|
|
876
|
+
None,
|
|
877
|
+
False,
|
|
878
|
+
"write",
|
|
879
|
+
),
|
|
880
|
+
(
|
|
881
|
+
"POST",
|
|
882
|
+
"/repository-groups/{group_id:hex}/repositories/remove",
|
|
883
|
+
self.remove_group_members,
|
|
884
|
+
None,
|
|
885
|
+
False,
|
|
886
|
+
"write",
|
|
887
|
+
),
|
|
888
|
+
("GET", f"{org}/policies", self.list_policy_targets, None, False, "read"),
|
|
889
|
+
(
|
|
890
|
+
"GET",
|
|
891
|
+
f"{org}/policy-targets/{{target_type:ident}}/versions",
|
|
892
|
+
self.policy_target_versions,
|
|
893
|
+
None,
|
|
894
|
+
False,
|
|
895
|
+
"read",
|
|
896
|
+
),
|
|
897
|
+
(
|
|
898
|
+
"POST",
|
|
899
|
+
f"{org}/policy-targets/{{target_type:ident}}/rollback",
|
|
900
|
+
self.policy_target_rollback,
|
|
901
|
+
None,
|
|
902
|
+
False,
|
|
903
|
+
"sensitive",
|
|
904
|
+
),
|
|
905
|
+
("GET", f"{org}/policy-drafts", self.list_drafts, None, False, "read"),
|
|
906
|
+
("POST", f"{org}/policy-drafts", self.create_draft, None, False, "write"),
|
|
907
|
+
("GET", "/policy-drafts/{draft_id:hex}", self.get_draft, None, False, "read"),
|
|
908
|
+
("PATCH", "/policy-drafts/{draft_id:hex}", self.patch_draft, None, False, "write"),
|
|
909
|
+
(
|
|
910
|
+
"POST",
|
|
911
|
+
"/policy-drafts/{draft_id:hex}/submit",
|
|
912
|
+
self.submit_draft,
|
|
913
|
+
None,
|
|
914
|
+
False,
|
|
915
|
+
"write",
|
|
916
|
+
),
|
|
917
|
+
(
|
|
918
|
+
"POST",
|
|
919
|
+
"/policy-drafts/{draft_id:hex}/approve",
|
|
920
|
+
self.approve_draft,
|
|
921
|
+
None,
|
|
922
|
+
False,
|
|
923
|
+
"sensitive",
|
|
924
|
+
),
|
|
925
|
+
(
|
|
926
|
+
"POST",
|
|
927
|
+
"/policy-drafts/{draft_id:hex}/reject",
|
|
928
|
+
self.reject_draft,
|
|
929
|
+
None,
|
|
930
|
+
False,
|
|
931
|
+
"write",
|
|
932
|
+
),
|
|
933
|
+
(
|
|
934
|
+
"POST",
|
|
935
|
+
"/policy-drafts/{draft_id:hex}/cancel",
|
|
936
|
+
self.cancel_draft,
|
|
937
|
+
None,
|
|
938
|
+
False,
|
|
939
|
+
"write",
|
|
940
|
+
),
|
|
941
|
+
(
|
|
942
|
+
"POST",
|
|
943
|
+
"/policy-drafts/{draft_id:hex}/publish",
|
|
944
|
+
self.publish_draft,
|
|
945
|
+
None,
|
|
946
|
+
False,
|
|
947
|
+
"sensitive",
|
|
948
|
+
),
|
|
949
|
+
(
|
|
950
|
+
"POST",
|
|
951
|
+
"/policy-drafts/{draft_id:hex}/emergency-publish",
|
|
952
|
+
self.emergency_publish_draft,
|
|
953
|
+
None,
|
|
954
|
+
False,
|
|
955
|
+
"sensitive",
|
|
956
|
+
),
|
|
957
|
+
(
|
|
958
|
+
"POST",
|
|
959
|
+
"/policy-drafts/{draft_id:hex}/simulations",
|
|
960
|
+
self.simulate_draft,
|
|
961
|
+
None,
|
|
962
|
+
False,
|
|
963
|
+
"write",
|
|
964
|
+
),
|
|
965
|
+
("GET", f"{org}/simulations", self.list_simulations, None, False, "read"),
|
|
966
|
+
(
|
|
967
|
+
"GET",
|
|
968
|
+
"/simulations/{simulation_id:hex}",
|
|
969
|
+
self.get_simulation,
|
|
970
|
+
None,
|
|
971
|
+
False,
|
|
972
|
+
"read",
|
|
973
|
+
),
|
|
974
|
+
("GET", f"{org}/rollouts", self.list_rollouts, None, False, "read"),
|
|
975
|
+
("GET", "/rollouts/{rollout_id:hex}", self.get_rollout, None, False, "read"),
|
|
976
|
+
(
|
|
977
|
+
"POST",
|
|
978
|
+
"/rollouts/{rollout_id:hex}/advance",
|
|
979
|
+
self.advance_rollout,
|
|
980
|
+
None,
|
|
981
|
+
False,
|
|
982
|
+
"sensitive",
|
|
983
|
+
),
|
|
984
|
+
(
|
|
985
|
+
"POST",
|
|
986
|
+
"/rollouts/{rollout_id:hex}/pause",
|
|
987
|
+
self.pause_rollout,
|
|
988
|
+
None,
|
|
989
|
+
False,
|
|
990
|
+
"write",
|
|
991
|
+
),
|
|
992
|
+
(
|
|
993
|
+
"POST",
|
|
994
|
+
"/rollouts/{rollout_id:hex}/resume",
|
|
995
|
+
self.resume_rollout,
|
|
996
|
+
None,
|
|
997
|
+
False,
|
|
998
|
+
"sensitive",
|
|
999
|
+
),
|
|
1000
|
+
(
|
|
1001
|
+
"POST",
|
|
1002
|
+
"/rollouts/{rollout_id:hex}/rollback",
|
|
1003
|
+
self.rollback_rollout,
|
|
1004
|
+
None,
|
|
1005
|
+
False,
|
|
1006
|
+
"sensitive",
|
|
1007
|
+
),
|
|
1008
|
+
("GET", f"{org}/policy-propagation", self.propagation, None, False, "read"),
|
|
1009
|
+
("GET", f"{org}/exceptions", self.list_exceptions, None, False, "read"),
|
|
1010
|
+
("POST", f"{org}/exceptions", self.create_exception, None, False, "sensitive"),
|
|
1011
|
+
("GET", "/exceptions/{exception_id:hex}", self.get_exception, None, False, "read"),
|
|
1012
|
+
(
|
|
1013
|
+
"POST",
|
|
1014
|
+
"/exceptions/{exception_id:hex}/approve",
|
|
1015
|
+
self.approve_exception,
|
|
1016
|
+
None,
|
|
1017
|
+
False,
|
|
1018
|
+
"sensitive",
|
|
1019
|
+
),
|
|
1020
|
+
(
|
|
1021
|
+
"POST",
|
|
1022
|
+
"/exceptions/{exception_id:hex}/reject",
|
|
1023
|
+
self.reject_exception,
|
|
1024
|
+
None,
|
|
1025
|
+
False,
|
|
1026
|
+
"write",
|
|
1027
|
+
),
|
|
1028
|
+
(
|
|
1029
|
+
"POST",
|
|
1030
|
+
"/exceptions/{exception_id:hex}/revoke",
|
|
1031
|
+
self.revoke_exception,
|
|
1032
|
+
None,
|
|
1033
|
+
False,
|
|
1034
|
+
"sensitive",
|
|
1035
|
+
),
|
|
1036
|
+
(
|
|
1037
|
+
"POST",
|
|
1038
|
+
"/exceptions/{exception_id:hex}/cancel",
|
|
1039
|
+
self.cancel_exception,
|
|
1040
|
+
None,
|
|
1041
|
+
False,
|
|
1042
|
+
"write",
|
|
1043
|
+
),
|
|
1044
|
+
("GET", f"{org}/rules", self.get_rules, None, False, "read"),
|
|
1045
|
+
("PUT", f"{org}/rules", self.put_rules, None, False, "sensitive"),
|
|
1046
|
+
("GET", f"{org}/rules/history", self.rules_history, None, False, "read"),
|
|
1047
|
+
("GET", f"{org}/bulk-operations", self.list_bulk, None, False, "read"),
|
|
1048
|
+
("POST", f"{org}/bulk-operations", self.create_bulk, None, False, "sensitive"),
|
|
1049
|
+
("GET", "/bulk-operations/{operation_id:hex}", self.get_bulk, None, False, "read"),
|
|
1050
|
+
(
|
|
1051
|
+
"POST",
|
|
1052
|
+
"/bulk-operations/{operation_id:hex}/cancel",
|
|
1053
|
+
self.cancel_bulk,
|
|
1054
|
+
None,
|
|
1055
|
+
False,
|
|
1056
|
+
"write",
|
|
1057
|
+
),
|
|
1058
|
+
(
|
|
1059
|
+
"POST",
|
|
1060
|
+
"/bulk-operations/{operation_id:hex}/retry",
|
|
1061
|
+
self.retry_bulk,
|
|
1062
|
+
None,
|
|
1063
|
+
False,
|
|
1064
|
+
"write",
|
|
1065
|
+
),
|
|
1066
|
+
("GET", f"{org}/scan-schedules", self.list_schedules, None, False, "read"),
|
|
1067
|
+
("POST", f"{org}/scan-schedules", self.create_schedule, None, False, "write"),
|
|
1068
|
+
("GET", "/scan-schedules/{schedule_id:hex}", self.get_schedule, None, False, "read"),
|
|
1069
|
+
(
|
|
1070
|
+
"PATCH",
|
|
1071
|
+
"/scan-schedules/{schedule_id:hex}",
|
|
1072
|
+
self.patch_schedule,
|
|
1073
|
+
None,
|
|
1074
|
+
False,
|
|
1075
|
+
"write",
|
|
1076
|
+
),
|
|
1077
|
+
(
|
|
1078
|
+
"POST",
|
|
1079
|
+
"/scan-schedules/{schedule_id:hex}/disable",
|
|
1080
|
+
self.disable_schedule,
|
|
1081
|
+
None,
|
|
1082
|
+
False,
|
|
1083
|
+
"write",
|
|
1084
|
+
),
|
|
1085
|
+
]
|