cast must be team-aware: assert the token's team before mutating (fail-closed) #9
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#9
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The fact
Coolify API tokens are team-scoped, and nothing below the team scopes them.
Verified against
coollabsio/coolifyv4.1.2:User::createTokenoverrides Sanctum's and stamps the token with the session's team:getResourceByUuidwalksresource → environment → project → team_idand returnsnullwhen itdoesn't match.
Environmenthas noteam_idof its own (it belongs to a project). No API path scopes byenvironment. Coolify environments are an organizational construct, not an auth boundary.
(
Team hasMany ...). A server belongs to exactly one team — no pivot, and unlikeGithubAppthere is no
is_system_wideescape hatch. Upstream confirms teams cannot share a server and defersit to v5: coollabsio/coolify#1820, coollabsio/coolify#3235.
Why this is a correctness bug, not a hardening nice-to-have
A wrong-team token does not error.
getResourceByUuidjust returnsnull.To
cast apply, "null" is indistinguishable from "this resource does not exist yet" — which is aninvitation to create it. So an apply run with a token minted under the wrong team will not fail
loudly; it will happily provision a duplicate set of resources into the wrong team, against
whatever server that team happens to own. That is the worst shape of failure: silent, mutating, and
discovered late.
Today this is masked because there is exactly one team and one token. It stops being masked the moment
staging joins the control plane (incubator prod-migration Task 13).
Asks
environments.yamlcarries the expected team (name and/or id) perenvironment.
belongs to and compare it against the manifest's expected team. Abort on mismatch, loudly. This is
the whole point of the issue: turn a silent mis-target into a refusal.
cast server addregisters under the token's team, so it inherits the same check.GithubApp.is_system_wideas the supported way to let one App serve every team —it may make per-team App duplication unnecessary (and may make #6 moot; see below).
Related
github_appskeyed by full org/repo slug) may be moot: under the consuming project'sthree-tier promotion, staging now clones upstream — the same repo and org as prod — so a single
App can plausibly serve both. Worth confirming before building the full-slug keying.
run at a time). That is a correctness guarantee — it stops two concurrent applies clobbering each
other's Coolify state. Worth having on its own merits; separate issue.