unitysvc-core 0.2.37__tar.gz → 0.3.0__tar.gz
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.
- {unitysvc_core-0.2.37/src/unitysvc_core.egg-info → unitysvc_core-0.3.0}/PKG-INFO +59 -26
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/README.md +58 -25
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/pyproject.toml +1 -1
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/grants.py +129 -5
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/__init__.py +29 -0
- unitysvc_core-0.3.0/src/unitysvc_core/models/channel.py +302 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/listing_data.py +0 -11
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/listing_v1.py +0 -1
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/offering_v1.py +6 -23
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/validators.py +100 -56
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/parameter_usage.py +21 -46
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/schema/listing_v1.json +0 -28
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/schema/offering_v1.json +2479 -240
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/validator.py +232 -174
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0/src/unitysvc_core.egg-info}/PKG-INFO +59 -26
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core.egg-info/SOURCES.txt +3 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_canonical_interfaces.py +31 -2
- unitysvc_core-0.3.0/tests/test_channel.py +138 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_grants.py +154 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_parameter_usage.py +51 -53
- unitysvc_core-0.3.0/tests/test_validate_service.py +115 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_validator.py +83 -179
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/LICENSE +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/setup.cfg +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/pricing.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_effective_price.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_listing_audience.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_offering_data.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.37 → unitysvc_core-0.3.0}/tests/test_validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unitysvc-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Shared data models and validation helpers for UnitySVC packages
|
|
5
5
|
Author-email: Bo Peng <bo.peng@unitysvc.com>
|
|
6
6
|
Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
|
|
@@ -57,6 +57,8 @@ src/unitysvc_core/
|
|
|
57
57
|
│ ├── base.py # All enums (status, type, currency, rate-limit, etc.)
|
|
58
58
|
│ ├── pricing.py # PriceStr, UsageData, all *PriceData classes,
|
|
59
59
|
│ │ # Pricing union, validate_pricing, cost calculation
|
|
60
|
+
│ ├── channel.py # UpstreamAccessConfig, ChannelConfig, per-protocol
|
|
61
|
+
│ │ # servers, ChannelParams, RETIRED_CHANNEL_KEYS
|
|
60
62
|
│ ├── documents.py # DocumentData
|
|
61
63
|
│ ├── service.py # RateLimit, ServiceConstraints, AccessInterfaceData,
|
|
62
64
|
│ │ # UpstreamAccessConfigData
|
|
@@ -135,24 +137,26 @@ MIT
|
|
|
135
137
|
|
|
136
138
|
### Required enrollment parameter usage
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
This validates usage, not channel attribution: a document reference counts as a
|
|
151
|
-
consumer without proving that the intended paid channel takes enrollment.
|
|
140
|
+
Each channel's required parameters must be read by that channel or by document
|
|
141
|
+
content (a `.j2` document's template source, which the seller validator reads
|
|
142
|
+
from disk as the SDK uploads it); another channel's use does not count. Metadata
|
|
143
|
+
such as `display_name` or `description` is never rendered with `params`, and a
|
|
144
|
+
user access interface never reads them. File validation checks a listing against
|
|
145
|
+
its paired offering when present; backend ingest calls the same
|
|
146
|
+
`validate_required_parameter_usage` helper with the complete listing and
|
|
147
|
+
offering. Schema declarations alone are not consumers. Jinja attribute access,
|
|
148
|
+
string-key lookups, and `params.get("name")` are inspected; dynamic access and
|
|
149
|
+
whole-map iteration conservatively consume all parameters. Deprecated content can
|
|
150
|
+
still be retired.
|
|
152
151
|
|
|
153
152
|
### Canonical user access interfaces
|
|
154
153
|
|
|
155
|
-
A user access interface is the service's canonical address, the same for every caller
|
|
154
|
+
A user access interface is the service's canonical address, the same for every caller. `validate_listing_interfaces_are_canonical` renders `{{ service_name }}` (the one value the platform fills in when it stores an interface) and then requires:
|
|
155
|
+
|
|
156
|
+
- a `base_url` that is one of the four gateways, `${API_GATEWAY_BASE_URL}`, `${S3_GATEWAY_BASE_URL}`, `${SMTP_GATEWAY_BASE_URL}` or `${MCP_GATEWAY_BASE_URL}`, optionally followed by `/` and a path (`/a/<alias>` included); never an external URL, another variable or a secret reference;
|
|
157
|
+
- no template left over. `{{ routing_vars.X }}` would stay literal in the stored interface and never route, and a per-enrollment value (`enrollment.*`, `params.*`) is never canonical: an enrollment is reached through `/e/<CODE>` on the API gateway, which a listing never declares.
|
|
158
|
+
|
|
159
|
+
The one exception is `{{ enrollment.code }}` as the whole SMTP `routing_key.username` or the whole S3 bucket segment, a reminder of the per-enrollment address on protocols without a path for it. The shared validator runs it on every `listing_v1`; backend ingest calls the same function (unitysvc/unitysvc#2726).
|
|
156
160
|
|
|
157
161
|
### Listing audience
|
|
158
162
|
|
|
@@ -193,7 +197,7 @@ Three stages (unitysvc/unitysvc#2720): a target service's **`exports`** (what it
|
|
|
193
197
|
- **Without `permissions`** (most services, including one that declares only `options`), the service exports one permission, `invoke`, on every route (`"* /*"`); `service:<name>` and the `service:*` wildcard grant it.
|
|
194
198
|
- **With `permissions`**, the service exports exactly those, and `invoke` only if it declares it too. A data service (`me`, `log`, `needs`) declares `read` / `write` and no `invoke`: it holds the customer's own data, so only a grant that names its permissions reaches it, never `invoke` or a wildcard.
|
|
195
199
|
- `permissions`: a name (lowercase letters, digits and `_`) → the routes it covers, each `"<METHOD> /<path>"` (`*` for any method, `*` globs in the path). Permissions are never inferred from HTTP methods.
|
|
196
|
-
- `options`: a JSON Schema (draft 7) object of the customizations a grant may set on the service, like
|
|
200
|
+
- `options`: a JSON Schema (draft 7) object of the customizations a grant may set on the service, like a channel's `params.schema`, with an optional react-jsonschema-form `options_ui_schema`; the frontend renders it with the same form component. It covers limits only the service can apply (a message size). Restrictions so a grant can write every value in query-string syntax: property names are lowercase letters, digits and `_`; each property is a `string`, `integer`, `number`, `boolean` or an `array` of strings, serialized as an OpenAPI form-style query parameter with arrays comma-joined (`style: form, explode: false`); options are optional (no `required`) and only declared properties are accepted; the schema is self-contained (no `$ref`), so validating against it never fetches anything. A property's `default` is what the service does when the policy does not set the option.
|
|
197
201
|
|
|
198
202
|
**2. A customer access request (`upstream_access_config.<channel>.customer_access_requests`), and the policy entry it becomes.** Any channel may declare its own list; each approved request becomes an entry (a grant) in the issued key's policy, in the same shape:
|
|
199
203
|
|
|
@@ -204,10 +208,11 @@ Three stages (unitysvc/unitysvc#2720): a target service's **`exports`** (what it
|
|
|
204
208
|
"limits": {"count_per_day": 1000}}]
|
|
205
209
|
```
|
|
206
210
|
|
|
207
|
-
- `object` names one service (`service:<name>`, no wildcards). `enrollment:<CODE>` (one enrollment of an enrollable service) may be allowed later.
|
|
211
|
+
- `object` names one service (`service:<name>`, no wildcards), or `apikey` for the leased key's lifespan (below). `enrollment:<CODE>` (one enrollment of an enrollable service) may be allowed later.
|
|
208
212
|
- `options` is written in query-string syntax (`parse_options`; a notation, not how options are delivered): each name once, each value non-empty; with a catalog, each value is converted to its declared type and validated against the target's `options` schema.
|
|
209
213
|
- **Enrollment is where options and limits are set.** In a request, `options` and `limits` only pre-fill the enrollment form for convenience. When enrolling, the customer confirms and sets the options (a form rendered from the target's `options` schema) and the limits, and adds their own limits; the result is the issued key's policy.
|
|
210
214
|
- `limits` (`validate_limits`): `count_per_hour` / `count_per_day` / `count_per_month` / `count_total` (positive integer) and the same for `amount_` (finite positive decimal, as a number or a string). Limits are universal: the platform meters every call, so any service can be limited without exporting anything. The seller's limits are a pre-fill; the customer sets them at enrollment, and an enrollment's own limits (unitysvc/unitysvc#2687) use the same shape. They are counted per policy entry (the enrollment's calls to that service) from the usage stream (counters keyed by scope, metric and window bucket), and the gateway refuses calls once one is exceeded.
|
|
215
|
+
- **A longer-lived leased key** is asked for with one `apikey` entry: `{"object": "apikey", "limits": {"api_key_lifespan": 14400}, "justification": "Batch runs take up to four hours"}`. It names the key itself, so it takes only `limits` (exactly `api_key_lifespan`, whole seconds from 1 to 86400) and a `justification`, needs no catalog lookup, and grants nothing. Without it, a key the seller leases for the enrollment lives `DEFAULT_API_KEY_LIFESPAN` (3600 s). `api_key_lifespan` is valid only on this entry.
|
|
211
216
|
|
|
212
217
|
**3. How options reach the service.** Not through the upstream URL: the gateway never changes a target's `base_url` or query to carry policy. How an option is applied is up to the service's implementation:
|
|
213
218
|
|
|
@@ -224,9 +229,40 @@ Three stages (unitysvc/unitysvc#2720): a target service's **`exports`** (what it
|
|
|
224
229
|
- `DataValidator(data_dir, schema_dir, exports_resolver=...)` runs both checks on every `offering_v1` (`exports` and each channel's `customer_access_requests`). The seller SDK passes a resolver that downloads the catalog bundle once and reads the target row's `exports`; offline it passes none and gets the shape checks.
|
|
225
230
|
- Backend ingest calls the same function with a resolver over the published services, so an unknown target, permission or option fails at upload.
|
|
226
231
|
|
|
232
|
+
**6. From approvals to a policy (`build_policy`).** When the customer enrolls, `build_policy(requests, approvals, resolve)` turns their approvals of the channel's requests into the policy the enrollment keeps and every key leased for it copies (unitysvc/unitysvc#2766):
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
[{"object": "service:log", "permissions": ["invoke"], "options": "log_level=error", "limits": {"count_per_day": 200}},
|
|
236
|
+
{"object": "apikey", "limits": {"api_key_lifespan": 7200}}]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- Each approval names a requested object, at most once, with some of the permissions requested. Every `required` request must be approved; optional ones may be left out.
|
|
240
|
+
- Options and limits are the customer's. They are checked against the target's `exports` (with `resolve`) and `validate_limits`, not against the request, whose values only pre-fill the form.
|
|
241
|
+
- The `apikey` lifespan may be shortened, never lengthened beyond the request.
|
|
242
|
+
- The result drops `required` / `justification` and empty `options` / `limits`. `api_key_lifespan(policy)` reads the lifespan back (the default when there is none).
|
|
243
|
+
|
|
244
|
+
### Upstream access channels
|
|
245
|
+
|
|
246
|
+
`upstream_access_config` maps a channel name to a channel (`UpstreamAccessConfig`, `ChannelConfig`; unitysvc/unitysvc#2730). A channel describes one way the platform reaches the upstream, as a whole, and its `servers` hold the connection, typed by the channel's protocol:
|
|
247
|
+
|
|
248
|
+
| Channel key | What it is |
|
|
249
|
+
| --- | --- |
|
|
250
|
+
| `access_method` | `http` (default), `smtp`, `s3` or `mcp`; selects the server type |
|
|
251
|
+
| `type` | normally derived by the platform; declare `external` (or `enrollable`) to state it |
|
|
252
|
+
| `description`, `is_active`, `sort_order` | display text; inactive channels are skipped; rank, lowest first |
|
|
253
|
+
| `request_formats`, `applicable_interfaces` | the formats the channel serves; the interfaces that reach it (display only) |
|
|
254
|
+
| `rate_limit_refs` | the provider rate limits it consumes |
|
|
255
|
+
| `customer_access_requests` | what the enrolling customer is asked to approve (see above) |
|
|
256
|
+
| `params` | the enrollment parameters (below) |
|
|
257
|
+
| `servers` | the connection: `HttpServer`, `SmtpServer`, `S3Server` or `McpServer` |
|
|
258
|
+
|
|
259
|
+
The common one-server case writes the connection fields directly on the channel; they are read as `servers[0]`. When a channel has both, each server inherits the direct fields it does not set. Any string may be a template (`{{ params.X }}`, `${ secrets.X }`, `${ customer_secrets.X ?? default }`), so numeric and boolean connection fields also accept strings.
|
|
260
|
+
|
|
261
|
+
Unknown keys are refused. Retired keys are refused with what to do instead (`RETIRED_CHANNEL_KEYS`): `raw`, `is_primary`, the listing-style parameter trio, `grants`, `rate_limits` (nothing reads it; use `rate_limit_refs`), `storage_type` (use `access_method: s3`), `response_rules`, `timeout_ms`, `transport`, and the platform-derived `customer_secrets_required` / `customer_secrets_optional`.
|
|
262
|
+
|
|
227
263
|
### Enrollment parameters on channels
|
|
228
264
|
|
|
229
|
-
A channel
|
|
265
|
+
A channel declares the enrollment parameters it reads, so each plan of a service asks for what it needs (unitysvc/unitysvc#2726). A listing declares none: `user_parameters_schema`, `user_parameters_ui_schema` and `service_options.ops_testing_parameters` are refused.
|
|
230
266
|
|
|
231
267
|
```json
|
|
232
268
|
"upstream_access_config": {
|
|
@@ -241,11 +277,8 @@ A channel may declare its own enrollment parameters, so each plan of a service a
|
|
|
241
277
|
}
|
|
242
278
|
```
|
|
243
279
|
|
|
244
|
-
`params.schema` is a JSON Schema of the parameters (a property's own `default` pre-fills the customer's form), `params.ui_schema` holds form hints, and `params.testing_defaults` holds the values the platform tests the channel with.
|
|
245
|
-
|
|
246
|
-
- a channel reads only the parameters its `params.schema` declares, and a channel that reads `{{ params.* }}` must declare its own `params` (`validate_channel_parameters`, on every `offering_v1`);
|
|
247
|
-
- each required parameter has a value in that channel's `params.testing_defaults`;
|
|
248
|
-
- each required parameter is read by its own channel or by document content; another channel's use does not count (`validate_required_parameter_usage`, which backend ingest also calls);
|
|
249
|
-
- the listing must not also declare `user_parameters_schema`.
|
|
280
|
+
`params.schema` is a JSON Schema of the parameters (a property's own `default` pre-fills the customer's form), `params.ui_schema` holds form hints, and `params.testing_defaults` holds the values the platform tests the channel with. Rules:
|
|
250
281
|
|
|
251
|
-
|
|
282
|
+
- a channel reads only the parameters its `params.schema` declares, and a channel that reads `{{ params.* }}` must declare `params` (`validate_channel_parameters`, on every `offering_v1`);
|
|
283
|
+
- each required parameter has a value in that channel's `params.testing_defaults`, which is also what `${ customer_secrets.{{ params.X }} }` renders with when secret references are checked;
|
|
284
|
+
- each required parameter is read by its own channel or by document content (`validate_required_parameter_usage`, which backend ingest also calls).
|
|
@@ -22,6 +22,8 @@ src/unitysvc_core/
|
|
|
22
22
|
│ ├── base.py # All enums (status, type, currency, rate-limit, etc.)
|
|
23
23
|
│ ├── pricing.py # PriceStr, UsageData, all *PriceData classes,
|
|
24
24
|
│ │ # Pricing union, validate_pricing, cost calculation
|
|
25
|
+
│ ├── channel.py # UpstreamAccessConfig, ChannelConfig, per-protocol
|
|
26
|
+
│ │ # servers, ChannelParams, RETIRED_CHANNEL_KEYS
|
|
25
27
|
│ ├── documents.py # DocumentData
|
|
26
28
|
│ ├── service.py # RateLimit, ServiceConstraints, AccessInterfaceData,
|
|
27
29
|
│ │ # UpstreamAccessConfigData
|
|
@@ -100,24 +102,26 @@ MIT
|
|
|
100
102
|
|
|
101
103
|
### Required enrollment parameter usage
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
This validates usage, not channel attribution: a document reference counts as a
|
|
116
|
-
consumer without proving that the intended paid channel takes enrollment.
|
|
105
|
+
Each channel's required parameters must be read by that channel or by document
|
|
106
|
+
content (a `.j2` document's template source, which the seller validator reads
|
|
107
|
+
from disk as the SDK uploads it); another channel's use does not count. Metadata
|
|
108
|
+
such as `display_name` or `description` is never rendered with `params`, and a
|
|
109
|
+
user access interface never reads them. File validation checks a listing against
|
|
110
|
+
its paired offering when present; backend ingest calls the same
|
|
111
|
+
`validate_required_parameter_usage` helper with the complete listing and
|
|
112
|
+
offering. Schema declarations alone are not consumers. Jinja attribute access,
|
|
113
|
+
string-key lookups, and `params.get("name")` are inspected; dynamic access and
|
|
114
|
+
whole-map iteration conservatively consume all parameters. Deprecated content can
|
|
115
|
+
still be retired.
|
|
117
116
|
|
|
118
117
|
### Canonical user access interfaces
|
|
119
118
|
|
|
120
|
-
A user access interface is the service's canonical address, the same for every caller
|
|
119
|
+
A user access interface is the service's canonical address, the same for every caller. `validate_listing_interfaces_are_canonical` renders `{{ service_name }}` (the one value the platform fills in when it stores an interface) and then requires:
|
|
120
|
+
|
|
121
|
+
- a `base_url` that is one of the four gateways, `${API_GATEWAY_BASE_URL}`, `${S3_GATEWAY_BASE_URL}`, `${SMTP_GATEWAY_BASE_URL}` or `${MCP_GATEWAY_BASE_URL}`, optionally followed by `/` and a path (`/a/<alias>` included); never an external URL, another variable or a secret reference;
|
|
122
|
+
- no template left over. `{{ routing_vars.X }}` would stay literal in the stored interface and never route, and a per-enrollment value (`enrollment.*`, `params.*`) is never canonical: an enrollment is reached through `/e/<CODE>` on the API gateway, which a listing never declares.
|
|
123
|
+
|
|
124
|
+
The one exception is `{{ enrollment.code }}` as the whole SMTP `routing_key.username` or the whole S3 bucket segment, a reminder of the per-enrollment address on protocols without a path for it. The shared validator runs it on every `listing_v1`; backend ingest calls the same function (unitysvc/unitysvc#2726).
|
|
121
125
|
|
|
122
126
|
### Listing audience
|
|
123
127
|
|
|
@@ -158,7 +162,7 @@ Three stages (unitysvc/unitysvc#2720): a target service's **`exports`** (what it
|
|
|
158
162
|
- **Without `permissions`** (most services, including one that declares only `options`), the service exports one permission, `invoke`, on every route (`"* /*"`); `service:<name>` and the `service:*` wildcard grant it.
|
|
159
163
|
- **With `permissions`**, the service exports exactly those, and `invoke` only if it declares it too. A data service (`me`, `log`, `needs`) declares `read` / `write` and no `invoke`: it holds the customer's own data, so only a grant that names its permissions reaches it, never `invoke` or a wildcard.
|
|
160
164
|
- `permissions`: a name (lowercase letters, digits and `_`) → the routes it covers, each `"<METHOD> /<path>"` (`*` for any method, `*` globs in the path). Permissions are never inferred from HTTP methods.
|
|
161
|
-
- `options`: a JSON Schema (draft 7) object of the customizations a grant may set on the service, like
|
|
165
|
+
- `options`: a JSON Schema (draft 7) object of the customizations a grant may set on the service, like a channel's `params.schema`, with an optional react-jsonschema-form `options_ui_schema`; the frontend renders it with the same form component. It covers limits only the service can apply (a message size). Restrictions so a grant can write every value in query-string syntax: property names are lowercase letters, digits and `_`; each property is a `string`, `integer`, `number`, `boolean` or an `array` of strings, serialized as an OpenAPI form-style query parameter with arrays comma-joined (`style: form, explode: false`); options are optional (no `required`) and only declared properties are accepted; the schema is self-contained (no `$ref`), so validating against it never fetches anything. A property's `default` is what the service does when the policy does not set the option.
|
|
162
166
|
|
|
163
167
|
**2. A customer access request (`upstream_access_config.<channel>.customer_access_requests`), and the policy entry it becomes.** Any channel may declare its own list; each approved request becomes an entry (a grant) in the issued key's policy, in the same shape:
|
|
164
168
|
|
|
@@ -169,10 +173,11 @@ Three stages (unitysvc/unitysvc#2720): a target service's **`exports`** (what it
|
|
|
169
173
|
"limits": {"count_per_day": 1000}}]
|
|
170
174
|
```
|
|
171
175
|
|
|
172
|
-
- `object` names one service (`service:<name>`, no wildcards). `enrollment:<CODE>` (one enrollment of an enrollable service) may be allowed later.
|
|
176
|
+
- `object` names one service (`service:<name>`, no wildcards), or `apikey` for the leased key's lifespan (below). `enrollment:<CODE>` (one enrollment of an enrollable service) may be allowed later.
|
|
173
177
|
- `options` is written in query-string syntax (`parse_options`; a notation, not how options are delivered): each name once, each value non-empty; with a catalog, each value is converted to its declared type and validated against the target's `options` schema.
|
|
174
178
|
- **Enrollment is where options and limits are set.** In a request, `options` and `limits` only pre-fill the enrollment form for convenience. When enrolling, the customer confirms and sets the options (a form rendered from the target's `options` schema) and the limits, and adds their own limits; the result is the issued key's policy.
|
|
175
179
|
- `limits` (`validate_limits`): `count_per_hour` / `count_per_day` / `count_per_month` / `count_total` (positive integer) and the same for `amount_` (finite positive decimal, as a number or a string). Limits are universal: the platform meters every call, so any service can be limited without exporting anything. The seller's limits are a pre-fill; the customer sets them at enrollment, and an enrollment's own limits (unitysvc/unitysvc#2687) use the same shape. They are counted per policy entry (the enrollment's calls to that service) from the usage stream (counters keyed by scope, metric and window bucket), and the gateway refuses calls once one is exceeded.
|
|
180
|
+
- **A longer-lived leased key** is asked for with one `apikey` entry: `{"object": "apikey", "limits": {"api_key_lifespan": 14400}, "justification": "Batch runs take up to four hours"}`. It names the key itself, so it takes only `limits` (exactly `api_key_lifespan`, whole seconds from 1 to 86400) and a `justification`, needs no catalog lookup, and grants nothing. Without it, a key the seller leases for the enrollment lives `DEFAULT_API_KEY_LIFESPAN` (3600 s). `api_key_lifespan` is valid only on this entry.
|
|
176
181
|
|
|
177
182
|
**3. How options reach the service.** Not through the upstream URL: the gateway never changes a target's `base_url` or query to carry policy. How an option is applied is up to the service's implementation:
|
|
178
183
|
|
|
@@ -189,9 +194,40 @@ Three stages (unitysvc/unitysvc#2720): a target service's **`exports`** (what it
|
|
|
189
194
|
- `DataValidator(data_dir, schema_dir, exports_resolver=...)` runs both checks on every `offering_v1` (`exports` and each channel's `customer_access_requests`). The seller SDK passes a resolver that downloads the catalog bundle once and reads the target row's `exports`; offline it passes none and gets the shape checks.
|
|
190
195
|
- Backend ingest calls the same function with a resolver over the published services, so an unknown target, permission or option fails at upload.
|
|
191
196
|
|
|
197
|
+
**6. From approvals to a policy (`build_policy`).** When the customer enrolls, `build_policy(requests, approvals, resolve)` turns their approvals of the channel's requests into the policy the enrollment keeps and every key leased for it copies (unitysvc/unitysvc#2766):
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
[{"object": "service:log", "permissions": ["invoke"], "options": "log_level=error", "limits": {"count_per_day": 200}},
|
|
201
|
+
{"object": "apikey", "limits": {"api_key_lifespan": 7200}}]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- Each approval names a requested object, at most once, with some of the permissions requested. Every `required` request must be approved; optional ones may be left out.
|
|
205
|
+
- Options and limits are the customer's. They are checked against the target's `exports` (with `resolve`) and `validate_limits`, not against the request, whose values only pre-fill the form.
|
|
206
|
+
- The `apikey` lifespan may be shortened, never lengthened beyond the request.
|
|
207
|
+
- The result drops `required` / `justification` and empty `options` / `limits`. `api_key_lifespan(policy)` reads the lifespan back (the default when there is none).
|
|
208
|
+
|
|
209
|
+
### Upstream access channels
|
|
210
|
+
|
|
211
|
+
`upstream_access_config` maps a channel name to a channel (`UpstreamAccessConfig`, `ChannelConfig`; unitysvc/unitysvc#2730). A channel describes one way the platform reaches the upstream, as a whole, and its `servers` hold the connection, typed by the channel's protocol:
|
|
212
|
+
|
|
213
|
+
| Channel key | What it is |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `access_method` | `http` (default), `smtp`, `s3` or `mcp`; selects the server type |
|
|
216
|
+
| `type` | normally derived by the platform; declare `external` (or `enrollable`) to state it |
|
|
217
|
+
| `description`, `is_active`, `sort_order` | display text; inactive channels are skipped; rank, lowest first |
|
|
218
|
+
| `request_formats`, `applicable_interfaces` | the formats the channel serves; the interfaces that reach it (display only) |
|
|
219
|
+
| `rate_limit_refs` | the provider rate limits it consumes |
|
|
220
|
+
| `customer_access_requests` | what the enrolling customer is asked to approve (see above) |
|
|
221
|
+
| `params` | the enrollment parameters (below) |
|
|
222
|
+
| `servers` | the connection: `HttpServer`, `SmtpServer`, `S3Server` or `McpServer` |
|
|
223
|
+
|
|
224
|
+
The common one-server case writes the connection fields directly on the channel; they are read as `servers[0]`. When a channel has both, each server inherits the direct fields it does not set. Any string may be a template (`{{ params.X }}`, `${ secrets.X }`, `${ customer_secrets.X ?? default }`), so numeric and boolean connection fields also accept strings.
|
|
225
|
+
|
|
226
|
+
Unknown keys are refused. Retired keys are refused with what to do instead (`RETIRED_CHANNEL_KEYS`): `raw`, `is_primary`, the listing-style parameter trio, `grants`, `rate_limits` (nothing reads it; use `rate_limit_refs`), `storage_type` (use `access_method: s3`), `response_rules`, `timeout_ms`, `transport`, and the platform-derived `customer_secrets_required` / `customer_secrets_optional`.
|
|
227
|
+
|
|
192
228
|
### Enrollment parameters on channels
|
|
193
229
|
|
|
194
|
-
A channel
|
|
230
|
+
A channel declares the enrollment parameters it reads, so each plan of a service asks for what it needs (unitysvc/unitysvc#2726). A listing declares none: `user_parameters_schema`, `user_parameters_ui_schema` and `service_options.ops_testing_parameters` are refused.
|
|
195
231
|
|
|
196
232
|
```json
|
|
197
233
|
"upstream_access_config": {
|
|
@@ -206,11 +242,8 @@ A channel may declare its own enrollment parameters, so each plan of a service a
|
|
|
206
242
|
}
|
|
207
243
|
```
|
|
208
244
|
|
|
209
|
-
`params.schema` is a JSON Schema of the parameters (a property's own `default` pre-fills the customer's form), `params.ui_schema` holds form hints, and `params.testing_defaults` holds the values the platform tests the channel with.
|
|
210
|
-
|
|
211
|
-
- a channel reads only the parameters its `params.schema` declares, and a channel that reads `{{ params.* }}` must declare its own `params` (`validate_channel_parameters`, on every `offering_v1`);
|
|
212
|
-
- each required parameter has a value in that channel's `params.testing_defaults`;
|
|
213
|
-
- each required parameter is read by its own channel or by document content; another channel's use does not count (`validate_required_parameter_usage`, which backend ingest also calls);
|
|
214
|
-
- the listing must not also declare `user_parameters_schema`.
|
|
245
|
+
`params.schema` is a JSON Schema of the parameters (a property's own `default` pre-fills the customer's form), `params.ui_schema` holds form hints, and `params.testing_defaults` holds the values the platform tests the channel with. Rules:
|
|
215
246
|
|
|
216
|
-
|
|
247
|
+
- a channel reads only the parameters its `params.schema` declares, and a channel that reads `{{ params.* }}` must declare `params` (`validate_channel_parameters`, on every `offering_v1`);
|
|
248
|
+
- each required parameter has a value in that channel's `params.testing_defaults`, which is also what `${ customer_secrets.{{ params.X }} }` renders with when secret references are checked;
|
|
249
|
+
- each required parameter is read by its own channel or by document content (`validate_required_parameter_usage`, which backend ingest also calls).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitysvc-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Shared data models and validation helpers for UnitySVC packages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Bo Peng", email = "bo.peng@unitysvc.com" }]
|
|
@@ -22,7 +22,7 @@ Three stages (unitysvc/unitysvc#2720):
|
|
|
22
22
|
that names its permissions, never by ``invoke`` or a wildcard. Permissions
|
|
23
23
|
name the routes they cover; they are never inferred from HTTP methods
|
|
24
24
|
(``POST /chat/completions`` modifies nothing). ``options`` is a JSON Schema,
|
|
25
|
-
like
|
|
25
|
+
like a channel's ``params.schema`` (with an optional
|
|
26
26
|
``options_ui_schema``), of the customizations a grant may set on the
|
|
27
27
|
service, including limits only it can apply (a message size); a property's
|
|
28
28
|
``default`` is what the service does when the policy does not set the
|
|
@@ -72,8 +72,11 @@ customer consent and key issuance establish its *authority*.
|
|
|
72
72
|
be limited by ``count`` or ``amount`` per hour, day or month, or in total,
|
|
73
73
|
without exporting anything (``{"count_per_day": 2, "amount_per_month": "2.00"}``).
|
|
74
74
|
|
|
75
|
-
Today a request's ``object`` is ``service:<name
|
|
76
|
-
enrollment
|
|
75
|
+
Today a request's ``object`` is ``service:<name>``, or ``apikey``: how long a
|
|
76
|
+
key leased for the enrollment lives (``limits.api_key_lifespan``, up to a day,
|
|
77
|
+
with a justification), which grants nothing. ``enrollment:<CODE>`` (one
|
|
78
|
+
enrollment of an enrollable service) may follow. :func:`build_policy` turns a
|
|
79
|
+
customer's approvals into the policy an enrollment keeps.
|
|
77
80
|
|
|
78
81
|
There is no central list of services. Core does no I/O: every request's
|
|
79
82
|
permissions and options are checked against its target's ``exports`` when the
|
|
@@ -101,6 +104,13 @@ from .models.validators import validate_service_identifier
|
|
|
101
104
|
|
|
102
105
|
SERVICE_PREFIX = "service:"
|
|
103
106
|
INVOKE = "invoke"
|
|
107
|
+
# The key itself: a request for a longer-lived leased key, and the approved
|
|
108
|
+
# lifespan in a policy. It grants nothing.
|
|
109
|
+
APIKEY_OBJECT = "apikey"
|
|
110
|
+
API_KEY_LIFESPAN = "api_key_lifespan"
|
|
111
|
+
# Seconds a leased key lives unless the policy says otherwise, and at most.
|
|
112
|
+
DEFAULT_API_KEY_LIFESPAN = 3600
|
|
113
|
+
MAX_API_KEY_LIFESPAN = 86400
|
|
104
114
|
# The channel key a seller's requests are declared under.
|
|
105
115
|
REQUESTS_KEY = "customer_access_requests"
|
|
106
116
|
|
|
@@ -144,6 +154,9 @@ DEFAULT_PERMISSIONS: dict[str, tuple[str, ...]] = {INVOKE: (ALL_ROUTES,)}
|
|
|
144
154
|
DEFAULT_EXPORTS = Exports(DEFAULT_PERMISSIONS, {})
|
|
145
155
|
|
|
146
156
|
_REQUEST_KEYS = frozenset({"object", "permissions", "required", "justification", "options", "limits"})
|
|
157
|
+
_APIKEY_REQUEST_KEYS = frozenset({"object", "justification", "limits"})
|
|
158
|
+
# What a policy entry (an approved request) holds.
|
|
159
|
+
_POLICY_KEYS = frozenset({"object", "permissions", "options", "limits"})
|
|
147
160
|
|
|
148
161
|
|
|
149
162
|
def _strings(value: Any) -> bool:
|
|
@@ -310,15 +323,29 @@ def parse_options(where: str, options: Any) -> dict[str, str]:
|
|
|
310
323
|
return parsed
|
|
311
324
|
|
|
312
325
|
|
|
313
|
-
def validate_limits(where: str, limits: Any) -> None:
|
|
326
|
+
def validate_limits(where: str, limits: Any, *, apikey: bool = False) -> None:
|
|
314
327
|
"""Refuse ``limits`` unless it maps ``<metric>_per_<window>`` or
|
|
315
328
|
``<metric>_total`` to a maximum: ``count`` a positive integer, ``amount`` a
|
|
316
329
|
finite positive decimal, as a number (``2.5``) or a string (``"2.50"``).
|
|
317
330
|
Windows: hour, day, month. The same shape serves a request's suggested
|
|
318
331
|
limits, the limits a customer sets in a key's policy, and an enrollment's own
|
|
319
|
-
limits.
|
|
332
|
+
limits.
|
|
333
|
+
|
|
334
|
+
With ``apikey`` (the ``apikey`` entry), the only limit is
|
|
335
|
+
``api_key_lifespan``: how many seconds a leased key lives, an integer from 1
|
|
336
|
+
to :data:`MAX_API_KEY_LIFESPAN`.
|
|
337
|
+
"""
|
|
320
338
|
if not isinstance(limits, dict):
|
|
321
339
|
raise ValueError(f'{where} must be an object like {{"count_per_day": 2}}')
|
|
340
|
+
if apikey:
|
|
341
|
+
if set(limits) != {API_KEY_LIFESPAN}:
|
|
342
|
+
raise ValueError(f'{where} must be {{"{API_KEY_LIFESPAN}": <seconds>}}')
|
|
343
|
+
value = limits[API_KEY_LIFESPAN]
|
|
344
|
+
if not isinstance(value, int) or isinstance(value, bool) or not 0 < value <= MAX_API_KEY_LIFESPAN:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"{where}.{API_KEY_LIFESPAN} must be a whole number of seconds from 1 to {MAX_API_KEY_LIFESPAN}"
|
|
347
|
+
)
|
|
348
|
+
return
|
|
322
349
|
for key, value in limits.items():
|
|
323
350
|
at = f"{where}.{key}"
|
|
324
351
|
match = _LIMIT.match(key) if isinstance(key, str) else None
|
|
@@ -351,6 +378,12 @@ def validate_access_requests(where: str, requests: Any, resolve: ExportsResolver
|
|
|
351
378
|
if not isinstance(entry, dict) or not isinstance(entry.get("object"), str):
|
|
352
379
|
raise ValueError(f'{at} must be {{"object": "service:<name>", ...}}')
|
|
353
380
|
obj = entry["object"]
|
|
381
|
+
if obj == APIKEY_OBJECT:
|
|
382
|
+
if obj in seen:
|
|
383
|
+
raise ValueError(f"{at}: {obj} is listed twice")
|
|
384
|
+
seen.add(obj)
|
|
385
|
+
_check_apikey_request(at, entry)
|
|
386
|
+
continue
|
|
354
387
|
if not obj.startswith(SERVICE_PREFIX):
|
|
355
388
|
raise ValueError(f"{at}: {obj!r} cannot be requested; name one service as 'service:<name>'")
|
|
356
389
|
name = obj.removeprefix(SERVICE_PREFIX)
|
|
@@ -383,6 +416,16 @@ def validate_access_requests(where: str, requests: Any, resolve: ExportsResolver
|
|
|
383
416
|
check_claim(at, obj, permissions, options, parse_exports(f"{obj} exports", declared))
|
|
384
417
|
|
|
385
418
|
|
|
419
|
+
def _check_apikey_request(at: str, entry: dict[str, Any]) -> None:
|
|
420
|
+
"""The ``apikey`` request: how long a leased key should live, with why."""
|
|
421
|
+
if extra := set(entry) - _APIKEY_REQUEST_KEYS:
|
|
422
|
+
raise ValueError(f"{at}: unknown keys {sorted(extra)} (the {APIKEY_OBJECT} request takes only limits)")
|
|
423
|
+
justification = entry.get("justification")
|
|
424
|
+
if not isinstance(justification, str) or not justification.strip():
|
|
425
|
+
raise ValueError(f"{at}.justification must say why a leased key needs to live this long; the customer reads it")
|
|
426
|
+
validate_limits(f"{at}.limits", entry.get("limits"), apikey=True)
|
|
427
|
+
|
|
428
|
+
|
|
386
429
|
def check_claim(where: str, obj: str, permissions: list[str], options: dict[str, str], exports: Exports) -> None:
|
|
387
430
|
"""Refuse a claim on ``obj`` (``service:<name>``) that its ``exports`` don't cover.
|
|
388
431
|
|
|
@@ -410,3 +453,84 @@ def validate_channel_access_requests(config: Any, resolve: ExportsResolver | Non
|
|
|
410
453
|
for name, channel in config.items():
|
|
411
454
|
if isinstance(channel, dict) and REQUESTS_KEY in channel:
|
|
412
455
|
validate_access_requests(f"upstream_access_config.{name}.{REQUESTS_KEY}", channel[REQUESTS_KEY], resolve)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def build_policy(requests: Any, approvals: Any, resolve: ExportsResolver | None = None) -> list[dict[str, Any]]:
|
|
459
|
+
"""The key policy a customer's ``approvals`` of a channel's ``requests``
|
|
460
|
+
make (unitysvc/unitysvc#2766).
|
|
461
|
+
|
|
462
|
+
``requests`` is the channel's ``customer_access_requests``; ``approvals``
|
|
463
|
+
lists the requests the customer approves, each ``{"object", "permissions",
|
|
464
|
+
"options", "limits"}`` (the ``apikey`` entry only ``{"object", "limits"}``).
|
|
465
|
+
Each approval names a requested object, at most once; its permissions are
|
|
466
|
+
some of those requested; every ``required`` request is approved. The
|
|
467
|
+
customer sets options and limits: a request's own only pre-fill them, so
|
|
468
|
+
they are checked against the target's ``exports`` (with ``resolve``) and
|
|
469
|
+
:func:`validate_limits`, not against the request. A key lifespan may only
|
|
470
|
+
be shortened. Returns the approvals as policy entries, without empty
|
|
471
|
+
``options`` / ``limits``. Raises ``ValueError``.
|
|
472
|
+
"""
|
|
473
|
+
validate_access_requests("requests", requests)
|
|
474
|
+
requested = {entry["object"]: entry for entry in requests}
|
|
475
|
+
if not isinstance(approvals, list):
|
|
476
|
+
raise ValueError("approvals must be a list of approved requests")
|
|
477
|
+
policy: list[dict[str, Any]] = []
|
|
478
|
+
for index, approval in enumerate(approvals):
|
|
479
|
+
at = f"approvals[{index}]"
|
|
480
|
+
if not isinstance(approval, dict) or not isinstance(approval.get("object"), str):
|
|
481
|
+
raise ValueError(f'{at} must be {{"object": ..., "permissions": [...]}}')
|
|
482
|
+
obj = approval["object"]
|
|
483
|
+
request = requested.get(obj)
|
|
484
|
+
if request is None:
|
|
485
|
+
raise ValueError(f"{at}: {obj} is not requested")
|
|
486
|
+
if any(entry["object"] == obj for entry in policy):
|
|
487
|
+
raise ValueError(f"{at}: {obj} is approved twice")
|
|
488
|
+
if obj == APIKEY_OBJECT:
|
|
489
|
+
if extra := set(approval) - {"object", "limits"}:
|
|
490
|
+
raise ValueError(f"{at}: unknown keys {sorted(extra)} (the {APIKEY_OBJECT} entry takes only limits)")
|
|
491
|
+
validate_limits(f"{at}.limits", approval.get("limits"), apikey=True)
|
|
492
|
+
asked = request["limits"][API_KEY_LIFESPAN]
|
|
493
|
+
if approval["limits"][API_KEY_LIFESPAN] > asked:
|
|
494
|
+
raise ValueError(f"{at}.limits.{API_KEY_LIFESPAN}: the service asks for at most {asked} seconds")
|
|
495
|
+
policy.append({"object": obj, "limits": dict(approval["limits"])})
|
|
496
|
+
continue
|
|
497
|
+
if extra := set(approval) - _POLICY_KEYS:
|
|
498
|
+
raise ValueError(f"{at}: unknown keys {sorted(extra)}")
|
|
499
|
+
permissions: list[str] = approval.get("permissions") or []
|
|
500
|
+
if not _strings(permissions):
|
|
501
|
+
raise ValueError(f"{at}.permissions must be a non-empty list of distinct names")
|
|
502
|
+
if not set(permissions) <= set(request["permissions"]):
|
|
503
|
+
raise ValueError(f"{at}.permissions: the service requests only {request['permissions']}")
|
|
504
|
+
options_text = approval.get("options", "")
|
|
505
|
+
options = parse_options(f"{at}.options", options_text)
|
|
506
|
+
limits = approval.get("limits", {})
|
|
507
|
+
validate_limits(f"{at}.limits", limits)
|
|
508
|
+
if resolve is not None:
|
|
509
|
+
declared = resolve(obj.removeprefix(SERVICE_PREFIX))
|
|
510
|
+
if declared is None:
|
|
511
|
+
raise ValueError(f"{at}: {obj} is not a published service")
|
|
512
|
+
check_claim(at, obj, permissions, options, parse_exports(f"{obj} exports", declared))
|
|
513
|
+
entry: dict[str, Any] = {"object": obj, "permissions": list(permissions)}
|
|
514
|
+
if options:
|
|
515
|
+
entry["options"] = options_text
|
|
516
|
+
if limits:
|
|
517
|
+
entry["limits"] = dict(limits)
|
|
518
|
+
policy.append(entry)
|
|
519
|
+
approved = {entry["object"] for entry in policy}
|
|
520
|
+
if missing := sorted(obj for obj, request in requested.items() if request.get("required") and obj not in approved):
|
|
521
|
+
raise ValueError(f"approvals: the service requires {', '.join(missing)}")
|
|
522
|
+
return policy
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def api_key_lifespan(policy: Any) -> int:
|
|
526
|
+
"""Seconds a key leased with ``policy`` lives: its ``apikey`` entry's
|
|
527
|
+
``api_key_lifespan``, else :data:`DEFAULT_API_KEY_LIFESPAN`. A malformed
|
|
528
|
+
entry gets the default, never more."""
|
|
529
|
+
for entry in policy if isinstance(policy, list) else []:
|
|
530
|
+
if isinstance(entry, dict) and entry.get("object") == APIKEY_OBJECT:
|
|
531
|
+
try:
|
|
532
|
+
validate_limits("apikey.limits", entry.get("limits"), apikey=True)
|
|
533
|
+
except ValueError:
|
|
534
|
+
return DEFAULT_API_KEY_LIFESPAN
|
|
535
|
+
return int(entry["limits"][API_KEY_LIFESPAN])
|
|
536
|
+
return DEFAULT_API_KEY_LIFESPAN
|
|
@@ -26,6 +26,21 @@ from .base import (
|
|
|
26
26
|
TimeWindow,
|
|
27
27
|
UpstreamStatusEnum, # Backwards compatibility alias for OfferingStatusEnum
|
|
28
28
|
)
|
|
29
|
+
from .channel import (
|
|
30
|
+
RETIRED_CHANNEL_KEYS,
|
|
31
|
+
ChannelConfig,
|
|
32
|
+
ChannelParams,
|
|
33
|
+
HttpChannel,
|
|
34
|
+
HttpServer,
|
|
35
|
+
McpChannel,
|
|
36
|
+
McpServer,
|
|
37
|
+
RequestTransformer,
|
|
38
|
+
S3Channel,
|
|
39
|
+
S3Server,
|
|
40
|
+
SmtpChannel,
|
|
41
|
+
SmtpServer,
|
|
42
|
+
UpstreamAccessConfig,
|
|
43
|
+
)
|
|
29
44
|
from .documents import DocumentData
|
|
30
45
|
from .listing_data import ServiceListingData
|
|
31
46
|
from .listing_v1 import ListingV1
|
|
@@ -104,6 +119,20 @@ __all__ = [
|
|
|
104
119
|
"ListingV1",
|
|
105
120
|
"PromotionV1",
|
|
106
121
|
"ServiceGroupV1",
|
|
122
|
+
# Upstream access channels
|
|
123
|
+
"UpstreamAccessConfig",
|
|
124
|
+
"ChannelConfig",
|
|
125
|
+
"ChannelParams",
|
|
126
|
+
"HttpChannel",
|
|
127
|
+
"SmtpChannel",
|
|
128
|
+
"S3Channel",
|
|
129
|
+
"McpChannel",
|
|
130
|
+
"HttpServer",
|
|
131
|
+
"SmtpServer",
|
|
132
|
+
"S3Server",
|
|
133
|
+
"McpServer",
|
|
134
|
+
"RequestTransformer",
|
|
135
|
+
"RETIRED_CHANNEL_KEYS",
|
|
107
136
|
# Data models (for API/backend use)
|
|
108
137
|
"ProviderAccountRateLimit",
|
|
109
138
|
"ProviderData",
|