Skip to main content
Version: v1.2.x

Sharing Resources

PaletteAI shares resources down the scope hierarchy: a resource defined once at System or Tenant scope can be used by many Projects without being copied into each one. This enables platform teams to curate a common catalog of building blocks — for example, an approved Profile Bundle — while individual teams keep working in their own isolated Project namespaces.

Sharing is opt-in and controlled per resource by the sharedWith field.

Shareable Resources

These resource kinds can be placed at System or Tenant scope and shared with lower scopes through sharedWith:

A System-scoped Variable Set is an exception: it provides default variables to every Tenant and Project automatically and has no sharedWith field, because its access is not configurable per target.

Sharing with sharedWith

sharedWith is a match policy, not a plain list. For each candidate Tenant or Project, it answers one question: is this Tenant or Project allowed? Because the patterns support wildcards and exclusions, a single sharedWith can grant everyone, a specific set, or everyone except a few. At System scope, it can also target specific Projects within a Tenant.

sharedWith is a map of match-patterns. Each key is a pattern, and each key maps to a list of value-patterns:

System-scoped ProfileBundle shared with selected Tenants and Projects
apiVersion: spectrocloud.com/v1beta1
kind: ProfileBundle
metadata:
name: approved-llm-stack
namespace: pai-system
spec:
sharedWith:
# Tenant "research" can use this bundle in all of its Projects
research:
- '*'
# Tenant "platform" can use it only in Projects "team-a" and "team-b"
platform:
- team-a
- team-b
# ...

Selector Grammar

Every key and every value is a selector. A selector is one of three things, and they combine:

SelectorMeaningExample
nameAn exact literal name. Matches only that Tenant or Project.api
*A glob. * matches any run of characters, at any position.team-*, *-prod, *
!…A negation (deny). A leading ! turns the selector into an exclusion.!globex, !*-staging

Tenant and Project names are lowercase DNS-style names, so they can never contain * or !. There is no ambiguity and no escaping. The * is a plain "any characters" wildcard — it is not a shell glob or a regular expression.

Allow, Then Deny

To resolve a policy, the engine runs the map in two passes, in order:

  1. Allow — every positive selector (no !) contributes what it matches. This builds up the granted set.
  2. Deny — every negation (!…) subtracts what it matches from the granted set.

Deny always wins. A candidate is allowed only if some positive pattern granted it and no negation removed it. This is why {"*": ["*"], "!globex": ["*"]} means "everyone except Tenant globex" — the negation cannot be undone by the wildcard.

System Scope Semantics

At System scope, sharedWith reaches two tiers: the keys are Tenant names and each key's values are that Tenant's Project names.

System-scoped · two-tier
sharedWith:
acme:
- web # only Projects web and api of Tenant acme
- api
globex:
- '*' # all Projects of Tenant globex

A Tenant that appears as a matched key becomes visible, and its value list selects the Projects. This second tier is what System scope adds: it lets the instance-wide layer share to a specific Project inside a specific Tenant, which a flat per-Project list could never express.

The Tenant key is a gate. If a Tenant is not granted — no positive key matches it, or a ! negation removes it — then none of its Projects have access, no matter what the value selectors say. The value selectors are only consulted for Tenants that are already granted. So denying a Tenant denies every Project inside it in one stroke.

Tenant Scope Semantics

At Tenant scope, sharedWith reaches one tier: the keys are Project names and the values are ignored, because there is no level below a Project. The value list exists only so the shape matches the System case; use ["*"].

Tenant-scoped · share with Projects web and api
sharedWith:
web:
- '*'
api:
- '*'
# keys = Projects. web and api are in; everyone else is out.

Project Scope Semantics

Project-scoped resources are at the lowest level of the hierarchy, so sharedWith has no meaning. For Settings the validating webhook rejects a non-empty map at Project scope; for the other kinds there is simply no grant path.

nil vs {} vs {"*": ["*"]}

Sharing is always explicit, and "empty" has two different readings depending on the layer:

ValueAt the API layer (what you store)At resolution (what the engine computes)
nil / unsetDefaulted to {"*": ["*"]} by the mutating webhookDeny-all — a nil map grants nothing
{"*": ["*"]}The explicit "share with all" the webhook writes for youAllow-all
{} (empty)Explicit "share with none"Deny-all
warning

