Skip to content

refactor: unify URL rule and permission config shape (consistent target, value, match-type) #221

@coopernetes

Description

@coopernetes

Problem

URL rules (`AccessRule`) and user permissions (`RepoPermission`) operate on nearly identical matching logic — both match a repository path against a pattern using LITERAL, GLOB, or REGEX semantics — but their config shapes and internal models are inconsistent:

URL rules Permissions
Target field `slugs`/`owners`/`names` (plural lists) `path` (always full slug)
Match type Implicit `regex:` string prefix; glob otherwise Explicit `path-type: LITERAL|GLOB|REGEX` enum
Config shape One rule entry can expand to N `AccessRule` rows One entry = one `RepoPermission` row
Provider scope `providers: [list]` — multi-provider, empty = all `provider: name` — single provider
Operations `FETCH`, `PUSH`, `BOTH` enum `PUSH`, `REVIEW`, `PUSH_AND_REVIEW`, `SELF_CERTIFY` enum

This divergence makes the mental model harder to reason about as the rule system grows (time-based permissions, JIT grants, license-based rules).

Provider field mismatch

The `providers: [list]` shape in URL rules exists to scope a single rule to multiple providers at once. In practice this creates the same silent-expansion problem as plural slugs/owners/names — one config entry creates N `AccessRule` rows — and it diverges from permissions.

The dashboard UI already reflects the simpler model: the access rule form exposes either a single provider selection or "all providers" (NULL). There is no multi-provider picker. Config and UI are out of sync.

Having a list also creates a footgun: a `providers: [github, gitlab]` deny rule silently creates two rows. Removing one provider from the list later leaves the other row active with no indication of the original intent.

Proposed unified shape

Align both systems on a common config structure:

rules:
  allow:
    - operations: [PUSH, FETCH]
      provider: internal-github     # single; omit = all providers
      match:
        target: slug                # slug | owner | name
        value: "/myorg/**"
        type: glob                  # literal | glob | regex

permissions:
  - username: alice
    provider: internal-github
    match:
      target: slug
      value: "/myorg/**"
      type: glob
    operations: PUSH_AND_REVIEW

Scope

  • Rename `AccessRule` fields to use a unified `MatchType` enum (LITERAL, GLOB, REGEX) — removes `regex:` prefix convention
  • `RepoPermission` picks up `target` (defaulting to `slug` to preserve existing behaviour)
  • Both sections use a single optional `provider` field (NULL / omitted = all providers)
  • DB migrations: add `match_type` column to `access_rules`; data migration strips `regex:` prefix from existing rows
  • Config YAML: both sections use `match.target` + `match.value` + `match.type`
  • Hot-reload and seedFromConfig updated accordingly
  • Dashboard API and UI already expose individual rules — no structural UI change needed

Enum consistency

Decide on a single casing convention (`UPPER_CASE` vs `lower-case`) for all config enums across both sections.

Notes

  • Pre-1.0 — breaking config change is acceptable; provide a migration guide in release notes
  • Lays the foundation for future rule dimensions (time-based, license-based, JIT) without further model divergence

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions