ocmo-api 0.8.19__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.
- LICENSE +201 -0
- NOTICE +16 -0
- core/__init__.py +0 -0
- core/api/__init__.py +201 -0
- core/api/_common.py +37 -0
- core/api/audit.py +244 -0
- core/api/auth.py +50 -0
- core/api/config.py +96 -0
- core/api/errors.py +65 -0
- core/api/global_permissions.py +107 -0
- core/api/lock.py +99 -0
- core/api/namespace.py +86 -0
- core/api/propagation.py +29 -0
- core/api/resolve.py +229 -0
- core/api/resolver.py +96 -0
- core/api/secret.py +53 -0
- core/api/system.py +42 -0
- core/api/template.py +53 -0
- core/api/tree.py +274 -0
- core/apps.py +18 -0
- core/audit_operations.py +0 -0
- core/constants/__init__.py +5 -0
- core/constants/audit_operations.py +35 -0
- core/constants/permission_actions.py +59 -0
- core/constants/permissions.py +41 -0
- core/constants/resolve.py +3 -0
- core/constants/transformers.py +22 -0
- core/constants/webhook_events.py +31 -0
- core/data/builtin_schemas/_git_sync.schema.yaml +24 -0
- core/data/builtin_schemas/_permissions.schema.yaml +211 -0
- core/data/builtin_schemas/_webhooks.schema.yaml +145 -0
- core/decorators/__init__.py +14 -0
- core/decorators/audit.py +105 -0
- core/decorators/binding.py +49 -0
- core/decorators/permissions.py +129 -0
- core/decorators/webhook.py +87 -0
- core/exceptions.py +165 -0
- core/management/__init__.py +0 -0
- core/management/commands/__init__.py +0 -0
- core/management/commands/dump_openapi.py +40 -0
- core/managers/artifacts.py +346 -0
- core/managers/audit/__init__.py +1172 -0
- core/managers/audit/timeline.py +365 -0
- core/managers/auth.py +276 -0
- core/managers/cast.py +506 -0
- core/managers/config_validation.py +216 -0
- core/managers/crypto.py +78 -0
- core/managers/global_permissions.py +183 -0
- core/managers/health.py +56 -0
- core/managers/lock.py +229 -0
- core/managers/namespace.py +132 -0
- core/managers/permissions.py +168 -0
- core/managers/propagation.py +244 -0
- core/managers/resolution.py +938 -0
- core/managers/resolve_cache.py +266 -0
- core/managers/resolve_parameters.py +503 -0
- core/managers/resolver.py +56 -0
- core/managers/resolver_tokens.py +114 -0
- core/managers/resolving.py +673 -0
- core/managers/secret.py +147 -0
- core/managers/system.py +44 -0
- core/managers/tree/__init__.py +85 -0
- core/managers/tree/_common.py +85 -0
- core/managers/tree/base.py +203 -0
- core/managers/tree/config_ops.py +167 -0
- core/managers/tree/constants.py +11 -0
- core/managers/tree/create.py +75 -0
- core/managers/tree/diff.py +270 -0
- core/managers/tree/mutate.py +433 -0
- core/managers/tree/paths.py +49 -0
- core/managers/tree/propagation.py +200 -0
- core/managers/tree/read.py +263 -0
- core/managers/tree/resolver_ops.py +21 -0
- core/managers/tree/tags.py +167 -0
- core/managers/tree/update.py +137 -0
- core/managers/tree/versions.py +87 -0
- core/managers/tree_capabilities.py +189 -0
- core/managers/webhook/__init__.py +3 -0
- core/managers/webhook/cache.py +68 -0
- core/managers/webhook/main.py +357 -0
- core/middleware.py +32 -0
- core/migrations/0001_initial.py +157 -0
- core/migrations/0002_folder.py +33 -0
- core/migrations/0003_alter_namespace_name.py +17 -0
- core/migrations/0004_alter_namespace_name_and_more.py +25 -0
- core/migrations/0005_configversion_templateversion.py +85 -0
- core/migrations/0006_alter_templateversion_options_and_more.py +47 -0
- core/migrations/0007_configversion_deleted_at_templateversion_deleted_at.py +22 -0
- core/migrations/0008_namespace_tags_dek_secret_global_permissions.py +139 -0
- core/migrations/0009_tree_lock.py +58 -0
- core/migrations/0010_resolver_token_encryption.py +75 -0
- core/migrations/0011_audit_event.py +134 -0
- core/migrations/0012_audit_event_namespace_name.py +24 -0
- core/migrations/0013_audit_event_object_id.py +28 -0
- core/migrations/0014_sync_permissions_webhooks_builtin_schemas.py +26 -0
- core/migrations/0015_audit_event_operation_subresource.py +27 -0
- core/migrations/0016_sync_permissions_schema_audit_actions.py +26 -0
- core/migrations/0017_sync_permissions_schema_valid_actions.py +26 -0
- core/migrations/__init__.py +0 -0
- core/models/__init__.py +9 -0
- core/models/audit.py +97 -0
- core/models/config.py +132 -0
- core/models/folder.py +12 -0
- core/models/global_permissions.py +27 -0
- core/models/lock.py +35 -0
- core/models/namespace.py +45 -0
- core/models/resolver.py +25 -0
- core/models/secret.py +58 -0
- core/models/tree.py +29 -0
- core/notice.py +18 -0
- core/operation_ids.py +153 -0
- core/schemas/__init__.py +2 -0
- core/schemas/audit.py +75 -0
- core/schemas/cast_json_schema.py +57 -0
- core/schemas/cast_options.py +508 -0
- core/schemas/generic.py +233 -0
- core/schemas/metadata_json_schema.py +21 -0
- core/schemas/propagation.py +88 -0
- core/schemas/requests.py +773 -0
- core/schemas/resolver_json_schema.py +65 -0
- core/schemas/responses.py +690 -0
- core/schemas/webhooks.py +40 -0
- core/shortcuts.py +415 -0
- core/tests/__init__.py +0 -0
- core/tests/auth_helpers.py +23 -0
- core/tests/namespace_helpers.py +29 -0
- core/tests/test_api_cli.py +43 -0
- core/tests/test_api_errors.py +151 -0
- core/tests/test_artifacts.py +123 -0
- core/tests/test_audit.py +973 -0
- core/tests/test_audit_event_id.py +90 -0
- core/tests/test_audit_operations.py +222 -0
- core/tests/test_builtin_config_schemas.py +174 -0
- core/tests/test_can_i.py +510 -0
- core/tests/test_cast_format_options.py +57 -0
- core/tests/test_cast_options_schema.py +54 -0
- core/tests/test_cast_yaml.py +26 -0
- core/tests/test_config_schema_api.py +173 -0
- core/tests/test_config_validation.py +378 -0
- core/tests/test_document_parser.py +122 -0
- core/tests/test_dump_openapi.py +46 -0
- core/tests/test_extend_key.py +267 -0
- core/tests/test_folder_get_item.py +23 -0
- core/tests/test_folder_resolve_missing_tags.py +73 -0
- core/tests/test_get_item_version.py +31 -0
- core/tests/test_global_permission_create.py +171 -0
- core/tests/test_list_pagination.py +262 -0
- core/tests/test_locks.py +209 -0
- core/tests/test_multiline_transformer.py +132 -0
- core/tests/test_namespace_create.py +11 -0
- core/tests/test_ocmo_metadata.py +127 -0
- core/tests/test_oidc_jwt_audiences.py +64 -0
- core/tests/test_omit_transformer.py +78 -0
- core/tests/test_operation_ids.py +47 -0
- core/tests/test_permissions.py +1148 -0
- core/tests/test_propagation.py +441 -0
- core/tests/test_public_base_url.py +20 -0
- core/tests/test_resolve_draft.py +176 -0
- core/tests/test_resolve_parameters_version.py +181 -0
- core/tests/test_resolve_permissions.py +492 -0
- core/tests/test_resolve_resolver_response.py +107 -0
- core/tests/test_resolver_auth.py +33 -0
- core/tests/test_resolver_folder_filters.py +95 -0
- core/tests/test_resolver_get_item.py +23 -0
- core/tests/test_resolver_navigate_search_scope.py +98 -0
- core/tests/test_resolver_schema_api.py +91 -0
- core/tests/test_security_hardening.py +324 -0
- core/tests/test_selectors.py +65 -0
- core/tests/test_special_namespace_paths.py +409 -0
- core/tests/test_stable_promotion.py +124 -0
- core/tests/test_system.py +140 -0
- core/tests/test_tree_manager_for_item.py +192 -0
- core/tests/test_two_layer_cache.py +334 -0
- core/tests/test_url_trailing_slashes.py +53 -0
- core/tests/test_validation_error_responses.py +43 -0
- core/tests/test_webhooks.py +985 -0
- core/utils/__init__.py +23 -0
- core/utils/deep_merge.py +91 -0
- core/utils/namespace_special_configs.py +105 -0
- core/utils/permissions_compiler.py +627 -0
- core/utils/permissions_schema_document.py +52 -0
- core/validation_errors.py +105 -0
- core/version.py +27 -0
- core/views.py +1 -0
- ocmo_api-0.8.19.dist-info/METADATA +252 -0
- ocmo_api-0.8.19.dist-info/RECORD +203 -0
- ocmo_api-0.8.19.dist-info/WHEEL +4 -0
- ocmo_api-0.8.19.dist-info/entry_points.txt +2 -0
- ocmo_api-0.8.19.dist-info/licenses/LICENSE +201 -0
- ocmo_api-0.8.19.dist-info/licenses/NOTICE +16 -0
- ocmoapi/__init__.py +1 -0
- ocmoapi/__main__.py +6 -0
- ocmoapi/asgi.py +16 -0
- ocmoapi/auth.py +163 -0
- ocmoapi/cli.py +85 -0
- ocmoapi/oauth2_provider.py +23 -0
- ocmoapi/parser.py +120 -0
- ocmoapi/settings.py +275 -0
- ocmoapi/templates/ninja/oauth2-redirect.html +81 -0
- ocmoapi/templates/ninja/swagger.html +55 -0
- ocmoapi/testing_auth.py +64 -0
- ocmoapi/urls.py +40 -0
- ocmoapi/wsgi.py +16 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
_ocmo:
|
|
2
|
+
is_json_schema: true
|
|
3
|
+
$schema: https://json-schema.org/draft/2020-12/schema
|
|
4
|
+
title: Namespace permissions policy document
|
|
5
|
+
description: ABAC policies evaluated on every namespace operation. Policies are matched top-to-bottom;
|
|
6
|
+
explicit Deny wins over Allow.
|
|
7
|
+
type: object
|
|
8
|
+
additionalProperties: false
|
|
9
|
+
required:
|
|
10
|
+
- policies
|
|
11
|
+
properties:
|
|
12
|
+
policies:
|
|
13
|
+
type: array
|
|
14
|
+
description: Ordered list of allow/deny policies.
|
|
15
|
+
items:
|
|
16
|
+
type: object
|
|
17
|
+
additionalProperties: false
|
|
18
|
+
required:
|
|
19
|
+
- effect
|
|
20
|
+
- actors
|
|
21
|
+
- actions
|
|
22
|
+
- resources
|
|
23
|
+
properties:
|
|
24
|
+
id:
|
|
25
|
+
type: string
|
|
26
|
+
minLength: 1
|
|
27
|
+
maxLength: 200
|
|
28
|
+
description: Optional stable identifier for audits and documentation.
|
|
29
|
+
examples:
|
|
30
|
+
- allow-dev-readers
|
|
31
|
+
description:
|
|
32
|
+
type: string
|
|
33
|
+
maxLength: 500
|
|
34
|
+
description: Human-readable summary of what this policy grants or denies.
|
|
35
|
+
examples:
|
|
36
|
+
- Allow developers read access to project/dev/**
|
|
37
|
+
effect:
|
|
38
|
+
type: string
|
|
39
|
+
enum:
|
|
40
|
+
- Allow
|
|
41
|
+
- Deny
|
|
42
|
+
description: Whether matching requests are allowed or denied.
|
|
43
|
+
examples:
|
|
44
|
+
- Allow
|
|
45
|
+
actors:
|
|
46
|
+
type: array
|
|
47
|
+
minItems: 1
|
|
48
|
+
description: Who the policy applies to. User actors match OIDC claims; Resolver actors match
|
|
49
|
+
resolver identities by tree path.
|
|
50
|
+
items:
|
|
51
|
+
oneOf:
|
|
52
|
+
- type: object
|
|
53
|
+
additionalProperties: false
|
|
54
|
+
required:
|
|
55
|
+
- kind
|
|
56
|
+
- claims
|
|
57
|
+
properties:
|
|
58
|
+
kind:
|
|
59
|
+
const: User
|
|
60
|
+
description: Match authenticated human users.
|
|
61
|
+
claims:
|
|
62
|
+
type: object
|
|
63
|
+
minProperties: 1
|
|
64
|
+
description: OIDC claim names mapped to literal values or ``*``. Use ``{!user.<claim>}``
|
|
65
|
+
only in resource globs, not here.
|
|
66
|
+
additionalProperties:
|
|
67
|
+
type: string
|
|
68
|
+
minLength: 1
|
|
69
|
+
pattern: ^(\*|\{!user\.[a-zA-Z0-9_]+\}|(?!.*\{!).+)$
|
|
70
|
+
description: Claim value, wildcard ``*``, or interpolation placeholder.
|
|
71
|
+
examples:
|
|
72
|
+
- alice@example.com
|
|
73
|
+
- '*'
|
|
74
|
+
examples:
|
|
75
|
+
- email: alice@example.com
|
|
76
|
+
- type: object
|
|
77
|
+
additionalProperties: false
|
|
78
|
+
required:
|
|
79
|
+
- kind
|
|
80
|
+
- path
|
|
81
|
+
properties:
|
|
82
|
+
kind:
|
|
83
|
+
const: Resolver
|
|
84
|
+
description: Match a resolver identity by its tree path.
|
|
85
|
+
path:
|
|
86
|
+
type: string
|
|
87
|
+
minLength: 1
|
|
88
|
+
maxLength: 512
|
|
89
|
+
format: uri-reference
|
|
90
|
+
x-ocmo-uri-reference: resolver
|
|
91
|
+
description: Resolver tree path. Use ``*`` to match any resolver. Relative paths resolve
|
|
92
|
+
from the ``_permissions`` folder.
|
|
93
|
+
examples:
|
|
94
|
+
- deploy/prod
|
|
95
|
+
- '*'
|
|
96
|
+
actions:
|
|
97
|
+
type: array
|
|
98
|
+
minItems: 1
|
|
99
|
+
description: Permission operations this policy governs. Use ``resource:verb`` pairs; ``*`` wildcards
|
|
100
|
+
match any resource or verb.
|
|
101
|
+
items:
|
|
102
|
+
type: string
|
|
103
|
+
pattern: ^(config|template|resolver|secret|lock|folder|\*):(read|write|delete|resolve|tag|describe|audit|\*)$
|
|
104
|
+
enum:
|
|
105
|
+
- config:read
|
|
106
|
+
- config:write
|
|
107
|
+
- config:delete
|
|
108
|
+
- config:resolve
|
|
109
|
+
- config:tag
|
|
110
|
+
- config:describe
|
|
111
|
+
- config:audit
|
|
112
|
+
- template:read
|
|
113
|
+
- template:write
|
|
114
|
+
- template:delete
|
|
115
|
+
- template:tag
|
|
116
|
+
- template:describe
|
|
117
|
+
- template:audit
|
|
118
|
+
- resolver:read
|
|
119
|
+
- resolver:write
|
|
120
|
+
- resolver:delete
|
|
121
|
+
- resolver:audit
|
|
122
|
+
- secret:read
|
|
123
|
+
- secret:write
|
|
124
|
+
- secret:delete
|
|
125
|
+
- secret:resolve
|
|
126
|
+
- secret:tag
|
|
127
|
+
- secret:describe
|
|
128
|
+
- secret:audit
|
|
129
|
+
- lock:read
|
|
130
|
+
- lock:write
|
|
131
|
+
- lock:delete
|
|
132
|
+
- folder:describe
|
|
133
|
+
- folder:audit
|
|
134
|
+
- config:*
|
|
135
|
+
- template:*
|
|
136
|
+
- resolver:*
|
|
137
|
+
- secret:*
|
|
138
|
+
- lock:*
|
|
139
|
+
- '*:read'
|
|
140
|
+
- '*:write'
|
|
141
|
+
- '*:delete'
|
|
142
|
+
- '*:resolve'
|
|
143
|
+
- '*:tag'
|
|
144
|
+
- '*:describe'
|
|
145
|
+
- '*:audit'
|
|
146
|
+
- '*:*'
|
|
147
|
+
description: Concrete ``<resource>:<verb>`` pair or glob such as ``config:*``, ``*:read``,
|
|
148
|
+
or ``*:*``.
|
|
149
|
+
examples:
|
|
150
|
+
- config:read
|
|
151
|
+
- config:*
|
|
152
|
+
- '*:read'
|
|
153
|
+
- '*:*'
|
|
154
|
+
examples:
|
|
155
|
+
- - config:read
|
|
156
|
+
- config:describe
|
|
157
|
+
resources:
|
|
158
|
+
type: array
|
|
159
|
+
minItems: 1
|
|
160
|
+
description: Resource path globs the policy applies to. Supports ``*`` and ``**`` fnmatch patterns
|
|
161
|
+
and ``{!user.claim}`` interpolation.
|
|
162
|
+
items:
|
|
163
|
+
type: string
|
|
164
|
+
minLength: 1
|
|
165
|
+
maxLength: 512
|
|
166
|
+
format: uri-reference
|
|
167
|
+
x-ocmo-uri-reference: resource
|
|
168
|
+
description: Tree path or glob (e.g. ``project/dev/**``, ``secrets/**``). Folder paths may
|
|
169
|
+
end with ``/**`` to match all descendants under the folder.
|
|
170
|
+
examples:
|
|
171
|
+
- project/dev/**
|
|
172
|
+
- app/prod/**
|
|
173
|
+
- '**'
|
|
174
|
+
examples:
|
|
175
|
+
- - project/dev/**
|
|
176
|
+
conditions:
|
|
177
|
+
type: object
|
|
178
|
+
additionalProperties: false
|
|
179
|
+
description: Optional constraints that must all match for the policy to apply.
|
|
180
|
+
properties:
|
|
181
|
+
ip_range:
|
|
182
|
+
type: array
|
|
183
|
+
minItems: 1
|
|
184
|
+
description: Client IP ranges (IPv4/IPv6 CIDR). Denied when IP is unknown.
|
|
185
|
+
items:
|
|
186
|
+
type: string
|
|
187
|
+
pattern: ^(?:(?:\d{1,3}\.){3}\d{1,3}(?:\/(?:\d|[1-2]\d|3[0-2]))?|[0-9a-fA-F:]+(?:\/\d{1,3})?)$
|
|
188
|
+
description: IPv4 or IPv6 network in CIDR notation.
|
|
189
|
+
examples:
|
|
190
|
+
- 10.0.0.0/8
|
|
191
|
+
- 2001:db8::/32
|
|
192
|
+
time_of_day:
|
|
193
|
+
type: array
|
|
194
|
+
minItems: 1
|
|
195
|
+
description: UTC time windows when the policy is active (half-open intervals).
|
|
196
|
+
items:
|
|
197
|
+
type: string
|
|
198
|
+
pattern: ^(?:[01]\d|2[0-3]):[0-5]\d-(?:[01]\d|2[0-3]):[0-5]\d$
|
|
199
|
+
description: UTC time range ``HH:MM-HH:MM``.
|
|
200
|
+
examples:
|
|
201
|
+
- 09:00-17:30
|
|
202
|
+
examples:
|
|
203
|
+
- - effect: Allow
|
|
204
|
+
actors:
|
|
205
|
+
- kind: User
|
|
206
|
+
claims:
|
|
207
|
+
email: '*'
|
|
208
|
+
actions:
|
|
209
|
+
- config:read
|
|
210
|
+
resources:
|
|
211
|
+
- project/**
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
_ocmo:
|
|
2
|
+
is_json_schema: true
|
|
3
|
+
$schema: https://json-schema.org/draft/2020-12/schema
|
|
4
|
+
title: Namespace webhook delivery configuration
|
|
5
|
+
description: Outbound HTTP notifications for namespace events. Each entry is evaluated independently;
|
|
6
|
+
payloads are HMAC-signed.
|
|
7
|
+
type: object
|
|
8
|
+
additionalProperties: false
|
|
9
|
+
required:
|
|
10
|
+
- webhooks
|
|
11
|
+
properties:
|
|
12
|
+
webhooks:
|
|
13
|
+
type: array
|
|
14
|
+
description: Webhook delivery entries.
|
|
15
|
+
items:
|
|
16
|
+
type: object
|
|
17
|
+
additionalProperties: false
|
|
18
|
+
required:
|
|
19
|
+
- id
|
|
20
|
+
- enabled
|
|
21
|
+
- url
|
|
22
|
+
- events
|
|
23
|
+
- signature_key
|
|
24
|
+
properties:
|
|
25
|
+
id:
|
|
26
|
+
type: string
|
|
27
|
+
minLength: 1
|
|
28
|
+
maxLength: 100
|
|
29
|
+
pattern: ^[a-zA-Z][a-zA-Z0-9_-]*$
|
|
30
|
+
description: Stable identifier unique within this config.
|
|
31
|
+
examples:
|
|
32
|
+
- prod-notify
|
|
33
|
+
enabled:
|
|
34
|
+
type: boolean
|
|
35
|
+
description: When false, the entry is skipped without removing it.
|
|
36
|
+
examples:
|
|
37
|
+
- true
|
|
38
|
+
url:
|
|
39
|
+
type: string
|
|
40
|
+
minLength: 1
|
|
41
|
+
maxLength: 2048
|
|
42
|
+
format: uri
|
|
43
|
+
description: HTTPS endpoint that receives POST requests.
|
|
44
|
+
examples:
|
|
45
|
+
- https://hooks.example.com/ocmo
|
|
46
|
+
events:
|
|
47
|
+
type: array
|
|
48
|
+
minItems: 1
|
|
49
|
+
description: Event names that trigger delivery for this entry.
|
|
50
|
+
items:
|
|
51
|
+
type: string
|
|
52
|
+
enum:
|
|
53
|
+
- config.created
|
|
54
|
+
- config.updated
|
|
55
|
+
- config.deleted
|
|
56
|
+
- config.tagged
|
|
57
|
+
- config.resolved
|
|
58
|
+
- resolver.created
|
|
59
|
+
- resolver.updated
|
|
60
|
+
- secret.created
|
|
61
|
+
- secret.updated
|
|
62
|
+
- secret.deleted
|
|
63
|
+
- secret.tagged
|
|
64
|
+
- secret.resolved
|
|
65
|
+
- namespace.updated
|
|
66
|
+
- lock.created
|
|
67
|
+
- lock.updated
|
|
68
|
+
- lock.deleted
|
|
69
|
+
- propagation.triggered
|
|
70
|
+
description: Namespace event identifier.
|
|
71
|
+
examples:
|
|
72
|
+
- config.updated
|
|
73
|
+
examples:
|
|
74
|
+
- - config.updated
|
|
75
|
+
- config.tagged
|
|
76
|
+
filter:
|
|
77
|
+
type: object
|
|
78
|
+
additionalProperties: false
|
|
79
|
+
description: Optional narrowing of which item paths fire this webhook.
|
|
80
|
+
properties:
|
|
81
|
+
paths:
|
|
82
|
+
type: array
|
|
83
|
+
minItems: 1
|
|
84
|
+
description: Fnmatch globs matched against the event item path. Omit the filter block to
|
|
85
|
+
receive all subscribed events.
|
|
86
|
+
items:
|
|
87
|
+
type: string
|
|
88
|
+
minLength: 1
|
|
89
|
+
maxLength: 512
|
|
90
|
+
format: uri-reference
|
|
91
|
+
x-ocmo-uri-reference: resource
|
|
92
|
+
description: Tree path glob (e.g. ``project/**``).
|
|
93
|
+
examples:
|
|
94
|
+
- project/**
|
|
95
|
+
- app/prod/**
|
|
96
|
+
signature_header:
|
|
97
|
+
type: string
|
|
98
|
+
minLength: 1
|
|
99
|
+
maxLength: 100
|
|
100
|
+
description: HTTP header carrying the HMAC-SHA256 hex digest.
|
|
101
|
+
default: X-OCMO-Signature
|
|
102
|
+
examples:
|
|
103
|
+
- X-OCMO-Signature
|
|
104
|
+
signature_key:
|
|
105
|
+
type: string
|
|
106
|
+
minLength: 1
|
|
107
|
+
maxLength: 512
|
|
108
|
+
description: HMAC signing key literal or ``{!param}`` reference resolved from ``_ocmo.parameters``.
|
|
109
|
+
examples:
|
|
110
|
+
- '{!hmac_signing_key}'
|
|
111
|
+
payload:
|
|
112
|
+
type: object
|
|
113
|
+
additionalProperties: true
|
|
114
|
+
description: Payload shape controls. Set ``preset`` or ``template`` (mutually exclusive).
|
|
115
|
+
properties:
|
|
116
|
+
preset:
|
|
117
|
+
type: string
|
|
118
|
+
enum:
|
|
119
|
+
- ocmo
|
|
120
|
+
- generic_json
|
|
121
|
+
- slack
|
|
122
|
+
- teams
|
|
123
|
+
- discord
|
|
124
|
+
description: Built-in payload formatter.
|
|
125
|
+
examples:
|
|
126
|
+
- ocmo
|
|
127
|
+
template:
|
|
128
|
+
type: string
|
|
129
|
+
minLength: 1
|
|
130
|
+
description: Jinja2 template rendered to the request body.
|
|
131
|
+
headers:
|
|
132
|
+
type: object
|
|
133
|
+
description: Extra HTTP headers appended to each delivery.
|
|
134
|
+
additionalProperties:
|
|
135
|
+
type: string
|
|
136
|
+
minLength: 1
|
|
137
|
+
examples:
|
|
138
|
+
- - id: example
|
|
139
|
+
enabled: false
|
|
140
|
+
url: https://example.com/ocmo/webhook
|
|
141
|
+
events:
|
|
142
|
+
- config.updated
|
|
143
|
+
signature_key: '{!hmac_signing_key}'
|
|
144
|
+
payload:
|
|
145
|
+
preset: ocmo
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Cross-cutting manager method decorators."""
|
|
2
|
+
|
|
3
|
+
from .audit import audit, enrich_audit
|
|
4
|
+
from .permissions import PermCheck, arg, require_permissions
|
|
5
|
+
from .webhook import webhook
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"audit",
|
|
9
|
+
"enrich_audit",
|
|
10
|
+
"arg",
|
|
11
|
+
"PermCheck",
|
|
12
|
+
"require_permissions",
|
|
13
|
+
"webhook",
|
|
14
|
+
]
|
core/decorators/audit.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""@audit decorator for append-only operation logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class AuditEventContext:
|
|
14
|
+
"""Public post-call fields a method may supply to the active audit event."""
|
|
15
|
+
|
|
16
|
+
operation: str | None = None
|
|
17
|
+
object_version: int | None = None
|
|
18
|
+
subresource_type: str | None = None
|
|
19
|
+
subresource: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_audit_operation_context: ContextVar[AuditEventContext | None] = ContextVar(
|
|
23
|
+
"audit_operation_context",
|
|
24
|
+
default=None,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def enrich_audit(**kwargs) -> None:
|
|
29
|
+
"""Set post-call audit fields from inside a decorated method body.
|
|
30
|
+
|
|
31
|
+
Safe to call when no audit is active (no-op).
|
|
32
|
+
Body values take precedence over decorator defaults.
|
|
33
|
+
"""
|
|
34
|
+
ctx = _audit_operation_context.get()
|
|
35
|
+
if ctx is None:
|
|
36
|
+
return
|
|
37
|
+
for key, value in kwargs.items():
|
|
38
|
+
if value is not None and hasattr(ctx, key):
|
|
39
|
+
setattr(ctx, key, value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _take_audit_context() -> AuditEventContext | None:
|
|
43
|
+
"""Consume and clear the ContextVar; called by run_decorated after func()."""
|
|
44
|
+
ctx = _audit_operation_context.get()
|
|
45
|
+
_audit_operation_context.set(None)
|
|
46
|
+
return ctx
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def audit(
|
|
50
|
+
object_type: str | Callable,
|
|
51
|
+
*,
|
|
52
|
+
object_id_attr: str | Callable | None = None,
|
|
53
|
+
namespace_attribute: str = "namespace",
|
|
54
|
+
resolve_type: str | None = None,
|
|
55
|
+
operation: str | Callable | None = None,
|
|
56
|
+
subresource_type: str | Callable | None = None,
|
|
57
|
+
subresource: str | Callable | None = None,
|
|
58
|
+
object_version: int | str | Callable | None = None,
|
|
59
|
+
skip_when: Callable | None = None,
|
|
60
|
+
skip_when_no_auth: bool = True,
|
|
61
|
+
auth_manager_attribute: str = "auth",
|
|
62
|
+
) -> Callable:
|
|
63
|
+
"""Decorator for manager public methods — logs outcome after execution."""
|
|
64
|
+
|
|
65
|
+
def decorator(func: Callable) -> Callable:
|
|
66
|
+
try:
|
|
67
|
+
sig = inspect.signature(func)
|
|
68
|
+
func_params = [
|
|
69
|
+
name
|
|
70
|
+
for name, p in sig.parameters.items()
|
|
71
|
+
if name != "self" and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
|
|
72
|
+
]
|
|
73
|
+
except (ValueError, TypeError):
|
|
74
|
+
func_params = []
|
|
75
|
+
|
|
76
|
+
@functools.wraps(func)
|
|
77
|
+
def wrapper(self, *args, **kwargs):
|
|
78
|
+
from ..managers.audit import AuditManager
|
|
79
|
+
|
|
80
|
+
audit_mgr = AuditManager.for_decorated_call(
|
|
81
|
+
self,
|
|
82
|
+
auth_manager_attribute=auth_manager_attribute,
|
|
83
|
+
skip_when_no_auth=skip_when_no_auth,
|
|
84
|
+
)
|
|
85
|
+
if audit_mgr is None:
|
|
86
|
+
return func(self, *args, **kwargs)
|
|
87
|
+
return audit_mgr.run_decorated(
|
|
88
|
+
func,
|
|
89
|
+
self,
|
|
90
|
+
args,
|
|
91
|
+
kwargs,
|
|
92
|
+
object_type=object_type,
|
|
93
|
+
object_id_attr=object_id_attr,
|
|
94
|
+
skip_when=skip_when,
|
|
95
|
+
func_params=func_params,
|
|
96
|
+
resolve_type=resolve_type,
|
|
97
|
+
operation=operation,
|
|
98
|
+
subresource_type=subresource_type,
|
|
99
|
+
subresource=subresource,
|
|
100
|
+
object_version=object_version,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return wrapper
|
|
104
|
+
|
|
105
|
+
return decorator
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Shared argument-binding helpers for manager method decorators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def bind_args(param_names: list[str], args: tuple, kwargs: dict) -> dict[str, Any]:
|
|
12
|
+
"""Map positional + keyword call arguments to a name→value dict."""
|
|
13
|
+
bound: dict[str, Any] = {}
|
|
14
|
+
for i, name in enumerate(param_names):
|
|
15
|
+
if i < len(args):
|
|
16
|
+
bound[name] = args[i]
|
|
17
|
+
bound.update(kwargs)
|
|
18
|
+
return bound
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@functools.lru_cache(maxsize=512)
|
|
22
|
+
def _callable_extra_params(fn: Callable) -> tuple[str, ...]:
|
|
23
|
+
"""Return declared parameter names beyond 'self' (cached per callable)."""
|
|
24
|
+
try:
|
|
25
|
+
sig = inspect.signature(fn)
|
|
26
|
+
params = list(sig.parameters.values())
|
|
27
|
+
return tuple(p.name for p in params[1:] if p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD))
|
|
28
|
+
except (ValueError, TypeError):
|
|
29
|
+
return ()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def invoke(fn: Callable, instance, bound_args: dict[str, Any]):
|
|
33
|
+
"""Call fn(instance) or fn(instance, **matching_bound_args) from its signature."""
|
|
34
|
+
extra = _callable_extra_params(fn)
|
|
35
|
+
if not extra:
|
|
36
|
+
return fn(instance)
|
|
37
|
+
kwargs = {name: bound_args[name] for name in extra if name in bound_args}
|
|
38
|
+
return fn(instance, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_resource(instance, resource, bound_args: dict[str, Any]) -> str:
|
|
42
|
+
"""Resolve a resource specification to a string."""
|
|
43
|
+
if resource is None:
|
|
44
|
+
return getattr(instance, "path", "")
|
|
45
|
+
if callable(resource):
|
|
46
|
+
if getattr(resource, "_is_arg_extractor", False):
|
|
47
|
+
return resource(instance, bound_args)
|
|
48
|
+
return invoke(resource, instance, bound_args)
|
|
49
|
+
return getattr(instance, resource, "")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""@require_permissions decorator and PermCheck configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..exceptions import CapabilityDenied, NotFound, PermissionDenied
|
|
13
|
+
from .binding import bind_args, invoke, resolve_resource
|
|
14
|
+
|
|
15
|
+
_NAMESPACE_PREFIX = "namespace"
|
|
16
|
+
_GLOBAL_PREFIX = "global"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PermCheck:
|
|
21
|
+
"""One permission check: action on a resource with optional error masking."""
|
|
22
|
+
|
|
23
|
+
action: str | Callable
|
|
24
|
+
resource: str | Callable | None = None
|
|
25
|
+
mask_as_not_found: bool = False
|
|
26
|
+
not_found_message: str | Callable | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def arg(name: str, transform: Callable | None = None) -> Callable:
|
|
30
|
+
"""Create a resource callable that extracts a named method argument."""
|
|
31
|
+
|
|
32
|
+
def _extractor(instance, bound_args: dict) -> str:
|
|
33
|
+
value = bound_args.get(name, "")
|
|
34
|
+
return transform(value) if transform else value
|
|
35
|
+
|
|
36
|
+
_extractor._is_arg_extractor = True # type: ignore[attr-defined]
|
|
37
|
+
return _extractor
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _coerce_check(check: PermCheck | str | Callable) -> PermCheck:
|
|
41
|
+
if isinstance(check, PermCheck):
|
|
42
|
+
return check
|
|
43
|
+
return PermCheck(action=check)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _apply_check(
|
|
47
|
+
check: PermCheck,
|
|
48
|
+
instance,
|
|
49
|
+
bound_args: dict,
|
|
50
|
+
auth,
|
|
51
|
+
pm,
|
|
52
|
+
) -> None:
|
|
53
|
+
try:
|
|
54
|
+
resolved = invoke(check.action, instance, bound_args) if callable(check.action) else check.action
|
|
55
|
+
if resolved is None:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if isinstance(resolved, list):
|
|
59
|
+
pairs = resolved
|
|
60
|
+
else:
|
|
61
|
+
resource = resolve_resource(instance, check.resource, bound_args)
|
|
62
|
+
pairs = [(resolved, resource)]
|
|
63
|
+
|
|
64
|
+
for action, resource in pairs:
|
|
65
|
+
type_, _, verb = action.partition(":")
|
|
66
|
+
|
|
67
|
+
if type_ == _GLOBAL_PREFIX and verb == "admin":
|
|
68
|
+
if not auth.is_global_admin:
|
|
69
|
+
raise PermissionDenied("Global administrator access required")
|
|
70
|
+
|
|
71
|
+
elif type_ == _NAMESPACE_PREFIX:
|
|
72
|
+
pm.require_namespace_object(resource, verb)
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
request_ctx: dict[str, Any] = {"time": datetime.now(UTC)}
|
|
76
|
+
pm.require_tree(action, resource, request_ctx)
|
|
77
|
+
|
|
78
|
+
except PermissionDenied as exc:
|
|
79
|
+
if check.mask_as_not_found:
|
|
80
|
+
if check.not_found_message is None:
|
|
81
|
+
msg = str(exc)
|
|
82
|
+
elif callable(check.not_found_message):
|
|
83
|
+
msg = check.not_found_message(instance)
|
|
84
|
+
else:
|
|
85
|
+
msg = check.not_found_message
|
|
86
|
+
raise NotFound(msg) from exc
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def require_permissions(
|
|
91
|
+
*checks: PermCheck | str | Callable,
|
|
92
|
+
auth_manager_attribute: str = "auth",
|
|
93
|
+
namespace_attribute: str = "namespace",
|
|
94
|
+
) -> Callable:
|
|
95
|
+
"""Decorator for manager public methods."""
|
|
96
|
+
|
|
97
|
+
def decorator(func: Callable) -> Callable:
|
|
98
|
+
try:
|
|
99
|
+
sig = inspect.signature(func)
|
|
100
|
+
func_params = [
|
|
101
|
+
name
|
|
102
|
+
for name, p in sig.parameters.items()
|
|
103
|
+
if name != "self" and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
|
|
104
|
+
]
|
|
105
|
+
except (ValueError, TypeError):
|
|
106
|
+
func_params = []
|
|
107
|
+
|
|
108
|
+
@functools.wraps(func)
|
|
109
|
+
def wrapper(self, *args, **kwargs):
|
|
110
|
+
auth = getattr(self, auth_manager_attribute, None)
|
|
111
|
+
|
|
112
|
+
if auth is None:
|
|
113
|
+
return func(self, *args, **kwargs)
|
|
114
|
+
|
|
115
|
+
bound_args = bind_args(func_params, args, kwargs)
|
|
116
|
+
namespace = getattr(self, namespace_attribute, None)
|
|
117
|
+
pm = auth.permissions(namespace)
|
|
118
|
+
|
|
119
|
+
for check in checks:
|
|
120
|
+
_apply_check(_coerce_check(check), self, bound_args, auth, pm)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return func(self, *args, **kwargs)
|
|
124
|
+
except CapabilityDenied as exc:
|
|
125
|
+
raise PermissionDenied(str(exc)) from exc
|
|
126
|
+
|
|
127
|
+
return wrapper
|
|
128
|
+
|
|
129
|
+
return decorator
|