PostHog / PostHog/posthog

fix(skills): key per-skill access control on the logical skill, not the version row

Open
#97,488 0 comments 0 reactions 1 assignee View on GitHub

@JakeRuth is already working on this.

Since Sep 9, 2026.

self-driving
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 none plus an explicit viewer grant on one skill: after the next edit, the viewer loses access. Fail-closed but broken.
  • Resource-level viewer plus an explicit none restriction 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.py publish_skill_version: flips is_latest on 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), and id__in in filter_queryset_by_access_level.
  • products/access_control/backend/presentation/access_control.py: the serializer validates that resource_id matches the_object.pk, and the viewset reads grants back by obj.id.

Proposed fix

Key skill access control on the logical skill, not the version row.

  1. Add a stable identity column on LLMSkill shared by every version of one skill. Backfill existing rows: for each (team, name) pick the id of the lowest version. Follow /django-migrations for the multi-phase add and backfill.
  2. Give the access-control facade one hook for "which value identifies this object". Default stays obj.pk. LLMSkill returns the stable id. Use it in the point-check paths and in filter_queryset_by_access_level, which needs the field name for the __in filter as well as the value.
  3. 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.
  4. 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 none still applies after the skill is edited, to the new version and to old versions read by ?version=.
  • A none restriction on a skill under resource-level viewer still blocks after the skill is edited, on every version.
  • List filtering under resource-level none shows the granted skill after an edit.
  • The access-control API accepts the stable id as resource_id for llm_skill and returns the grant when asked about any version.
  • Existing tests in products/skills/backend/api/test/ for the name/<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 LLMPrompt in model_to_resource.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.