diff --git a/docker/git-proxy-docker-default.yml b/docker/git-proxy-docker-default.yml index 265ae3ff..7608618c 100644 --- a/docker/git-proxy-docker-default.yml +++ b/docker/git-proxy-docker-default.yml @@ -48,32 +48,44 @@ permissions: # test-user: LITERAL — only /test-owner/test-repo - username: test-user provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: PUSH # user2: GLOB — any repo under otherorg - username: user2 provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: PUSH # user3: REGEX — test-owner repos matching test-repo.* - username: user3 provider: gitea - path: /test-owner/test-repo.* - path-type: REGEX + match: + target: SLUG + value: '/test-owner/test-repo.*' + type: REGEX operations: PUSH # reviewer: APPROVE on all the same paths - username: reviewer provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: REVIEW - username: reviewer provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: REVIEW # LDAP testuser (maps to gitea/test-user): same literal grant as test-user above. @@ -82,18 +94,26 @@ permissions: # so that push-permission-literal.sh still passes the deny-on-other-repo case. - username: testuser provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: PUSH # admin APPROVE grants for LDAP setup (admin approves via dashboard). - username: admin provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: REVIEW - username: admin provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: REVIEW providers: @@ -149,65 +169,70 @@ rules: allow: - enabled: true order: 110 - operations: - - FETCH - - PUSH - providers: - - gitea - slugs: - - /test-owner/test-repo - - /test-owner/test-repo-2 + operations: BOTH + provider: gitea + match: + target: SLUG + value: '/test-owner/test-repo(-2)?' + type: REGEX + - enabled: true order: 111 - operations: - - FETCH - - PUSH - providers: - - gitea - owners: - - otherorg + operations: BOTH + provider: gitea + match: + target: OWNER + value: otherorg + type: GLOB + - enabled: true order: 120 - operations: - - FETCH - providers: - - github - owners: - - finos + operations: FETCH + provider: github + match: + target: OWNER + value: finos + type: GLOB deny: - # Deny a specific repo inside the otherwise-allowed otherorg/* owner glob. - # Demonstrates that deny wins: user2 has GLOB permission on /otherorg/* and + # Deny a specific repo inside the otherwise-allowed otherorg owner. + # Demonstrates that deny wins: user2 has permission on otherorg and # the allow rule covers all of otherorg, but this repo is explicitly off-limits. - enabled: true order: 100 - operations: - - FETCH - - PUSH - providers: - - gitea - slugs: - - /otherorg/other-secret - - # Deny repos whose name ends in -readonly or -archived — these are archival - # copies that should not receive new commits through the proxy. + operations: BOTH + provider: gitea + match: + target: SLUG + value: /otherorg/other-secret + type: LITERAL + + # Deny repos whose name ends in -readonly or -archived — archival copies + # that should not receive new commits through the proxy. + - enabled: true + order: 101 + operations: PUSH + provider: gitea + match: + target: NAME + value: "*-readonly" + type: GLOB + - enabled: true order: 101 - operations: - - PUSH - providers: - - gitea - names: - - "*-readonly" - - "*-archived" - - # Regex deny: block any repo whose name contains the word "secret" as a - # distinct segment (e.g. secret-store, my-secret-keys, not "secretariat"). + operations: PUSH + provider: gitea + match: + target: NAME + value: "*-archived" + type: GLOB + + # Regex deny: block any repo whose name contains "secret" as a distinct segment. - enabled: true order: 102 - operations: - - PUSH - providers: - - gitea - names: - - "regex:(?i)(^|-)secret(-|$).*" + operations: PUSH + provider: gitea + match: + target: NAME + value: "(?i)(^|-)secret(-|$).*" + type: REGEX diff --git a/docker/git-proxy-ldap.yml b/docker/git-proxy-ldap.yml index 0b6a7bc8..c521f360 100644 --- a/docker/git-proxy-ldap.yml +++ b/docker/git-proxy-ldap.yml @@ -30,19 +30,29 @@ auth: permissions: - username: testuser provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: PUSH - username: testuser provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: PUSH - username: admin provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: REVIEW - username: admin provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: REVIEW diff --git a/docker/git-proxy-local.yml b/docker/git-proxy-local.yml index f62b5ba6..e083b678 100644 --- a/docker/git-proxy-local.yml +++ b/docker/git-proxy-local.yml @@ -18,12 +18,17 @@ users: permissions: - username: admin provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: PUSH_AND_REVIEW - username: admin provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: PUSH_AND_REVIEW providers: diff --git a/docker/git-proxy-oidc.yml b/docker/git-proxy-oidc.yml index d73f1f6b..3ce20915 100644 --- a/docker/git-proxy-oidc.yml +++ b/docker/git-proxy-oidc.yml @@ -37,19 +37,29 @@ auth: permissions: - username: testuser provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: PUSH - username: testuser provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: PUSH - username: admin provider: gitea - path: /test-owner/test-repo + match: + target: SLUG + value: /test-owner/test-repo + type: LITERAL operations: REVIEW - username: admin provider: gitea - path: /otherorg/* - path-type: GLOB + match: + target: OWNER + value: otherorg + type: GLOB operations: REVIEW diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 4fdc7fe4..886dc1dd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -811,99 +811,141 @@ URL rules control which repositories are accessible through the proxy. git-proxy rules are configured for a provider, all pushes and fetches to that provider are rejected. At least one allow rule must match for a request to proceed. -Slugs, owners, and names support glob patterns (e.g. `finos/*`, `*-public`) and Java regex via a `regex:` prefix. +Rules use a unified `match` block that specifies what to match against (`target`), the pattern string (`value`), and how +to interpret it (`type`). Evaluation is first-match-wins by `order` — identical to iptables/firewall rule semantics. ```yaml rules: allow: - # Specific repos by slug (owner/name) — both FETCH and PUSH + # Specific repo by exact slug — both operations, scoped to one provider - enabled: true order: 110 - operations: - - FETCH - - PUSH - providers: - - github - slugs: - - /finos/git-proxy - - /coopernetes/test-repo - - # All repos under an owner — FETCH only, any provider + operations: BOTH + provider: github + match: + target: SLUG + value: /finos/git-proxy + type: LITERAL + + # All repos under an owner — fetch only, any provider - enabled: true order: 120 - operations: - - FETCH - owners: - - finos + operations: FETCH + match: + target: OWNER + value: finos + type: GLOB deny: # Block a specific repo across all operations - enabled: true order: 100 - slugs: - - /myorg/forbidden-repo + match: + target: SLUG + value: /myorg/forbidden-repo + type: LITERAL ``` -To allow all repositories on a provider (open mode), use a wildcard slug: +To allow all repositories on a provider (open mode): ```yaml rules: allow: - enabled: true order: 110 - operations: - - FETCH - - PUSH - slugs: - - "*/*" + operations: BOTH + provider: internal-github + match: + target: OWNER + value: "*" + type: GLOB ``` ### URL rule properties -| Property | Type | Default | Description | -| ------------ | ------- | ------- | -------------------------------------------------------------------------- | -| `enabled` | boolean | `true` | Whether this entry is active | -| `order` | int | — | Evaluation order (lower = earlier; first match wins) | -| `operations` | list | _none_ | `FETCH`, `PUSH` — which operations this entry matches | -| `providers` | list | _all_ | Provider names to scope this entry to | -| `slugs` | list | _none_ | `/owner/repo` slugs; supports glob patterns and `regex:` prefix | -| `owners` | list | _none_ | Owner/org names; supports glob patterns and `regex:` prefix | -| `names` | list | _none_ | Repository names; supports glob patterns and `regex:` prefix | +| Property | Type | Default | Description | +| ------------- | ------- | -------- | -------------------------------------------------------------------- | +| `enabled` | boolean | `true` | Whether this entry is active | +| `order` | int | `1100` | Evaluation order (lower = earlier; first match wins) | +| `operations` | string | `BOTH` | `FETCH`, `PUSH`, or `BOTH` — which operations this entry matches | +| `provider` | string | _(all)_ | Provider name to scope this entry to; omit or leave blank for all | +| `match` | object | — | Repository match criteria — see below | +| `match.target`| enum | `SLUG` | What to match: `SLUG` (`/owner/repo`), `OWNER`, or `NAME` | +| `match.value` | string | — | The pattern to match against the chosen target | +| `match.type` | enum | `GLOB` | How to interpret the pattern: `LITERAL`, `GLOB`, or `REGEX` | -### Pattern matching in rules +### Pattern matching -All three list fields (`slugs`, `owners`, `names`) support three matching modes: +Each rule matches exactly one thing — the `match` block selects what URL part to test (`target`) and how to test it +(`type`). To express an AND condition (e.g. specific owner AND specific name prefix), use `target: SLUG` and write a +single glob or regex that covers both parts. -- **Literal** (default): exact string match -- **Glob**: patterns using `*` (any characters) and `?` (single character) — e.g. `finos/*`, `*-public` -- **Regex**: prefix the pattern with `regex:` to use a full Java regular expression — e.g. `regex:(?i)(^|-)secret(-|$).*` +#### LITERAL + +Exact string match after normalising a leading `/`. `/acme/repo` and `acme/repo` are equivalent. + +#### GLOB + +Wildcard matching using `*` (any characters) and `?` (single character). + +| Target | `*` behaviour | +| ------- | ------------------------------------------------ | +| `SLUG` | Does **not** cross `/` — use `acme/*` not `acme/**` | +| `OWNER` | Owner names cannot contain `/` — `*` matches any valid name | +| `NAME` | Repo names cannot contain `/` — `*` matches any valid name | + +| Pattern (GLOB, target=SLUG) | Matches | Does NOT match | +| --------------------------- | ------- | -------------- | +| `/acme/repo` _(LITERAL)_ | `/acme/repo` | `/acme/other` | +| `/acme/*` | `/acme/repo`, `/acme/my-service` | `/other/repo` | +| `/acme/service-*` | `/acme/service-api`, `/acme/service-worker` | `/acme/repo` | +| `/acme/repo-?` | `/acme/repo-1`, `/acme/repo-a` | `/acme/repo-12` | + +#### REGEX + +Full Java regular expression. A few things to know before writing regex rules: + +- **Full-string match**: the pattern must match the entire candidate string — there are no implicit anchors, but + `matches()` semantics apply. A pattern of `acme` does **not** match `/acme/repo`; write `/acme/.*` or `.*acme.*`. +- **`/` does not need escaping**: Java regex uses strings, not a `/pattern/` literal syntax. Write `/acme/.*` not + `\/acme\/.*`. +- **Case-insensitive**: use the `(?i)` inline flag — e.g. `(?i)/acme/.*`. +- **Anchoring**: explicit `^` and `$` are redundant with `matches()` but harmless if included. + +| Pattern (REGEX, target=SLUG) | Matches | Does NOT match | +| ------------------------------------- | -------------------------------- | ------------------- | +| `/acme/.*` | `/acme/repo`, `/acme/my-service` | `/other/repo` | +| `/(acme\|partner)/.*` | `/acme/repo`, `/partner/repo` | `/other/repo` | +| `/acme/service-[0-9]+` | `/acme/service-1`, `/acme/service-42` | `/acme/service-api` | +| `(?i)/acme/.*` | `/acme/repo`, `/ACME/Repo` | `/other/repo` | + +| Pattern (REGEX, target=NAME) | Matches | Does NOT match | +| ------------------------------------------- | ------------------------------ | --------------- | +| `(?i)(^|-)secret(-|$).*` | `secret-config`, `my-secret` | `secretariat` | +| `migrate-.*` | `migrate-app`, `migrate-db` | `old-migrate` | ```yaml rules: deny: - # Block any repo whose name contains "secret" as a distinct word segment + # Block any repo whose name contains "secret" as a distinct word segment (case-insensitive) - enabled: true order: 50 - operations: - - PUSH - names: - - "regex:(?i)(^|-)secret(-|$).*" + operations: PUSH + match: + target: NAME + value: "(?i)(^|-)secret(-|$).*" + type: REGEX - # Block GitHub Pages repos (glob on name) + # Block repos matching multiple owner orgs using alternation - enabled: true order: 51 - operations: - - PUSH - providers: - - github - names: - - "*.github.io" + operations: BOTH + match: + target: OWNER + value: "(blocked-org|suspended-org)" + type: REGEX ``` -> **Glob on `owners` and `names`:** Owner and repository names cannot contain `/`, so `*` reliably matches any -> valid name including hyphens, underscores, and dots. You do not need `**` in these fields — `*` is sufficient -> and the two are equivalent in this context. - ### Real-world URL rule examples **Gateway for a specific SCM — allow all push and fetch:** @@ -913,90 +955,86 @@ rules: allow: - enabled: true order: 110 - operations: - - FETCH - - PUSH - providers: - - internal-github - slugs: - - "*/*" + operations: BOTH + provider: internal-github + match: + target: OWNER + value: "*" + type: GLOB ``` **Allow repos under a set of known owner orgs, identified by a name prefix:** -This is useful when multiple teams or divisions share a SCM and are distinguished by an owner-name convention -(e.g. `teamA-*`, `teamB-*`). - ```yaml rules: allow: - enabled: true order: 110 - operations: - - FETCH - - PUSH - providers: - - internal-github - owners: - - "regex:team-(alpha|beta|gamma)" + operations: BOTH + provider: internal-github + match: + target: OWNER + value: "team-(alpha|beta|gamma)" + type: REGEX ``` **Allow push only for repos whose name starts with a known prefix:** -When application or project repos are identified by a prefix (e.g. a fixed-length code followed by a hyphen), -match on `names` rather than the full slug so the rule applies regardless of which owner org the repo lives under. +When repos are identified by a project code followed by a hyphen, match on `NAME` so the rule applies regardless of +which org the repo lives under. ```yaml rules: allow: - enabled: true order: 110 - operations: - - PUSH - providers: - - internal-github - names: - - "proj0-*" - - "proj1-*" - - "shared-*" + operations: PUSH + provider: internal-github + match: + target: NAME + value: "proj0-*" + type: GLOB + + # Multiple project prefixes — one rule per prefix, or combine with REGEX alternation: + allow: + - enabled: true + order: 111 + operations: PUSH + provider: internal-github + match: + target: NAME + value: "(proj0|proj1|shared)-.*" + type: REGEX ``` -**Combine owner and name matching (SCM-to-SCM migration scenario):** +**Combine owner and name matching (AND condition):** -When acting as a gateway between two SCMs (e.g. during an M&A integration), you may want to allow only repos -that belong to a specific org AND follow a naming pattern. Use `slugs` with a glob or regex for this — each -entry in `owners` and `names` is evaluated independently (OR logic), so combining both in one rule block does -not produce an AND condition. +Use `target: SLUG` with a glob or regex — the slug is `/owner/repo` so a single pattern can constrain both parts. ```yaml rules: allow: - # Allow fetch from the source SCM for a specific org + name prefix — use slug for AND logic + # Allow fetch from the source SCM for a specific org + name prefix (glob AND) - enabled: true order: 110 - operations: - - FETCH - providers: - - source-github - slugs: - - "acquired-org/migrate-*" - - # Allow push to the destination SCM for the same set, using regex for stricter control + operations: FETCH + provider: source-github + match: + target: SLUG + value: "acquired-org/migrate-*" + type: GLOB + + # Allow push to the destination SCM — stricter control with regex - enabled: true order: 120 - operations: - - PUSH - providers: - - dest-gitlab - slugs: - - "regex:/migrated-org/migrate-.*" + operations: PUSH + provider: dest-gitlab + match: + target: SLUG + value: "/migrated-org/migrate-.*" + type: REGEX ``` -> **`owners` and `names` are OR conditions.** An entry under `owners: [acme]` allows any repo whose owner is -> `acme`, regardless of repo name. An entry under `names: [migrate-*]` allows any repo whose name matches, -> regardless of owner. To restrict both owner AND name together, use `slugs` with a glob (e.g. -> `acme/migrate-*`) or a `regex:`-prefixed pattern. - ## Permissions Permissions control which proxy users can push to or review pushes from specific repositories. They are checked @@ -1009,104 +1047,115 @@ permissions: # LITERAL (default): exact /owner/repo match - username: alice provider: github - path: /myorg/myrepo + match: + target: SLUG + value: /myorg/myrepo + type: LITERAL operations: PUSH - # GLOB: wildcard owner or repo + # GLOB: wildcard repo name under a specific owner - username: bob provider: gitlab - path: /myorg/* - path-type: GLOB + match: + target: SLUG + value: /myorg/* + type: GLOB operations: PUSH_AND_REVIEW - # REGEX: full Java regex matched against the /owner/repo path + # OWNER target: grant access to all repos under an org - username: carol provider: github - path: \/myorg\/service-.* - path-type: REGEX + match: + target: OWNER + value: myorg + type: GLOB operations: REVIEW + # REGEX on SLUG: match repos under multiple orgs + - username: dave + provider: github + match: + target: SLUG + value: "/team-(alpha|beta)/.*" + type: REGEX + operations: PUSH_AND_REVIEW + # SELF_CERTIFY: trusted contributor who can approve their own clean pushes. # Requires both this permission entry AND the SELF_CERTIFY role on the user. - username: trusted provider: github - path: /myorg/myrepo + match: + target: SLUG + value: /myorg/myrepo + type: LITERAL operations: SELF_CERTIFY ``` ### Permission properties -| Property | Type | Default | Description | -| ----------- | ------ | --------- | ------------------------------------------------------------------------------------------- | -| `username` | string | — | Proxy username (must match a `users:` entry or a DB user) | -| `provider` | string | — | Provider name as defined in `providers:` config | -| `path` | string | — | Repository path pattern (`/owner/repo`); interpretation depends on `path-type` | -| `path-type` | enum | `LITERAL` | `LITERAL` (exact), `GLOB` (`*`/`?` wildcards), `REGEX` (Java regex against the full path) | -| `operations`| enum | `PUSH` | What the user may do: `PUSH`, `REVIEW`, `PUSH_AND_REVIEW`, `SELF_CERTIFY` | - -### GLOB path semantics - -GLOB matching uses Java NIO `PathMatcher` (`glob:` prefix). Paths follow the `/owner/repo` convention. +| Property | Type | Default | Description | +| --------------- | ------ | ---------------- | ------------------------------------------------------------------------------- | +| `username` | string | — | Proxy username (must match a `users:` entry or a DB user) | +| `provider` | string | — | Provider name as defined in `providers:` config | +| `match` | object | — | Repository match criteria — see below | +| `match.target` | enum | `SLUG` | What to match: `SLUG` (`/owner/repo`), `OWNER`, or `NAME` | +| `match.value` | string | — | The pattern to match against the chosen target | +| `match.type` | enum | `GLOB` | How to interpret the pattern: `LITERAL`, `GLOB`, or `REGEX` | +| `operations` | enum | `PUSH_AND_REVIEW`| What the user may do: `PUSH`, `REVIEW`, `PUSH_AND_REVIEW`, `SELF_CERTIFY` | -| Wildcard | Matches | Does NOT match | -| -------- | ------- | -------------- | -| `*` | Any sequence of characters within **one** path segment (no `/` crossing) | Another path segment or the `/` separator itself | -| `**` | Any sequence of characters **including** path separators (zero or more segments) | — | -| `?` | Exactly **one** character within a path segment | `/` or a multi-character sequence | +### Pattern matching -Hyphens, digits, and dots in names are treated as regular characters — no special escaping needed. +Permissions support the same three match types as URL rules (LITERAL, GLOB, REGEX) applied to the same three targets +(SLUG, OWNER, NAME). See [Pattern matching](#pattern-matching) above for full semantics including regex behaviour. -**Pattern examples:** +GLOB on `target: SLUG` follows slug path conventions: -| Pattern | Matches | Does NOT match | -| ------- | ------- | -------------- | -| `/acme/repo` _(LITERAL)_ | `/acme/repo` | `/acme/other` | -| `/acme/*` | `/acme/repo`, `/acme/my-service`, `/acme/repo-v2` | `/acme/sub/repo`, `/other/repo` | -| `/acme/**` | `/acme/repo`, `/acme/sub/repo`, `/acme/a/b/c` | `/other/repo` | -| `/**` | Every path | — | -| `/*/repo` | `/acme/repo`, `/other/repo` | `/acme/other-repo` | -| `/acme/service-*` | `/acme/service-api`, `/acme/service-worker` | `/acme/repo`, `/acme/my-service-api` | -| `/acme/repo-?` | `/acme/repo-1`, `/acme/repo-a` | `/acme/repo-12`, `/acme/repo-` | - -> **Glob on the repo-name segment:** Repository names cannot contain `/`, so `*` will never cross a path separator -> when used in the name position (e.g. `/owner/prefix-*`). In this position `*` and `**` are equivalent — use `*`. +| Pattern (GLOB, target=SLUG) | Matches | Does NOT match | +| ----------------------------- | -------------------------------------- | ------------------- | +| `/acme/repo` | `/acme/repo` | `/acme/other` | +| `/acme/*` | `/acme/repo`, `/acme/my-service` | `/other/repo` | +| `/acme/service-*` | `/acme/service-api`, `/acme/service-worker` | `/acme/repo` | +| `/*/proj0-*` | `/acme/proj0-api`, `/other/proj0-db` | `/acme/other` | > **Conflict detection:** At config load time and when saving via the dashboard API, git-proxy-java rejects any -> new permission entry whose path overlaps with an existing entry for the same user and provider. Two paths overlap -> when they are equal, or when one is a GLOB/REGEX pattern that would match the other path string. This prevents +> new permission entry whose pattern overlaps with an existing entry for the same user and provider. Two entries overlap +> when they are equal, or when one is a GLOB/REGEX pattern that would match the other's value. This prevents > silent misconfiguration where the effective permission depends on evaluation order. ### Real-world permission examples -**Gateway use case — allow a user to push any repo on a specific provider:** +**Allow a user to push any repo on a specific provider:** ```yaml permissions: - username: alice provider: internal-github - path: "/**" - path-type: GLOB + match: + target: OWNER + value: "*" + type: GLOB operations: PUSH_AND_REVIEW ``` -**Allow push to all repos whose name starts with a known prefix (e.g. a project code):** - -Useful when repos are identified by a fixed prefix such as a project or application code. Since repo names cannot -contain `/`, the `*` in the name segment matches any suffix including hyphens and digits. +**Allow push to repos whose name starts with a project code:** ```yaml permissions: - username: alice provider: internal-github - path: "/myorg/proj0-*" - path-type: GLOB + match: + target: NAME + value: "proj0-*" + type: GLOB operations: PUSH - # Or match the same prefix across any owner using a wildcard in the owner position: + # Or match the same prefix across any owner using SLUG: - username: alice provider: internal-github - path: "/*/proj0-*" - path-type: GLOB + match: + target: SLUG + value: "/*/proj0-*" + type: GLOB operations: PUSH ``` @@ -1116,8 +1165,10 @@ permissions: permissions: - username: alice provider: internal-github - path: "/team-(alpha|beta)/.*" - path-type: REGEX + match: + target: SLUG + value: "/team-(alpha|beta)/.*" + type: REGEX operations: PUSH_AND_REVIEW ``` @@ -1131,15 +1182,19 @@ permissions: # Push and review access - username: trusted-dev provider: internal-github - path: "/myorg/proj0-*" - path-type: GLOB + match: + target: NAME + value: "proj0-*" + type: GLOB operations: PUSH_AND_REVIEW # Self-certify on the same scope (requires SELF_CERTIFY role on the user too) - username: trusted-dev provider: internal-github - path: "/myorg/proj0-*" - path-type: GLOB + match: + target: NAME + value: "proj0-*" + type: GLOB operations: SELF_CERTIFY ``` diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/DatabaseMigrator.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/DatabaseMigrator.java index a6f4eda3..3a6fb8f3 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/DatabaseMigrator.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/DatabaseMigrator.java @@ -40,7 +40,8 @@ private record Migration(String version, String description, String resource, bo new Migration( "2.1", "widen provider columns", "db/migration-postgresql/V2_1__widen_provider_columns.sql", true), new Migration("3", "email unique constraint", "db/migration/V3__email_unique.sql", false), - new Migration("4", "spring session tables", "db/migration/V4__spring_session.sql", false)); + new Migration("4", "spring session tables", "db/migration/V4__spring_session.sql", false), + new Migration("5", "unified rule shape", "db/migration/V5__unified_rule_shape.sql", false)); // --------------------------------------------------------------------------- diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcUrlRuleRegistry.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcUrlRuleRegistry.java index 06004905..421edc45 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcUrlRuleRegistry.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcUrlRuleRegistry.java @@ -34,10 +34,10 @@ public void initialize() { public void save(AccessRule rule) { jdbc.update(""" INSERT INTO access_rules - (id, provider, slug, owner, name, access, operations, + (id, provider, target, match_value, match_type, access, operations, description, enabled, rule_order, source) VALUES - (:id, :provider, :slug, :owner, :name, :access, :operations, + (:id, :provider, :target, :matchValue, :matchType, :access, :operations, :description, :enabled, :ruleOrder, :source) """, params(rule)); } @@ -46,7 +46,7 @@ public void save(AccessRule rule) { public void update(AccessRule rule) { jdbc.update(""" UPDATE access_rules SET - provider = :provider, slug = :slug, owner = :owner, name = :name, + provider = :provider, target = :target, match_value = :matchValue, match_type = :matchType, access = :access, operations = :operations, description = :description, enabled = :enabled, rule_order = :ruleOrder, source = :source WHERE id = :id @@ -84,9 +84,9 @@ private static MapSqlParameterSource params(AccessRule rule) { return new MapSqlParameterSource() .addValue("id", rule.getId()) .addValue("provider", rule.getProvider()) - .addValue("slug", rule.getSlug()) - .addValue("owner", rule.getOwner()) - .addValue("name", rule.getName()) + .addValue("target", rule.getTarget().name()) + .addValue("matchValue", rule.getValue()) + .addValue("matchType", rule.getMatchType().name()) .addValue("access", rule.getAccess().name()) .addValue("operations", rule.getOperations().name()) .addValue("description", rule.getDescription()) diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/mapper/AccessRuleRowMapper.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/mapper/AccessRuleRowMapper.java index 3eb2afa4..05d39b90 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/mapper/AccessRuleRowMapper.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/mapper/AccessRuleRowMapper.java @@ -3,6 +3,8 @@ import java.sql.ResultSet; import java.sql.SQLException; import org.finos.gitproxy.db.model.AccessRule; +import org.finos.gitproxy.db.model.MatchTarget; +import org.finos.gitproxy.db.model.MatchType; import org.springframework.jdbc.core.RowMapper; /** Maps an {@code access_rules} result-set row to an {@link AccessRule}. */ @@ -17,9 +19,9 @@ public AccessRule mapRow(ResultSet rs, int rowNum) throws SQLException { return AccessRule.builder() .id(rs.getString("id")) .provider(rs.getString("provider")) - .slug(rs.getString("slug")) - .owner(rs.getString("owner")) - .name(rs.getString("name")) + .target(MatchTarget.valueOf(rs.getString("target"))) + .value(rs.getString("match_value")) + .matchType(MatchType.valueOf(rs.getString("match_type"))) .access(AccessRule.Access.valueOf(rs.getString("access"))) .operations(AccessRule.Operations.valueOf(rs.getString("operations"))) .description(rs.getString("description")) diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/AccessRule.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/AccessRule.java index a66ae57b..e7603235 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/AccessRule.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/AccessRule.java @@ -7,11 +7,10 @@ /** * A single access control rule governing which repositories may be fetched from or pushed to through the proxy. Rules - * are evaluated by {@code UrlAccessControlFilter} (see #60). + * are evaluated by {@code UrlRuleEvaluator} (see #60). * - *

