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.
@@ -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