Permission Model
LinkOStar implements a hybrid permission model where each supplier picks the SSOT. The default is LinkOStar-SSOT; when a supplier registers a permission endpoint, that surface is delegated to the supplier (V13). Each integrator decides whether their supplier registers a permission endpoint.
SSOT split
| Condition | Permission SSOT | Scope |
|---|---|---|
supplier_oauth_config.permission_query_url null | LinkOStar (default) | Native authorisation by tenant_uuid + UserPersona |
permission_query_url set | Supplier | The supplier's view of which user can do what on its hubs/devices |
Within the same LinkOStar instance, supplier A can run on LinkOStar-SSOT while supplier B runs on supplier-SSOT.
Permission enum (V13)
| Permission | Meaning | Implies |
|---|---|---|
owner | Everything + delete | operator · viewer |
operator | Read + write commands; no delete | viewer |
viewer | Read-only | — |
Unknown values are dropped silently — extending the enum requires a LinkOStar code change.
1. Supplier-SSOT lookup (D-Sync-1/2/3)
- User hits
/app/hubsor/app/devices. - LinkOStar gathers candidates, groups them by supplier_uuid.
- For supplier-SSOT groups, check
SupplierPermissionCache:- cache hit within TTL → filter directly
- cache miss / stale → call
permission_query_urlwith the user's supplier access_token, fill the cache, then filter
- For LinkOStar-SSOT groups, filter by native auth (tenant_uuid + persona).
- Merge both groups into the response.
2. permission_query_url response shape
GET https://supplier.example.com/api/m2m/users/{sub}/permissions
Authorization: Bearer <supplier-issued user access_token>
Response 200:
[
{"resource_type": "HUB", "resource_uuid": "550e8400-...", "permissions": ["owner"]},
{"resource_type": "DEVICE", "resource_uuid": "660e8400-...", "permissions": ["operator","viewer"]}
]A {sub}placeholder in the URL is substituted with the user's supplier sub. Under the n:1 model (one LinkOStar user linked to multiple accounts of the same supplier), the service queries each sub and unions the responses.
3. Cache + invalidation
- TTL:
linkostar.supplier-permission.cache-ttl(5 min default). - Webhook invalidate: a supplier change posts to
/webhook/supplier-permission/{supplierUuid}with the HMAC signature. Every active SupplierLink with the body'suser_subgets that user's cache dropped. - Implicit on revoke: when a user revokes a supplier-link, every cache row for that (user, supplier) pair is deleted.
4. Users without a supplier link (D-Sync-6)
If a supplier requires OAuth (it has a supplier_oauth_config) and the user hasn't linked yet, that supplier's hubs are hidden from /app/hubs. The PUBLISHED bundle is still visible via /app/bundle-catalog with requiresSupplierLink: true and currentUserLinked: false, so the app can show the link entry point.
To avoid the "hidden → can't link → still hidden" deadlock, BLE proximity and similar near-field signals should also trigger the supplier-link flow from the mobile app.
5. Permission hierarchy at a glance
required \ have none viewer operator owner
viewer ❌ ✅ ✅ ✅
operator ❌ ❌ ✅ ✅
owner ❌ ❌ ❌ ✅Mutating endpoints like AppDeviceService.sendCommand require at least OPERATOR when the supplier is the SSOT.
Operational notes
- A short TTL (5 min or less) combined with webhook invalidation is the safest mix. Per-request lookups against the supplier are expensive — keep the cache.
- If
permission_query_url5xx's, LinkOStar treats the response as empty and the user sees "no permission". Supplier endpoint availability directly impacts user UX. - In the n:1 model permissions are unioned — if any of the user's accounts at the same supplier has access, it is granted. Stricter policies are a separate sprint.