The {@code owner}, {@code name}, and {@code slug} fields use the standard {@code {owner}/{repo}} URL shape shared - * by GitHub, GitLab, Gitea, Codeberg, and Forgejo. Generic providers with arbitrary URL paths should use {@code slug} - * with a glob pattern and leave {@code owner}/{@code name} null. + *

{@link #target} selects which part of the repo URL is compared; {@link #value} is the pattern string; + * {@link #matchType} controls how the pattern is interpreted (literal equality, glob, or regex). */ @Data @Builder @@ -24,14 +23,16 @@ public class AccessRule { /** Provider name this rule applies to. Null = applies to all providers. */ private String provider; - /** Glob pattern matching {@code /owner/repo} slug. Null = not used. */ - private String slug; + /** Which part of the repository URL is matched. Defaults to {@link MatchTarget#SLUG}. */ + @Builder.Default + private MatchTarget target = MatchTarget.SLUG; - /** Glob pattern matching the owner/org portion of the URL. Null = not used. */ - private String owner; + /** Pattern to match against the {@link #target} portion of the URL. */ + private String value; - /** Glob pattern matching the repository name portion of the URL. Null = not used. */ - private String name; + /** How {@link #value} is interpreted when matching. Defaults to {@link MatchType#GLOB}. */ + @Builder.Default + private MatchType matchType = MatchType.GLOB; /** Whether this rule allows or denies matched repositories. */ @Builder.Default diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/MatchTarget.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/MatchTarget.java new file mode 100644 index 00000000..5251c4db --- /dev/null +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/MatchTarget.java @@ -0,0 +1,11 @@ +package org.finos.gitproxy.db.model; + +/** Which part of the repository URL path the match pattern is applied to. */ +public enum MatchTarget { + /** Full {@code /owner/repo} slug. */ + SLUG, + /** Owner or organisation portion only (e.g. {@code myorg}). */ + OWNER, + /** Repository name portion only (e.g. {@code my-repo}). */ + NAME +} diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/MatchType.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/MatchType.java new file mode 100644 index 00000000..a1503abc --- /dev/null +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/MatchType.java @@ -0,0 +1,11 @@ +package org.finos.gitproxy.db.model; + +/** How a pattern value is matched against a repository path component. */ +public enum MatchType { + /** Exact string equality (case-sensitive). Leading {@code /} is normalised before comparison. */ + LITERAL, + /** Shell glob: {@code *} matches within one path segment; {@code **} matches any depth. */ + GLOB, + /** Full Java regex matched against the value as-is. */ + REGEX +} diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/mongo/MongoUrlRuleRegistry.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/mongo/MongoUrlRuleRegistry.java index 73e2ceff..39b5cc63 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/mongo/MongoUrlRuleRegistry.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/mongo/MongoUrlRuleRegistry.java @@ -12,6 +12,8 @@ import org.bson.Document; import org.finos.gitproxy.db.UrlRuleRegistry; import org.finos.gitproxy.db.model.AccessRule; +import org.finos.gitproxy.db.model.MatchTarget; +import org.finos.gitproxy.db.model.MatchType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,9 +87,9 @@ private MongoCollection getCollection() { private static Document toDocument(AccessRule r) { return new Document("_id", r.getId()) .append("provider", r.getProvider()) - .append("slug", r.getSlug()) - .append("owner", r.getOwner()) - .append("name", r.getName()) + .append("target", r.getTarget().name()) + .append("value", r.getValue()) + .append("matchType", r.getMatchType().name()) .append("access", r.getAccess().name()) .append("operations", r.getOperations().name()) .append("description", r.getDescription()) @@ -100,9 +102,9 @@ private static AccessRule fromDocument(Document doc) { return AccessRule.builder() .id(doc.getString("_id")) .provider(doc.getString("provider")) - .slug(doc.getString("slug")) - .owner(doc.getString("owner")) - .name(doc.getString("name")) + .target(MatchTarget.valueOf(doc.getString("target"))) + .value(doc.getString("value")) + .matchType(MatchType.valueOf(doc.getString("matchType"))) .access(AccessRule.Access.valueOf(doc.getString("access"))) .operations(AccessRule.Operations.valueOf(doc.getString("operations"))) .description(doc.getString("description")) diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/JdbcRepoPermissionStore.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/JdbcRepoPermissionStore.java index ae2ec6b0..cb33e9e9 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/JdbcRepoPermissionStore.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/JdbcRepoPermissionStore.java @@ -6,6 +6,8 @@ import java.util.Map; import java.util.Optional; import javax.sql.DataSource; +import org.finos.gitproxy.db.model.MatchTarget; +import org.finos.gitproxy.db.model.MatchType; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -22,8 +24,8 @@ public JdbcRepoPermissionStore(DataSource dataSource) { @Override public void save(RepoPermission p) { jdbc.update(""" - INSERT INTO repo_permissions (id, username, provider, path, path_type, operations, source) - VALUES (:id, :username, :provider, :path, :pathType, :operations, :source) + INSERT INTO repo_permissions (id, username, provider, target, match_value, match_type, operations, source) + VALUES (:id, :username, :provider, :target, :matchValue, :matchType, :operations, :source) """, params(p)); } @@ -40,13 +42,13 @@ public Optional findById(String id) { @Override public List findAll() { - return jdbc.query("SELECT * FROM repo_permissions ORDER BY provider, path, username", ROW_MAPPER); + return jdbc.query("SELECT * FROM repo_permissions ORDER BY provider, match_value, username", ROW_MAPPER); } @Override public List findByUsername(String username) { return jdbc.query( - "SELECT * FROM repo_permissions WHERE username = :username ORDER BY provider, path", + "SELECT * FROM repo_permissions WHERE username = :username ORDER BY provider, match_value", Map.of("username", username), ROW_MAPPER); } @@ -54,7 +56,7 @@ public List findByUsername(String username) { @Override public List findByProvider(String provider) { return jdbc.query( - "SELECT * FROM repo_permissions WHERE provider = :provider ORDER BY path, username", + "SELECT * FROM repo_permissions WHERE provider = :provider ORDER BY match_value, username", Map.of("provider", provider), ROW_MAPPER); } @@ -64,8 +66,9 @@ private static MapSqlParameterSource params(RepoPermission p) { .addValue("id", p.getId()) .addValue("username", p.getUsername()) .addValue("provider", p.getProvider()) - .addValue("path", p.getPath()) - .addValue("pathType", p.getPathType().name()) + .addValue("target", p.getTarget().name()) + .addValue("matchValue", p.getValue()) + .addValue("matchType", p.getMatchType().name()) .addValue("operations", p.getOperations().name()) .addValue("source", p.getSource().name()); } @@ -77,8 +80,9 @@ private static RepoPermission mapRow(ResultSet rs, int i) throws SQLException { .id(rs.getString("id")) .username(rs.getString("username")) .provider(rs.getString("provider")) - .path(rs.getString("path")) - .pathType(RepoPermission.PathType.valueOf(rs.getString("path_type"))) + .target(MatchTarget.valueOf(rs.getString("target"))) + .value(rs.getString("match_value")) + .matchType(MatchType.valueOf(rs.getString("match_type"))) .operations(RepoPermission.Operations.valueOf(rs.getString("operations"))) .source(RepoPermission.Source.valueOf(rs.getString("source"))) .build(); diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/MongoRepoPermissionStore.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/MongoRepoPermissionStore.java index 68defc5f..10b76a4d 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/MongoRepoPermissionStore.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/MongoRepoPermissionStore.java @@ -10,6 +10,8 @@ import java.util.List; import java.util.Optional; import org.bson.Document; +import org.finos.gitproxy.db.model.MatchTarget; +import org.finos.gitproxy.db.model.MatchType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,7 +56,7 @@ public List findAll() { List results = new ArrayList<>(); getCollection() .find() - .sort(Sorts.ascending("provider", "path", "username")) + .sort(Sorts.ascending("provider", "value", "username")) .forEach(doc -> results.add(fromDocument(doc))); return results; } @@ -64,7 +66,7 @@ public List findByUsername(String username) { List results = new ArrayList<>(); getCollection() .find(Filters.eq("username", username)) - .sort(Sorts.ascending("provider", "path")) + .sort(Sorts.ascending("provider", "value")) .forEach(doc -> results.add(fromDocument(doc))); return results; } @@ -74,7 +76,7 @@ public List findByProvider(String provider) { List results = new ArrayList<>(); getCollection() .find(Filters.eq("provider", provider)) - .sort(Sorts.ascending("path", "username")) + .sort(Sorts.ascending("value", "username")) .forEach(doc -> results.add(fromDocument(doc))); return results; } @@ -87,8 +89,9 @@ private static Document toDocument(RepoPermission p) { return new Document("_id", p.getId()) .append("username", p.getUsername()) .append("provider", p.getProvider()) - .append("path", p.getPath()) - .append("pathType", p.getPathType().name()) + .append("target", p.getTarget().name()) + .append("value", p.getValue()) + .append("matchType", p.getMatchType().name()) .append("operations", p.getOperations().name()) .append("source", p.getSource().name()); } @@ -98,8 +101,9 @@ private static RepoPermission fromDocument(Document doc) { .id(doc.getString("_id")) .username(doc.getString("username")) .provider(doc.getString("provider")) - .path(doc.getString("path")) - .pathType(RepoPermission.PathType.valueOf(doc.getString("pathType"))) + .target(MatchTarget.valueOf(doc.getString("target"))) + .value(doc.getString("value")) + .matchType(MatchType.valueOf(doc.getString("matchType"))) .operations(RepoPermission.Operations.valueOf(doc.getString("operations"))) .source(RepoPermission.Source.valueOf(doc.getString("source"))) .build(); diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermission.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermission.java index bdd659f0..6921cea3 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermission.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermission.java @@ -5,14 +5,16 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.finos.gitproxy.db.model.MatchTarget; +import org.finos.gitproxy.db.model.MatchType; /** * A single authorization grant: {@link #username} is permitted to perform {@link #operations} on repos matching - * {@link #path} at {@link #provider}. + * {@link #value} at {@link #provider}. * - *

{@link #path} is a pattern of the form {@code /owner/repo}. {@link #pathType} controls how it is matched: - * {@code LITERAL} for exact equality, {@code GLOB} for {@code *}/{@code ?} wildcards, {@code REGEX} for full Java regex - * matched against the path string. + *

{@link #target} selects which part of the repo URL is compared (default {@link MatchTarget#SLUG}); + * {@link #matchType} controls how {@link #value} is interpreted: {@code LITERAL} for exact equality, {@code GLOB} for + * {@code *}/{@code ?} wildcards, {@code REGEX} for full Java regex. */ @Data @Builder @@ -25,10 +27,17 @@ public class RepoPermission { private String username; private String provider; - private String path; + /** Which part of the repository URL is matched. Defaults to {@link MatchTarget#SLUG}. */ @Builder.Default - private PathType pathType = PathType.LITERAL; + private MatchTarget target = MatchTarget.SLUG; + + /** Pattern to match against the {@link #target} portion of the URL. */ + private String value; + + /** How {@link #value} is interpreted when matching. Defaults to {@link MatchType#LITERAL}. */ + @Builder.Default + private MatchType matchType = MatchType.LITERAL; @Builder.Default private Operations operations = Operations.PUSH; @@ -36,12 +45,6 @@ public class RepoPermission { @Builder.Default private Source source = Source.DB; - public enum PathType { - LITERAL, - GLOB, - REGEX - } - public enum Operations { /** Can submit pushes for review. */ PUSH, diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermissionService.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermissionService.java index 85bad823..779436b7 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermissionService.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/permission/RepoPermissionService.java @@ -9,6 +9,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import lombok.extern.slf4j.Slf4j; +import org.finos.gitproxy.db.model.MatchType; /** * Service that evaluates whether a proxy user is authorised to push to or approve a push for a given repository. @@ -22,7 +23,7 @@ *

Path matching

* *

Paths use the {@code /owner/repo} convention (leading slash, no {@code .git} suffix). Matching is controlled by - * {@link RepoPermission.PathType}: + * {@link MatchType}: * *