fix(skills): key per-skill access control on the logical skill, not the version row
@JakeRuth is already working on this.
Since Sep 9, 2026.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Problem
A per-skill access-control grant or restriction is keyed on the LLMSkill row id. Every publish creates a new row and marks the old one as not latest, so the grant points at a row nobody reads anymore.
Two failure modes, the second is the serious one:
- Resource-level
noneplus an explicit viewer grant on one skill: after the next edit, the viewer loses access. Fail-closed but broken. - Resource-level
viewerplus an explicitnonerestriction on one skill: after the next edit, the restriction is orphaned and the blocked member can read the new version. Fail-open.
Nobody is affected today. The skill scene only creates resource-level rules, so per-skill rows can only come from the access-control API directly, and prod-US has none. This has to land before per-skill grants are exposed in the skill scene.
Flagged in #88376 and #97083, and by Greptile on https://github.com/PostHog/posthog/pull/97083#discussion_r3964080386.
Where the row id is baked in
products/skills/backend/api/skill_services.pypublish_skill_version: flipsis_lateston the current row and creates a new one. Nothing carries access control forward.products/access_control/backend/facade/user_access_control.py:str(obj.id)in five point-check paths (preload,access_level_for_object, and the object-row readers around lines 707, 807, 902, 1626, 1731), andid__ininfilter_queryset_by_access_level.products/access_control/backend/presentation/access_control.py: the serializer validates thatresource_idmatchesthe_object.pk, and the viewset reads grants back byobj.id.
Proposed fix
Key skill access control on the logical skill, not the version row.
- Add a stable identity column on
LLMSkillshared by every version of one skill. Backfill existing rows: for each(team, name)pick the id of the lowest version. Follow/django-migrationsfor the multi-phase add and backfill. - Give the access-control facade one hook for "which value identifies this object". Default stays
obj.pk.LLMSkillreturns the stable id. Use it in the point-check paths and infilter_queryset_by_access_level, which needs the field name for the__infilter as well as the value. - Use the same hook in the access-control API serializer validation and the read-back path, so a client posting a grant for a skill uses the stable id and sees it on every version.
- Skill serializers expose the stable id so the frontend has something to pass when the skill scene starts creating per-skill grants.
Copying grants onto the new row at publish time is not enough. Old versions stay readable by name and version, and a restriction added after a publish never reaches them.
LLMPrompt uses the same versioning pattern and would get the same fix for free through the hook once it is mapped in model_to_resource. Out of scope here, but do not design the hook so it only works for skills.
Acceptance
- A viewer grant on a skill under resource-level
nonestill applies after the skill is edited, to the new version and to old versions read by?version=. - A
nonerestriction on a skill under resource-levelviewerstill blocks after the skill is edited, on every version. - List filtering under resource-level
noneshows the granted skill after an edit. - The access-control API accepts the stable id as
resource_idforllm_skilland returns the grant when asked about any version. - Existing tests in
products/skills/backend/api/test/for thename/<slug>actions from #97083 still pass. - Migration is non-blocking on
llm_analytics_llmskill.
Non-goals
- Exposing per-skill grants in the skill scene UI.
- Mapping
LLMPromptinmodel_to_resource.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.