There is a real difference between omitting sharedWith and setting it to an empty map {}. The mutating webhook defaults an omitted field to share-with-all; an empty map {} shares with no one and persists on the wire (it is not dropped). Set {} explicitly to revoke all access.

A nil map resolves to deny-all, not allow-all: if an object ever reaches the engine before the defaulter has run, the engine fails closed and grants nothing. "nil means all" is the stored-data contract; "nil means none" is the fail-closed compute contract.

Defaults and Validation

Two admission webhooks run on every create and update, before anything is stored:

  • Defaulter — an unset sharedWith (nil) becomes {"*": ["*"]} (explicit share-all). A field that is already set — even to {} — is left alone; your value always wins.
  • Validator — every selector must be a syntactically valid lowercase DNS-style name, an optional leading !, and/or a * glob. For Settings there is one extra rule: a non-empty sharedWith on a Project-scoped Settings is rejected. ComputePools, AIWorkloads, and ProfileBundles get the syntax check only.

What Sharing Grants

The sharedWith policy language is identical for every kind, but what a grant buys differs:

  • Settings integrations — being shared means a lower scope inherits the integration's configuration (Hugging Face / NVIDIA credentials). This is a cascade with precedence (System → Tenant → Project) and a locked flag that can force the higher scope's value onto everyone it is shared with. The resolved result lands in read-only status.effectiveSettings. For the full model, refer to Settings and Integrations.
  • Compute Pools, AIWorkloads, Profile Bundles, Compute Configs, and Scaling Policies — being shared is an access grant: matched Tenants and Projects can view the resource, reference and use it, and clone it. For example, a Project can deploy an AIWorkload onto a Tenant- or System-scoped Compute Pool that is shared with it, or reference a shared Profile Bundle or Scaling Policy — without copying it into the Project. There is no locked, no precedence, and no merge — sharing simply creates or removes read access on every reconcile.

Read Visibility

Sharing a resource grants its consumers read access — the ability to view and clone it — not write access. The original resource stays owned and managed by the scope that defines it; consumers work with their own clones.

PaletteAI enforces this with aggregate read RBAC. When you share a System-scoped resource, the controller reconciles Roles and RoleBindings in the pai-system namespace that grant get and watch on that resource kind to the Tenants and Projects that the sharedWith policy resolves to. Removing an entry revokes the corresponding access on the next reconcile. Access is also enforced at reference time: an AIWorkload in a Project that references a Compute Pool not shared with that Project is rejected by admission, checked fail-closed with the same engine.

Because visibility is reconciled from sharedWith, editing that field is the single source of truth for who can see a System- or Tenant-scoped resource — there is no separate access list to keep in sync.

Worked Example: System-Scope Sharing

The following System-scoped Compute Pool combines every selector feature — a Tenant glob, a Project glob, a Project exclusion, and a whole-Tenant exclusion.

System-scoped ComputePool · sharedWith
sharedWith:
# Every Tenant whose name starts with "dev-" — all of their Projects.
'dev-*':
- '*'
# Tenant "acme" — only Projects starting with "team-", but never "team-secret".
acme:
- 'team-*'
- '!team-secret'
# Deny: remove Tenant "dev-sandbox" entirely, even though "dev-*" matched it.
'!dev-sandbox':
- '*'

Resolve it against this set of live Tenants and their Projects:

TenantProjects
dev-euweb, api
dev-sandboxscratch
acmeteam-a, team-secret, billing
globexstore

The engine runs the allow pass, then the deny pass (deny wins), and produces:

Tenant / ProjectAccessWhy
dev-eu / web, apidev-* grants the Tenant; the * value grants every Project.
dev-sandbox / scratch!dev-sandbox removes the whole Tenant — deny wins over the dev-* grant.
acme / team-aacme grants the Tenant; team-* grants the Project.
acme / team-secretteam-* matched it, but !team-secret removes it — a value-level deny.
acme / billingacme is granted, but no value selector matches billing, so it is not shared.
globex / storeNo key matches globex, so the Tenant — and every Project in it — is denied.

Two patterns to take away: dev-sandbox shows a whole-Tenant deny (!dev-sandbox) overriding a wildcard grant, and globex shows that an unmatched Tenant is denied along with all of its Projects — the Tenant gate again.

For an overview of how Tenants and Projects relate, refer to Multi-Tenancy. For the role definitions that govern write access within a Project, refer to Roles and Permissions.