odcp-contracts 0.1.3__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.
- odcp_contracts-0.1.3/CP_README.md +411 -0
- odcp_contracts-0.1.3/PKG-INFO +420 -0
- odcp_contracts-0.1.3/odcp_contracts/__init__.py +36 -0
- odcp_contracts-0.1.3/odcp_contracts/licensing.py +101 -0
- odcp_contracts-0.1.3/odcp_contracts/whoami.py +61 -0
- odcp_contracts-0.1.3/odcp_contracts.egg-info/PKG-INFO +420 -0
- odcp_contracts-0.1.3/odcp_contracts.egg-info/SOURCES.txt +10 -0
- odcp_contracts-0.1.3/odcp_contracts.egg-info/dependency_links.txt +1 -0
- odcp_contracts-0.1.3/odcp_contracts.egg-info/requires.txt +2 -0
- odcp_contracts-0.1.3/odcp_contracts.egg-info/top_level.txt +1 -0
- odcp_contracts-0.1.3/pyproject.toml +39 -0
- odcp_contracts-0.1.3/setup.cfg +4 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Owndivision Control Plane – How To & Concepts
|
|
2
|
+
|
|
3
|
+
This document explains **what the Control Plane (CP)** currently does, what the main
|
|
4
|
+
**domain objects** are (organization, workspace, user, subscription, license, etc.),
|
|
5
|
+
and **how it interacts with the Data Plane (DP)**.
|
|
6
|
+
|
|
7
|
+
It is written for:
|
|
8
|
+
- Future you
|
|
9
|
+
- Any developer onboarding to the project
|
|
10
|
+
- Anyone trying to understand how CP ↔ DP ↔ Auth0 fit together
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Big Picture
|
|
15
|
+
|
|
16
|
+
### Control Plane (CP)
|
|
17
|
+
|
|
18
|
+
The CP is the **“brain” of the platform**:
|
|
19
|
+
|
|
20
|
+
- Knows your **customers** (organizations)
|
|
21
|
+
- Knows **workspaces** inside those organizations
|
|
22
|
+
- Manages **users, identities & roles**
|
|
23
|
+
- Manages **plans, subscriptions & deployments**
|
|
24
|
+
- Issues **license tokens** (signed JWTs) that the DP uses to:
|
|
25
|
+
- Validate entitlement
|
|
26
|
+
- Know which **deployment_id** it belongs to
|
|
27
|
+
- Load license features/seat caps via a `LicenseContext`
|
|
28
|
+
|
|
29
|
+
### Data Plane (DP)
|
|
30
|
+
|
|
31
|
+
The DP is the **analytics app instance**:
|
|
32
|
+
|
|
33
|
+
- Exposes `/api/v1/...` for chats, charts, dashboards, alerts
|
|
34
|
+
- Talks to:
|
|
35
|
+
- **MFI DB** for customer data
|
|
36
|
+
- **Internal DB** for app metadata
|
|
37
|
+
- Validates every request via:
|
|
38
|
+
- A **license token** (X-License-Token or `LICENSE_TOKEN` in `.env`)
|
|
39
|
+
- An **Auth0 access token** (end-user identity)
|
|
40
|
+
- Uses CP’s `/users/whoami` to resolve:
|
|
41
|
+
- Which **CP user** this Auth0 identity belongs to
|
|
42
|
+
- Which **workspaces** and roles they have
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Core Domain Objects
|
|
47
|
+
|
|
48
|
+
Below is what we currently have in the CP models & schemas.
|
|
49
|
+
|
|
50
|
+
### 2.1 Organization
|
|
51
|
+
|
|
52
|
+
Represents a **customer organization** (tenant at business level).
|
|
53
|
+
|
|
54
|
+
Key fields (simplified):
|
|
55
|
+
|
|
56
|
+
- `id: UUID`
|
|
57
|
+
- `name: str`
|
|
58
|
+
- `slug: str` (URL-friendly identifier)
|
|
59
|
+
- `status: OrganizationStatus` (e.g. `active`, `suspended`)
|
|
60
|
+
- `external_id: Optional[str]` – e.g. CRM/customer id
|
|
61
|
+
|
|
62
|
+
Relationships:
|
|
63
|
+
|
|
64
|
+
- Has many **Workspaces**
|
|
65
|
+
- Has many **Subscriptions**
|
|
66
|
+
- Has many **Deployments** (DP instances)
|
|
67
|
+
- Indirectly linked to **Licenses** via deployments and subscriptions
|
|
68
|
+
|
|
69
|
+
Typical lifecycle:
|
|
70
|
+
|
|
71
|
+
1. CP admin creates an organization.
|
|
72
|
+
2. Assigns a subscription + plan.
|
|
73
|
+
3. Creates one or more deployments (DP instances) and workspaces.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### 2.2 Workspace
|
|
78
|
+
|
|
79
|
+
Represents a **logical environment inside an organization**.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
`Acme Corp` (org) can have workspaces:
|
|
83
|
+
|
|
84
|
+
- `acme-prod`
|
|
85
|
+
- `acme-demo`
|
|
86
|
+
- `acme-sandbox`
|
|
87
|
+
|
|
88
|
+
Fields (from `WorkspaceCreateRequest` / `WorkspaceReadResponse`):
|
|
89
|
+
|
|
90
|
+
- `id: UUID`
|
|
91
|
+
- `organization_id: UUID`
|
|
92
|
+
- `name: str`
|
|
93
|
+
- `slug: str`
|
|
94
|
+
- `type: WorkspaceType` (e.g. `PROD`, `DEMO`, `TRIAL`, `SANDBOX`)
|
|
95
|
+
- `is_default: bool`
|
|
96
|
+
|
|
97
|
+
Usage:
|
|
98
|
+
|
|
99
|
+
- CP uses workspaces to group users & roles.
|
|
100
|
+
- DP currently **does not yet** segment data by workspace; it scopes by `deployment_id` + `owner_sub`. Workspaces are already exposed in `/users/whoami` and FE, so they’re ready for future per-workspace scoping.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### 2.3 User
|
|
105
|
+
|
|
106
|
+
Logical **CP user**, independent of the actual IdP.
|
|
107
|
+
|
|
108
|
+
Fields (simplified):
|
|
109
|
+
|
|
110
|
+
- `id: UUID`
|
|
111
|
+
- `email: str`
|
|
112
|
+
- `display_name: Optional[str]`
|
|
113
|
+
- `is_active: bool`
|
|
114
|
+
|
|
115
|
+
Relationships:
|
|
116
|
+
|
|
117
|
+
- Has many **AuthAccounts** (concrete identities at Auth0/authentik/etc.)
|
|
118
|
+
- Has many **WorkspaceMemberships** (with roles per workspace)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### 2.4 AuthAccount
|
|
123
|
+
|
|
124
|
+
Connects a `User` to a **concrete identity provider account**.
|
|
125
|
+
|
|
126
|
+
Fields:
|
|
127
|
+
|
|
128
|
+
- `id: UUID`
|
|
129
|
+
- `user_id: UUID` → `cp_users.id`
|
|
130
|
+
- `provider: AuthProvider` enum (e.g. `AUTH0`, `AUTHENTIK`, `LOCAL`)
|
|
131
|
+
- `subject: str` – stable external ID (`sub` claim / NameID)
|
|
132
|
+
- `idp_tenant: Optional[str]` – external IdP tenant/org id
|
|
133
|
+
|
|
134
|
+
Constraint:
|
|
135
|
+
|
|
136
|
+
- `(provider, subject)` is globally unique
|
|
137
|
+
→ ensures we can look up **one** CP user for a given IdP identity.
|
|
138
|
+
|
|
139
|
+
This is what `/api/v1/users/whoami` uses when you pass `provider=auth0&subject=<sub>`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 2.5 Roles
|
|
144
|
+
|
|
145
|
+
A `Role` describes a **set of permissions** in a workspace (current implementation is mostly structural; enforcement is still light).
|
|
146
|
+
|
|
147
|
+
Fields:
|
|
148
|
+
|
|
149
|
+
- `id: UUID`
|
|
150
|
+
- `code: str` – machine-readable (e.g. `WORKSPACE_OWNER`, `WORKSPACE_ADMIN`, `WORKSPACE_MEMBER`)
|
|
151
|
+
- `name: str` – human-friendly
|
|
152
|
+
- `description: Optional[str]`
|
|
153
|
+
- `is_system: bool` – built-in vs custom
|
|
154
|
+
|
|
155
|
+
Roles are attached to memberships (below).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 2.6 WorkspaceMembership
|
|
160
|
+
|
|
161
|
+
Connects a **User** with a **Workspace** and a **Role**.
|
|
162
|
+
|
|
163
|
+
Fields (from `WorkspaceMembershipReadResponse`):
|
|
164
|
+
|
|
165
|
+
- `id: UUID`
|
|
166
|
+
- `workspace_id: UUID`
|
|
167
|
+
- `user_id: UUID`
|
|
168
|
+
- `role_id: UUID`
|
|
169
|
+
- `status: MembershipStatus` – e.g. `ACTIVE`, `INVITED`, `SUSPENDED`
|
|
170
|
+
- `created_at`, `updated_at`
|
|
171
|
+
- Embedded:
|
|
172
|
+
- `user: UserReadResponse`
|
|
173
|
+
- `role: RoleReadResponse`
|
|
174
|
+
|
|
175
|
+
Semantics:
|
|
176
|
+
|
|
177
|
+
- This is where **“who can access which workspace and with what level”** lives.
|
|
178
|
+
- A user can have multiple memberships across orgs & workspaces.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### 2.7 Plans & Subscriptions
|
|
183
|
+
|
|
184
|
+
#### Plan
|
|
185
|
+
|
|
186
|
+
Represents a **billing / feature tier**.
|
|
187
|
+
|
|
188
|
+
From `PlanReadResponse`:
|
|
189
|
+
|
|
190
|
+
- `code: PlanCode` – stable machine code (`free`, `pro`, `enterprise`, …)
|
|
191
|
+
- `name: str`
|
|
192
|
+
- `description: Optional[str]`
|
|
193
|
+
- `is_public: bool`
|
|
194
|
+
- `default_features: Dict[str, Any]` – raw JSON config for limits/features
|
|
195
|
+
|
|
196
|
+
Examples of features (already supported in schema):
|
|
197
|
+
|
|
198
|
+
- `alerts_enabled: true/false`
|
|
199
|
+
- `max_workspaces: int`
|
|
200
|
+
- `max_dashboards_per_workspace: int`
|
|
201
|
+
- `max_users_per_organization: int`
|
|
202
|
+
- `exports_enabled: bool`, etc.
|
|
203
|
+
|
|
204
|
+
#### Subscription
|
|
205
|
+
|
|
206
|
+
Represents **an org’s subscription to a plan**.
|
|
207
|
+
|
|
208
|
+
From `SubscriptionReadResponse`:
|
|
209
|
+
|
|
210
|
+
- `id: UUID`
|
|
211
|
+
- `organization_id: UUID`
|
|
212
|
+
- `plan_code: PlanCode`
|
|
213
|
+
- `status: SubscriptionStatus` (e.g. `active`, `trialing`, `canceled`)
|
|
214
|
+
- `seat_cap: int`
|
|
215
|
+
- `starts_at`, `ends_at`, `trial_ends_at`
|
|
216
|
+
- `external_id: Optional[str]` – ID in Stripe/Paddle/etc.
|
|
217
|
+
|
|
218
|
+
Lifecycle:
|
|
219
|
+
|
|
220
|
+
1. Org is created.
|
|
221
|
+
2. Subscription with `plan_code` is attached.
|
|
222
|
+
3. Licenses for deployments draw their default `seat_cap` / features from the subscription + plan.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### 2.8 Deployment
|
|
227
|
+
|
|
228
|
+
A **logical DP instance** (cluster/on-prem install/demo).
|
|
229
|
+
|
|
230
|
+
From `DeploymentReadResponse`:
|
|
231
|
+
|
|
232
|
+
- `id: UUID`
|
|
233
|
+
- `organization_id: UUID`
|
|
234
|
+
- `name: str`
|
|
235
|
+
- `type: DeploymentType` (`CLOUD`, `ONPREM`, `DEMO`, etc.)
|
|
236
|
+
- `region: Optional[str]`
|
|
237
|
+
- `api_base_url: Optional[str]`
|
|
238
|
+
- `status: DeploymentStatus`
|
|
239
|
+
|
|
240
|
+
A single organization can have multiple deployments, e.g.:
|
|
241
|
+
|
|
242
|
+
- `Acme Cloud EU-West`
|
|
243
|
+
- `Acme On-Prem`
|
|
244
|
+
- `Acme Demo`
|
|
245
|
+
|
|
246
|
+
DPs know which deployment they are via the **license token**.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### 2.9 License
|
|
251
|
+
|
|
252
|
+
Represents an **entitlement for a specific deployment**.
|
|
253
|
+
|
|
254
|
+
From `LicenseReadResponse`:
|
|
255
|
+
|
|
256
|
+
- `id: UUID`
|
|
257
|
+
- `subscription_id: UUID`
|
|
258
|
+
- `deployment_id: UUID`
|
|
259
|
+
- `seat_cap: int`
|
|
260
|
+
- `features: Dict[str, Any]` – effective feature set
|
|
261
|
+
- `valid_from`, `valid_until`
|
|
262
|
+
- `key_id: Optional[str]` – which key was used to sign exports
|
|
263
|
+
- `bundle_hash: Optional[str]` – hash of last exported bundle
|
|
264
|
+
- `created_at`, `updated_at`
|
|
265
|
+
|
|
266
|
+
The key thing: **this is what gets turned into a License Token (JWT)** and installed in the DP.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### 2.10 License Bundle (License Token)
|
|
271
|
+
|
|
272
|
+
From `LicenseBundleResponse`:
|
|
273
|
+
|
|
274
|
+
- `license_id: UUID`
|
|
275
|
+
- `token: str` – signed JWT (compact JWS)
|
|
276
|
+
- `key_id: str` – `kid` of signing key (must match public key in JWKS)
|
|
277
|
+
- `algorithm: str` – e.g. `RS256`
|
|
278
|
+
- `issued_at`, `expires_at`
|
|
279
|
+
- `payload: Dict[str, Any]` – decoded claims (for debugging in dev)
|
|
280
|
+
|
|
281
|
+
This token is:
|
|
282
|
+
|
|
283
|
+
- Signed by the CP’s **private key** (`CP_SIGNING_PRIVATE_KEY_PATH`)
|
|
284
|
+
- Verifiable via the CP’s **JWKS endpoint**
|
|
285
|
+
(`/api/v1/.well-known/jwks.json` → used by DP)
|
|
286
|
+
- Installed into the DP:
|
|
287
|
+
- Either via `LICENSE_TOKEN` env var (local/dev)
|
|
288
|
+
- Or via `X-License-Token` header at the gateway in production
|
|
289
|
+
|
|
290
|
+
Inside the token:
|
|
291
|
+
|
|
292
|
+
- High-level standard claims (`iss`, `aud`, `sub`, `exp`, etc.)
|
|
293
|
+
- A `license` object with:
|
|
294
|
+
- Organization (id, name, slug, status)
|
|
295
|
+
- Deployment (id, name, type, region, status)
|
|
296
|
+
- Subscription (id, status, seat_cap, dates)
|
|
297
|
+
- Plan (code, name, default_features)
|
|
298
|
+
- Features / limits for this license
|
|
299
|
+
|
|
300
|
+
The DP reads this and builds a `LicenseContext`.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 3. “Who Am I” Flows
|
|
305
|
+
|
|
306
|
+
### 3.1 On the Control Plane (`/api/v1/users/whoami`)
|
|
307
|
+
|
|
308
|
+
Endpoint:
|
|
309
|
+
`GET /api/v1/users/whoami?provider=<provider>&subject=<subject>`
|
|
310
|
+
|
|
311
|
+
Inputs:
|
|
312
|
+
|
|
313
|
+
- `provider` – e.g. `"auth0"` (`AuthProvider.AUTH0`)
|
|
314
|
+
- `subject` – IdP’s stable id (e.g. Auth0 `sub` claim)
|
|
315
|
+
|
|
316
|
+
Behavior:
|
|
317
|
+
|
|
318
|
+
1. Find `AuthAccount` via `(provider, subject)`.
|
|
319
|
+
2. Load the `User`.
|
|
320
|
+
3. Load all `WorkspaceMembership`s for that user (with embedded role).
|
|
321
|
+
4. Return a `WhoAmIResponse`:
|
|
322
|
+
|
|
323
|
+
```jsonc
|
|
324
|
+
{
|
|
325
|
+
"user": {
|
|
326
|
+
"id": "...",
|
|
327
|
+
"email": "...",
|
|
328
|
+
"display_name": "...",
|
|
329
|
+
"is_active": true
|
|
330
|
+
},
|
|
331
|
+
"memberships": [
|
|
332
|
+
{
|
|
333
|
+
"workspace_id": "...",
|
|
334
|
+
"workspace_name": "...",
|
|
335
|
+
"workspace_slug": "...",
|
|
336
|
+
"organization_id": "...",
|
|
337
|
+
"status": "ACTIVE",
|
|
338
|
+
"role": {
|
|
339
|
+
"id": "...",
|
|
340
|
+
"code": "WORKSPACE_OWNER",
|
|
341
|
+
"name": "Workspace Owner",
|
|
342
|
+
"description": "...",
|
|
343
|
+
"is_system": true
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## odcp-contracts: Versioning & Compatibility
|
|
351
|
+
|
|
352
|
+
The `odcp-contracts` package defines the **public contract** between the
|
|
353
|
+
Control Plane (CP) and Data Plane (DP).
|
|
354
|
+
|
|
355
|
+
### Public API
|
|
356
|
+
|
|
357
|
+
The only supported import path is:
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from odcp_contracts import DeploymentLicenseClaims, LicenseBodyClaims, WhoAmIResponse
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 2️⃣ Versioning & release ritual (how you’ll use it)
|
|
364
|
+
|
|
365
|
+
Whenever you change **public contracts**:
|
|
366
|
+
|
|
367
|
+
1. Bump version in `pyproject.toml`:
|
|
368
|
+
|
|
369
|
+
```toml
|
|
370
|
+
[project]
|
|
371
|
+
version = "0.2.0"
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
2. Optionally update docs:
|
|
375
|
+
- `CP_README.md`
|
|
376
|
+
- `CHANGELOG.md`
|
|
377
|
+
|
|
378
|
+
3. Commit and tag the release:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
git add .
|
|
382
|
+
git commit -m "Bump odcp-contracts to v0.2.0"
|
|
383
|
+
|
|
384
|
+
git tag -a contracts-v0.2.0 -m "odcp-contracts v0.2.0"
|
|
385
|
+
git push origin main
|
|
386
|
+
git push origin contracts-v0.2.0
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
4. The publish workflow runs:
|
|
390
|
+
- `pytest`
|
|
391
|
+
- `python -m build`
|
|
392
|
+
- `twine upload dist/*`
|
|
393
|
+
|
|
394
|
+
5. In the DP repo, depend on the published package:
|
|
395
|
+
|
|
396
|
+
```toml
|
|
397
|
+
# pyproject.toml of DP
|
|
398
|
+
[project]
|
|
399
|
+
dependencies = [
|
|
400
|
+
"odcp-contracts~=0.2.0",
|
|
401
|
+
# ...
|
|
402
|
+
]
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Notes
|
|
406
|
+
|
|
407
|
+
- `odcp-contracts~=0.2.0` allows patch upgrades within `0.2.*` (recommended). Use `==0.2.0` if you want fully pinned builds.
|
|
408
|
+
- Stick to semantic versioning:
|
|
409
|
+
- **MAJOR** for breaking contract changes
|
|
410
|
+
- **MINOR** for backward-compatible additions
|
|
411
|
+
- **PATCH** for backward-compatible fixes
|