@colony2/gitvend 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -379
- package/bin/jobdb-gitvend-secret-provider +4 -0
- package/docs/development.md +120 -0
- package/docs/docker-compose.md +181 -0
- package/docs/gateway.md +448 -0
- package/docs/git-proxy-design.md +308 -0
- package/docs/jobdb-secret-provider.md +303 -0
- package/docs/permission-syntax.md +136 -0
- package/examples/gitvend.yaml +31 -0
- package/examples/grant.json +9 -0
- package/examples/jobdb-gitvend-issuer.yaml +16 -0
- package/package.json +6 -3
- package/scripts/postinstall.js +20 -10
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Gitvend secret-provider plugin for JobDB
|
|
2
|
+
|
|
3
|
+
The JobDB plugin gives workers short-lived Gitvend credentials for their jobs.
|
|
4
|
+
You configure permissions per tenant and additional permissions for the target
|
|
5
|
+
cell repository. JobDB starts the plugin when needed and passes verified job and
|
|
6
|
+
lease information. The plugin signs tokens locally; the gateway needs only the
|
|
7
|
+
matching public key. Workers never receive the GitHub token or private signing key.
|
|
8
|
+
|
|
9
|
+
Set up a [Gitvend gateway](gateway.md) first, or configure it alongside the plugin.
|
|
10
|
+
The plugin runs on the JobDB host; it does not replace the gateway.
|
|
11
|
+
It returns only requested, authorized secret names:
|
|
12
|
+
|
|
13
|
+
| Secret name | JWT permissions |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `gitvend_base_token` | The configured tenant `base_permissions`, unchanged. |
|
|
16
|
+
| `gitvend_cell_token` | Base permissions plus matching `target_permissions`, narrowed to the exact target cell repository. |
|
|
17
|
+
|
|
18
|
+
These are signed JWT **tokens**, not signing keys. Both lists accept the standard
|
|
19
|
+
[Gitvend permission syntax](permission-syntax.md), including `r`, `w`, `d`, `c`,
|
|
20
|
+
ref selectors, and denies. A base-token request does not need cell metadata.
|
|
21
|
+
Matching denies still win across both lists and the gateway's deny rules.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
On the host where JobDB runs, install the package:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install -g @colony2/gitvend
|
|
29
|
+
jobdb-gitvend-secret-provider --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package supplies prebuilt `gitvend` and `jobdb-gitvend-secret-provider`
|
|
33
|
+
executables for Linux/macOS on amd64/arm64. npm requires Node.js 22+, `tar`, and
|
|
34
|
+
enabled install scripts. For installation without Node.js, download a
|
|
35
|
+
[release archive](https://github.com/colony-2/gitvend/releases) containing both
|
|
36
|
+
executables, verify its checksum, and put them on `PATH`. Older releases may
|
|
37
|
+
contain only the gateway; use a release that includes the plugin.
|
|
38
|
+
|
|
39
|
+
The npm installer places a native executable at the plugin command path, so
|
|
40
|
+
JobDB does not need Node.js or `PATH` in the plugin environment.
|
|
41
|
+
|
|
42
|
+
Use a JobDB version with secret-provider plugin support. JobDB must be able to
|
|
43
|
+
execute the installed command and read the issuer configuration and signing key.
|
|
44
|
+
|
|
45
|
+
## Configure the issuer
|
|
46
|
+
|
|
47
|
+
Configuration uses YAML. Unknown fields, duplicate keys and multiple documents
|
|
48
|
+
are rejected. JSON-shaped mappings also load as YAML.
|
|
49
|
+
|
|
50
|
+
Generate a signing key on the trusted JobDB host:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
mkdir -p gitvend-issuer
|
|
54
|
+
gitvend keygen --private gitvend-issuer/signing.key --public gitvend-issuer/signing.pub
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Save this as `gitvend-issuer/issuer.yaml`, replacing `YOUR_TENANT_ID` with the
|
|
58
|
+
actual JobDB tenant ID:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
issuer: jobdb
|
|
62
|
+
audience: gitvend
|
|
63
|
+
key_id: jobdb
|
|
64
|
+
signing_key_file: signing.key
|
|
65
|
+
cell_metadata_field: repo
|
|
66
|
+
tenants:
|
|
67
|
+
YOUR_TENANT_ID:
|
|
68
|
+
base_permissions:
|
|
69
|
+
- github.com/*:r
|
|
70
|
+
target_permissions:
|
|
71
|
+
- github.com/colony-2/*:rwc
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This permits all GitHub reads available through the gateway, plus read/write/create
|
|
75
|
+
on the target cell under `colony-2`. Use narrower base rules when tenants must not
|
|
76
|
+
share private repository access. The [complete issuer example](../examples/jobdb-gitvend-issuer.yaml)
|
|
77
|
+
also shows token lifetime and size settings.
|
|
78
|
+
|
|
79
|
+
`signing_key_file` resolves relative to the issuer file. Keep the private key on
|
|
80
|
+
the JobDB host and copy only `signing.pub` to the gateway. Add a gateway `keys`
|
|
81
|
+
entry with ID `jobdb`, issuer `jobdb`, and the public key's path. Its owner
|
|
82
|
+
allowlist and the upstream provider's allowed owners must cover the repositories
|
|
83
|
+
in your policy. The audience must also match. Configure upstream GitHub credentials
|
|
84
|
+
on the gateway separately.
|
|
85
|
+
|
|
86
|
+
Validate before registering the plugin:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
jobdb-gitvend-secret-provider validate --config gitvend-issuer/issuer.yaml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Expected output: `Configuration and signing key are valid.` This checks the local
|
|
93
|
+
configuration and private key; it does not test GitHub access or gateway connectivity.
|
|
94
|
+
|
|
95
|
+
## Tenant permissions
|
|
96
|
+
|
|
97
|
+
Example policy for more restricted reads:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
base_permissions:
|
|
101
|
+
- github.com/public-org/*:r
|
|
102
|
+
- github.com/example-org/shared-library:r
|
|
103
|
+
target_permissions:
|
|
104
|
+
- github.com/example-org/cell-*:rwc
|
|
105
|
+
principals:
|
|
106
|
+
- trusted-job-submitter
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`base_permissions` applies to both tokens, including the target repository.
|
|
110
|
+
`target_permissions` grants additional authority only for the repository selected
|
|
111
|
+
by metadata. For example, this tenant policy allows all accessible GitHub reads
|
|
112
|
+
and read/write/create on the target cell if it belongs to `colony-2`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
base_permissions:
|
|
116
|
+
- github.com/*:r
|
|
117
|
+
target_permissions:
|
|
118
|
+
- github.com/colony-2/*:rwc
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For `AppMetadata.repo = "https://github.com/colony-2/my-cell.git"`, the cell JWT
|
|
122
|
+
contains `github.com/*:r` and `github.com/colony-2/my-cell:rwc`. It does not grant
|
|
123
|
+
writes or creation to any other cell. A target rule such as
|
|
124
|
+
`github.com/colony-2/*#work/*:rw` retains its branch selector after narrowing.
|
|
125
|
+
Use a separate repository-wide `:c` rule for creation when writes are branch-scoped;
|
|
126
|
+
Gitvend rejects `c` with a ref selector.
|
|
127
|
+
|
|
128
|
+
An empty/omitted base list disables the base token. An empty/omitted target list,
|
|
129
|
+
or no matching target allow rule, disables the cell token. A tenant must have
|
|
130
|
+
at least one rule, with at most 128 rules across both lists. Optional `principals`
|
|
131
|
+
restricts both tokens to those exact broker-verified principals; omitting it
|
|
132
|
+
authorizes all principals, including absent principals, within that tenant.
|
|
133
|
+
|
|
134
|
+
Creation still requires the gateway provider's `allow_creation: true` and a
|
|
135
|
+
readable/writable branch. Its `creation_defaults` chooses visibility and GitHub
|
|
136
|
+
features; the issuer cannot override those defaults. See
|
|
137
|
+
[gateway creation settings](gateway.md#repository-creation-defaults).
|
|
138
|
+
|
|
139
|
+
### Migrating the original issuer config
|
|
140
|
+
|
|
141
|
+
Rename `read_permissions` to `base_permissions`. Replace `cell_write_repositories`
|
|
142
|
+
with `target_permissions`, adding `:rw` to each old repository pattern to preserve
|
|
143
|
+
its permissions, or `:rwc` to also allow creation. Update workers to request
|
|
144
|
+
`gitvend_base_token` instead of `gitvend_read_token`; `gitvend_cell_token` is unchanged.
|
|
145
|
+
Old config fields are rejected, and the old secret name is omitted rather than
|
|
146
|
+
silently returning a token that may now carry write permissions. Deploy the
|
|
147
|
+
new config and executable together; validate the config before restarting JobDB's
|
|
148
|
+
plugin process.
|
|
149
|
+
|
|
150
|
+
## Register with JobDB
|
|
151
|
+
|
|
152
|
+
Find the installed command with `command -v jobdb-gitvend-secret-provider`.
|
|
153
|
+
Save a `providers.json` file using absolute paths for that executable and your
|
|
154
|
+
issuer configuration:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"version": 1,
|
|
159
|
+
"providers": [
|
|
160
|
+
{
|
|
161
|
+
"id": "gitvend",
|
|
162
|
+
"type": "plugin",
|
|
163
|
+
"command": "/absolute/path/to/jobdb-gitvend-secret-provider",
|
|
164
|
+
"args": ["--config", "/absolute/path/to/gitvend-issuer/issuer.yaml"]
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
jobdb sqlite --db ./jobdb.db --secret-providers-file /absolute/path/to/providers.json
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Other providers, including SQLite, can coexist in the same provider file; the first entry for each secret name wins. See
|
|
175
|
+
[the provider guide](https://github.com/colony-2/jobdb/blob/f403447cb58a7195e32a55fb7f417e09da70f388/docs/GUIDE-SECRET-PROVIDERS.md) for collection behavior.
|
|
176
|
+
An omission here cannot veto a grant from a different provider.
|
|
177
|
+
|
|
178
|
+
The worker requests either or both names through JobDB's normal secret-resolution
|
|
179
|
+
API and uses the returned JWT as the Gitvend HTTP Basic password. It still uses
|
|
180
|
+
the gateway's Git URL, for example
|
|
181
|
+
`https://gitvend.example/github/example-org/cell-one.git`, with Git protocol v2.
|
|
182
|
+
Follow the [agent authentication instructions](gateway.md#configure-git-authentication)
|
|
183
|
+
to configure the Git credential helper.
|
|
184
|
+
|
|
185
|
+
## Cell metadata and authorization
|
|
186
|
+
|
|
187
|
+
The default `cell_metadata_field` is `repo`, matching c2j's target repository
|
|
188
|
+
metadata. It is a top-level field in `SecretResolveRequest.AppMetadata`, e.g.:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"cell_id": "cell-123",
|
|
193
|
+
"cell_name": "cell-one",
|
|
194
|
+
"repo": "https://github.com/example-org/cell-one.git"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Accepted repository forms are `https://host/owner/repo[.git]` and
|
|
199
|
+
`host/owner/repo[.git]`. Owner/repository casing is normalized. Credentials, query
|
|
200
|
+
strings, fragments, encoded paths, extra path segments and permission syntax are
|
|
201
|
+
rejected. Configure a different field if your application uses another name;
|
|
202
|
+
its value must still identify a repository, not an opaque cell ID. SSH URLs and
|
|
203
|
+
cell-ID-to-repository lookups are not implemented.
|
|
204
|
+
|
|
205
|
+
The provider matches that repository against the **verified tenant's** configured
|
|
206
|
+
target rules, then replaces each matching rule's repository pattern with the
|
|
207
|
+
exact repository while preserving its actions, ref selector and deny flag.
|
|
208
|
+
Missing, malformed or unauthorized metadata omits the cell token while leaving
|
|
209
|
+
an independently authorized base token available. Unknown names and tenants are
|
|
210
|
+
also omitted.
|
|
211
|
+
|
|
212
|
+
Application metadata may be submitter-controlled. The configured target patterns
|
|
213
|
+
are an authorization ceiling: authorized jobs in that tenant may select any
|
|
214
|
+
matching cell. If individual jobs or principals must be confined to different
|
|
215
|
+
cells within a tenant, a trusted job/principal-to-cell authorization lookup is
|
|
216
|
+
needed before widening this model. Metadata by itself does not establish such
|
|
217
|
+
a binding. `cell_id` and `cell_name` do not override `repo`.
|
|
218
|
+
|
|
219
|
+
## Private repository reads
|
|
220
|
+
|
|
221
|
+
The plugin does not discover GitHub visibility or user membership. `github.com/*:r` authorizes reads across the host, intersected
|
|
222
|
+
with Gitvend's signing-key owner ceiling, gateway routes/denies, and the actual
|
|
223
|
+
upstream GitHub token's permissions. Permission strings use `github.com`, not
|
|
224
|
+
the route alias `github`.
|
|
225
|
+
|
|
226
|
+
This broad setting works when all tenants receiving it are allowed to read
|
|
227
|
+
everything available to those Gitvend upstream credentials. The upstream token
|
|
228
|
+
cannot provide tenant-specific isolation: Gitvend selects it by provider route,
|
|
229
|
+
not by the JobDB tenant or principal. With a shared upstream token, a tenant can
|
|
230
|
+
read other tenants' private repositories if all those checks permit them.
|
|
231
|
+
|
|
232
|
+
For different tenant read scopes, configure explicit tenant read patterns. For
|
|
233
|
+
credential-level separation, use separate gateway deployments/credentials or
|
|
234
|
+
non-overlapping owner routes and key ceilings as appropriate. A pattern does
|
|
235
|
+
not mean “public repositories only”: matching private repositories are readable
|
|
236
|
+
if the upstream token allows them. The plugin has no “public repositories only” permission option.
|
|
237
|
+
|
|
238
|
+
## Lifetime and operation
|
|
239
|
+
|
|
240
|
+
The signed expiry and returned `SecretValue.ExpiresAt` are identical, and
|
|
241
|
+
`LeaseBound` is true. The provider uses the earlier of `AuthorizedNotAfter` and
|
|
242
|
+
`LeaseExpiresAt`, subtracts gateway leeway and the clock margin, and rounds down
|
|
243
|
+
to a whole second. It also caps the encoded lifetime at `max_lifetime_seconds`.
|
|
244
|
+
Defaults are 900 seconds, 30 seconds of gateway leeway, a 1-second clock margin,
|
|
245
|
+
and a 4096-byte token limit. Match these settings to the gateway; the provider
|
|
246
|
+
cannot discover its configuration. Set the margin for your actual clock skew.
|
|
247
|
+
|
|
248
|
+
Missing verified job/lease identity or deadlines fails issuance. Too little
|
|
249
|
+
remaining lifetime returns `ErrSecretsUnavailable`. The request context bounds
|
|
250
|
+
resolution work, not token lifetime. Backend renewal cannot extend a token minted
|
|
251
|
+
through an older presenting capability; obtain a renewed handle and use JobDB's
|
|
252
|
+
fresh secret resolution to refresh credentials. Already delivered JWTs remain
|
|
253
|
+
valid until their expiry plus gateway leeway even if the job is canceled earlier.
|
|
254
|
+
|
|
255
|
+
Every resolution issues fresh tokens. `policy_revision` is included for gateway
|
|
256
|
+
auditing; the audit subject identifies the tenant/job/lease context without
|
|
257
|
+
exposing the raw identifiers.
|
|
258
|
+
|
|
259
|
+
## Configuration reloads and key rotation
|
|
260
|
+
|
|
261
|
+
Configuration and signing-key changes reload automatically on Linux and macOS,
|
|
262
|
+
including atomic file replacements. A request uses one complete configuration
|
|
263
|
+
and key snapshot; subsequent requests use the replacement after validation.
|
|
264
|
+
Removed tenants and permissions affect newly issued tokens.
|
|
265
|
+
|
|
266
|
+
A malformed, missing or unreadable replacement leaves the last valid snapshot
|
|
267
|
+
active. The plugin reports a fixed error message and retries every second until
|
|
268
|
+
loading succeeds, including when a new signing-key path does not exist yet.
|
|
269
|
+
Startup still requires valid files and access to watch their directories. For coordinated key
|
|
270
|
+
rotation, write a new key file first, then atomically replace the config with its
|
|
271
|
+
new path and key ID; provision the corresponding gateway public key beforehand.
|
|
272
|
+
|
|
273
|
+
Reloading does not revoke already issued JWTs or replace credentials cached by
|
|
274
|
+
JobDB workflows. Use fresh secret resolution to obtain updated permissions.
|
|
275
|
+
Diagnostics go to stderr without keys, JWTs, raw configuration, or raw errors;
|
|
276
|
+
the JobDB host may suppress plugin output. Watching uses local filesystem events,
|
|
277
|
+
with no periodic polling after successful loads. Network filesystems and changes
|
|
278
|
+
to symlink targets outside the watched directories require a different update
|
|
279
|
+
mechanism or restarting the plugin host.
|
|
280
|
+
|
|
281
|
+
## Command reference
|
|
282
|
+
|
|
283
|
+
| Command | Purpose |
|
|
284
|
+
| --- | --- |
|
|
285
|
+
| `jobdb-gitvend-secret-provider --help` | Show setup instructions. |
|
|
286
|
+
| `jobdb-gitvend-secret-provider validate --config issuer.yaml` | Validate local configuration and the signing key. |
|
|
287
|
+
| `jobdb-gitvend-secret-provider --config issuer.yaml` | Invocation registered with JobDB; JobDB launches this process. |
|
|
288
|
+
| `jobdb-gitvend-secret-provider version` | Show the installed version. |
|
|
289
|
+
| `jobdb-gitvend-secret-provider completion --help` | Set up shell completion. |
|
|
290
|
+
|
|
291
|
+
## Troubleshooting
|
|
292
|
+
|
|
293
|
+
| Symptom | What to check |
|
|
294
|
+
| --- | --- |
|
|
295
|
+
| Command is missing after npm install | Use a release that includes the plugin, check `PATH`, and ensure npm install scripts ran. After fixing download/install-script errors, run `npm rebuild @colony2/gitvend`. |
|
|
296
|
+
| Configuration validation fails | Check YAML field names, permission syntax, tenant IDs, and the signing-key path relative to the issuer file. |
|
|
297
|
+
| Neither token is returned | Check the verified tenant/principal, requested secret names, and job lease authority. Unknown names and unauthorized tenants are omitted. |
|
|
298
|
+
| Base token works but cell token is missing | Check `AppMetadata.repo`, `cell_metadata_field`, and whether a target allow rule matches that exact repository. |
|
|
299
|
+
| Gateway rejects a token | Check issuer, key ID, audience, signing key, clock tolerance, lifetime limits, and gateway owner allowlists. |
|
|
300
|
+
| Cell push cannot create a repository | The target rules need `c` plus a readable/writable branch. Check the gateway's `allow_creation`, upstream permissions, and creation settings. |
|
|
301
|
+
| Config changed but a worker still has old permissions | Obtain fresh secrets through JobDB. Already issued JWTs retain their permissions until expiry, and invalid replacements leave the last valid config active. |
|
|
302
|
+
|
|
303
|
+
For source builds, tests, or SDK integration, see the [developer guide](development.md).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# JWT permission syntax, version 1
|
|
2
|
+
|
|
3
|
+
Status: implemented version 1. This defines the compact permission strings used by the [Git gateway design](git-proxy-design.md). Permissions are signed inside the JWT and evaluated locally; no permission-name or role lookup is required.
|
|
4
|
+
|
|
5
|
+
## Shape
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
[!]host/namespace/repository[#ref-selector]:actions
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
github.com/fooorg/*:r
|
|
15
|
+
github.com/fooorg/blue*:rw
|
|
16
|
+
github.com/*:r
|
|
17
|
+
github.com/fooorg/red#(main|master|link):rw
|
|
18
|
+
github.com/fooorg/red#agents/agent-47/*:rw
|
|
19
|
+
github.com/fooorg/red#refs/tags/v*:r
|
|
20
|
+
github.com/fooorg/agent-47-*:c
|
|
21
|
+
!github.com/fooorg/*#(main|master):wd
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The JWT contains an array of strings, not a comma-separated string. Each string is one rule. The optional leading `!` makes a deny rule; otherwise it is an allow rule. Rule order has no meaning.
|
|
25
|
+
|
|
26
|
+
## Actions
|
|
27
|
+
|
|
28
|
+
| Letter | Permission |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `r` | Read objects by hash in the matching repository and discover/resolve matching refs. A ref selector restricts discovery, not hash reads. |
|
|
31
|
+
| `w` | Create or update matching refs, including history rewrites, subject to upstream restrictions. Does not delete refs or create repositories. |
|
|
32
|
+
| `d` | Delete matching refs. Does not delete the repository. |
|
|
33
|
+
| `c` | Create matching repositories using the route's server-controlled creation defaults (private by default). Does not itself grant reads or ref writes. |
|
|
34
|
+
|
|
35
|
+
Letters may be combined, such as `rw`, `rwd`, or `rwc`. Reject empty, duplicate or unknown action letters; issuers serialize in `rwdc` order. `w` and `d` do not imply `r`: a write also requires effective read/discovery permission on its destination, possibly from another rule. This permits a broad `:r` plus a narrow `:w` without repeating read grants. No separate force-push letter is claimed: distinguishing history rewrites from fast-forwards remains outside the initial streaming gate.
|
|
36
|
+
|
|
37
|
+
`c` is valid only without a `#ref-selector`, including in combinations and denies. Repository creation has no branch identity. Actual create-on-discovery additionally requires repository read permission and an effective `r`/`w` overlap for at least one possible branch. Creation never grants authority to write other refs.
|
|
38
|
+
|
|
39
|
+
## Repository matching
|
|
40
|
+
|
|
41
|
+
The host is an exact, lowercase configured Git authority, optionally with a configured port. No host wildcards, scheme, userinfo, query or client-chosen server URL. For example, `github.com` maps to the configured GitHub provider, API endpoint, credential and allowed owners. Multiple credential routes must be resolved deterministically by host/namespace; ambiguous routes fail configuration validation. A host-wide grant cannot enable an unconfigured owner or exceed its issuer's local authority ceiling.
|
|
42
|
+
|
|
43
|
+
Repository paths are canonical `namespace/repository` names, without the transport `.git` suffix. GitHub paths have an owner and a repository; later adapters may allow nested namespaces. Match against the adapter's canonical identity, never against the raw incoming URL. Hostnames are case-insensitive; repository case handling follows the adapter; branch and tag names are case-sensitive. Redirects and aliases are not permission matches.
|
|
44
|
+
|
|
45
|
+
| Pattern | Meaning |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `fooorg/red` | That exact repository. |
|
|
48
|
+
| `fooorg/*` | Every repository directly in `fooorg`. |
|
|
49
|
+
| `fooorg/blue*` | Repositories directly in `fooorg` whose names begin with `blue`, including `blue` itself. |
|
|
50
|
+
| `*/red` | Repository `red` in any one-level namespace. |
|
|
51
|
+
| `fooorg/**` | Repositories in `fooorg` and recursively nested namespaces, for adapters that support them. |
|
|
52
|
+
| `*` immediately after the host | Whole-host shorthand: all canonical repository paths on that configured host, at any namespace depth. Equivalent to `**` in this position. |
|
|
53
|
+
|
|
54
|
+
In ordinary path segments, `*` matches zero or more characters but never `/`. A standalone `**` path segment spans namespace levels; `fooorg/**/red` can match `fooorg/red` and `fooorg/team/red`. `**` is not allowed embedded in a segment. Every matched target must still be a valid repository identity; a glob never names a namespace itself as a repository.
|
|
55
|
+
|
|
56
|
+
The whole-host `host/*` form is an explicit shorthand, not a general rule that ordinary path stars cross slashes. It supports the requested `github.com/*:r` spelling without broadening `github.com/fooorg/*` to unrelated namespaces.
|
|
57
|
+
|
|
58
|
+
## Ref selectors
|
|
59
|
+
|
|
60
|
+
| Selector | Meaning |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| No `#` | Whole repository: all supported branches and tags for `r`, `w` and `d`; repository-level scope for `c`. |
|
|
63
|
+
| `#main` | Exactly `refs/heads/main`. |
|
|
64
|
+
| `#(main|master|link)` | Those three branch names. |
|
|
65
|
+
| `#agents/agent-47/*` | Branches with that prefix, including nested suffixes. |
|
|
66
|
+
| `#*` | All branches, not tags. |
|
|
67
|
+
| `#refs/heads/release/*` | Explicit fully qualified branch selector. |
|
|
68
|
+
| `#refs/tags/v*` | Tags whose names begin with `v`. |
|
|
69
|
+
|
|
70
|
+
Without an explicit `refs/heads/` or `refs/tags/` prefix, the selector names a branch. Ref wildcards match the **whole branch or tag name**, so `*` may span `/` here. `**` has no additional meaning in ref selectors and is rejected to avoid competing spellings. This intentionally differs from namespace path segments: the slash in a branch name is part of the branch name, not a repository boundary.
|
|
71
|
+
|
|
72
|
+
Only branch and tag namespaces are supported initially. Explicit selectors beginning `refs/` must use a literal supported namespace prefix; `#refs/*` and selectors for provider-managed refs are invalid. To match a branch literally named `refs/tags/example`, use `#refs/heads/refs/tags/example`.
|
|
73
|
+
|
|
74
|
+
An unrestricted repository `:rw` therefore includes tag creation/update. Issuers that intend branch-only writes must use `#*:rw`, and branch-restricted work should use a narrower selector. Repository-wide grants deliberately opt into broader scope; tags are otherwise hidden and unwritable without a matching grant. `HEAD` is handled by the gateway's symbolic-ref visibility rules, not as a separate writable ref.
|
|
75
|
+
|
|
76
|
+
## Alternatives and literal characters
|
|
77
|
+
|
|
78
|
+
`(a|b|c)` means a choice of glob fragments, not a regular expression. It can appear inside a namespace/repository segment or ref selector. For example, `(blue|green)*` and `#(main|release/*)` are valid. Repository alternatives cannot contain `/`; use a separate rule for different path structures. Ref alternatives may contain `/`.
|
|
79
|
+
|
|
80
|
+
All matches cover the entire canonical field. `red` does not match `infra-red`; `main` does not match `main-old`; `.` is literal. There are no regex operators, character classes, nested groups or implicit substring matches. Reject empty alternatives, unmatched parentheses, unescaped bare `|`, `?`, `[` or `]`, and trailing escapes.
|
|
81
|
+
|
|
82
|
+
Backslash escapes grammar characters for literal matching, including `(`, `)`, `|`, `#`, `:`, `!` and backslash itself. Parse delimiters while respecting escapes; use the action-separating colon after the resource/ref portion, not a colon in a host port. JSON must itself escape backslashes: a selector for a branch literally named `topic(test)` is encoded as `"...#topic\\(test\\):r"`. Escaping does not legalize characters prohibited by the forge or Git. Do not percent-decode permission strings or interpret them as URLs.
|
|
83
|
+
|
|
84
|
+
## Combining rules
|
|
85
|
+
|
|
86
|
+
For each internal action, allow if at least one matching allow grants it and no matching deny removes it. Default deny. Specificity and ordering do not override a deny. Apply configured issuer/host/owner limits and gateway hard restrictions in addition; upstream permissions remain a final independent restriction.
|
|
87
|
+
|
|
88
|
+
| Internal action | Allow condition | Deny condition |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `repo.read` | Any repository-matching `r` rule, with or without a ref selector. | A repository-matching `r` deny **without** a ref selector. |
|
|
91
|
+
| `ref.discover` | A repository/ref-matching `r` allow. | A repository/ref-matching `r` deny. |
|
|
92
|
+
| `ref.create`, `ref.update` | A repository/ref-matching `w` allow. | A repository/ref-matching `w` deny. |
|
|
93
|
+
| `ref.delete` | A repository/ref-matching `d` allow. | A repository/ref-matching `d` deny. |
|
|
94
|
+
| `repo.create` | A repository-matching `c` allow. | A repository-matching `c` deny. |
|
|
95
|
+
|
|
96
|
+
Reading a named ref requires both `repo.read` and `ref.discover`. A ref mutation requires those two plus the corresponding write action. An allowed direct hash fetch only needs `repo.read`. A repository-wide `!…:r` consequently blocks all ordinary Git use of that repository, including writes that depend on read access. A branch-scoped `!…#secret/*:r` hides matching refs and blocks writes to them, but **does not** prevent fetching their objects by known hash. That is the previously selected read model, not a parser exception.
|
|
97
|
+
|
|
98
|
+
Examples of overlap:
|
|
99
|
+
|
|
100
|
+
- `github.com/fooorg/*:r` plus `github.com/fooorg/blue*:w`: discover all refs throughout the organization; create/update refs only in `blue*` repositories.
|
|
101
|
+
- `github.com/fooorg/red#main:r` plus `github.com/fooorg/red#agents/47/*:rw`: discover main and this agent's branches; only the agent branches are writable; hash reads remain repository-wide.
|
|
102
|
+
- `github.com/*:r` plus `github.com/fooorg/red#main:rw`: all configured GitHub repositories remain readable/discoverable. The narrower rule adds main writes; it does not narrow the broad read grant.
|
|
103
|
+
- `github.com/fooorg/*:rwd` plus `!github.com/fooorg/*#(main|master):wd`: keep those branches readable while preventing creation, updates and deletion, even if another allow grants them.
|
|
104
|
+
|
|
105
|
+
Grants are evaluated against the current repository/ref name at request time. They cover future matching repositories and branches, subject to configured owner access. If a repository is deleted and recreated under the same name, the same matching grant still applies. There is no repository enrollment or ID pinning; the grant contains only `v` and `permissions`.
|
|
106
|
+
|
|
107
|
+
## JWT example
|
|
108
|
+
|
|
109
|
+
The following is the custom claim fragment; the full token also requires the identity, audience, lifetime and signature fields from the main design:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"grant": {
|
|
114
|
+
"v": 1,
|
|
115
|
+
"permissions": [
|
|
116
|
+
"github.com/fooorg/red#main:r",
|
|
117
|
+
"github.com/fooorg/red#agents/47/*:rw",
|
|
118
|
+
"github.com/fooorg/agent-47-*:rc",
|
|
119
|
+
"github.com/fooorg/agent-47-*#agents/47/*:rw",
|
|
120
|
+
"!github.com/fooorg/*#(main|master):wd"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This can read main, discover/write the agent's branches, and provision matching scratch repositories. Creation uses the route's creation defaults, including visibility (private by default). The `rc` rule also deliberately grants all-ref discovery in those scratch repositories; remove `r` there and put it only on selected branches if narrower discovery is desired.
|
|
127
|
+
|
|
128
|
+
## Implementation and acceptance
|
|
129
|
+
|
|
130
|
+
Compile the signed strings into an immutable matcher after JWT verification and schema validation. Use a dedicated lexer/parser and bounded glob/alternative automata; do not feed raw token patterns into a regex engine or shell, and do not expand alternatives into an unbounded Cartesian product. Bound token bytes, rule count, pattern length and group count. Cache compiled grants by verified token digest, never just `sub` or a client-chosen `jti`, with expiry and relevant local configuration revision in the cache validity conditions.
|
|
131
|
+
|
|
132
|
+
Derive internal action checks from the table above. Apply the same canonicalization and matcher to ref discovery, direct receive-pack commands and provisioning. Creation eligibility is a nonempty language intersection: permitted branch `r` and `w` patterns minus denies must overlap. Compute that with bounded automata; do not sample candidate branch names or ignore denies. If computation exceeds configured complexity bounds, reject the grant/creation eligibility with a clear error.
|
|
133
|
+
|
|
134
|
+
Add a policy-explanation command for issuer/operator use that accepts a grant and concrete repository/ref/action and returns matching allows/denies and the decision. It should also flag obviously broad repository or host grants that make narrower grants redundant, without changing their meaning.
|
|
135
|
+
|
|
136
|
+
Acceptance cases must cover whole-host shorthand, segment boundaries, nested namespaces, branch slashes, alternatives, escapes, case rules, tags versus branches, overlapping grants/denies, denied branch hash fetches, unknown syntax, duplicate action letters, future matching names, recreated repository names and creation eligibility. Reject a malformed rule by rejecting the entire JWT grant, never by silently dropping that rule.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
listen: 127.0.0.1:8443
|
|
2
|
+
tls_cert: /run/secrets/gitvend.crt
|
|
3
|
+
tls_key: /run/secrets/gitvend.key
|
|
4
|
+
audience: gitvend
|
|
5
|
+
keys:
|
|
6
|
+
- id: orchestrator-1
|
|
7
|
+
issuer: orchestrator
|
|
8
|
+
public_key_file: var/signing.pub
|
|
9
|
+
owners:
|
|
10
|
+
github.com:
|
|
11
|
+
- fooorg
|
|
12
|
+
providers:
|
|
13
|
+
- alias: github
|
|
14
|
+
host: github.com
|
|
15
|
+
kind: github
|
|
16
|
+
git_base_url: https://github.com
|
|
17
|
+
api_base_url: https://api.github.com
|
|
18
|
+
allowed_owners:
|
|
19
|
+
- fooorg
|
|
20
|
+
credential:
|
|
21
|
+
kind: token
|
|
22
|
+
secret_ref: env:GITHUB_TOKEN
|
|
23
|
+
allow_creation: true
|
|
24
|
+
creation_defaults:
|
|
25
|
+
visibility: private
|
|
26
|
+
has_issues: false
|
|
27
|
+
has_projects: false
|
|
28
|
+
has_wiki: false
|
|
29
|
+
has_pull_requests: false
|
|
30
|
+
denies:
|
|
31
|
+
- '!github.com/fooorg/*#(main|master):wd'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
issuer: jobdb
|
|
2
|
+
audience: gitvend
|
|
3
|
+
key_id: jobdb
|
|
4
|
+
signing_key_file: signing.key
|
|
5
|
+
policy_revision: "1"
|
|
6
|
+
max_token_bytes: 4096
|
|
7
|
+
max_lifetime_seconds: 900
|
|
8
|
+
gateway_leeway_seconds: 30
|
|
9
|
+
clock_margin_seconds: 1
|
|
10
|
+
cell_metadata_field: repo
|
|
11
|
+
tenants:
|
|
12
|
+
example:
|
|
13
|
+
base_permissions:
|
|
14
|
+
- github.com/*:r
|
|
15
|
+
target_permissions:
|
|
16
|
+
- github.com/colony-2/*:rwc
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colony2/gitvend",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Git HTTPS gateway with signed repository and branch permissions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/colony-2/gitvend",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"security"
|
|
25
25
|
],
|
|
26
26
|
"bin": {
|
|
27
|
-
"gitvend": "bin/cli.js"
|
|
27
|
+
"gitvend": "bin/cli.js",
|
|
28
|
+
"jobdb-gitvend-secret-provider": "bin/jobdb-gitvend-secret-provider"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"postinstall": "node scripts/postinstall.js",
|
|
@@ -46,6 +47,8 @@
|
|
|
46
47
|
"scripts/",
|
|
47
48
|
"README.md",
|
|
48
49
|
"THIRD_PARTY_NOTICES.md",
|
|
49
|
-
"LICENSES/"
|
|
50
|
+
"LICENSES/",
|
|
51
|
+
"docs/",
|
|
52
|
+
"examples/"
|
|
50
53
|
]
|
|
51
54
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -44,7 +44,8 @@ async function install({
|
|
|
44
44
|
const { name, assetName, baseUrl } = releaseAsset(pkg, platform, arch);
|
|
45
45
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`));
|
|
46
46
|
const vendorDir = path.join(packageRoot, "vendor");
|
|
47
|
-
|
|
47
|
+
const binaries = [name, "jobdb-gitvend-secret-provider"];
|
|
48
|
+
const staged = [];
|
|
48
49
|
try {
|
|
49
50
|
const archivePath = path.join(tempDir, assetName);
|
|
50
51
|
const checksumsPath = path.join(tempDir, "checksums.txt");
|
|
@@ -53,17 +54,26 @@ async function install({
|
|
|
53
54
|
await downloadFile(`${baseUrl}/${assetName}`, archivePath);
|
|
54
55
|
await downloadFile(`${baseUrl}/checksums.txt`, checksumsPath, { maxBytes: 1024 * 1024 });
|
|
55
56
|
verifyChecksum(archivePath, checksumsPath, assetName);
|
|
56
|
-
// Extract
|
|
57
|
-
execFileSync("tar", ["-xzf", archivePath, "-C", extractDir,
|
|
58
|
-
const
|
|
59
|
-
|
|
57
|
+
// Extract and validate both commands before replacing either installed binary.
|
|
58
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", extractDir, ...binaries], { stdio: "pipe" });
|
|
59
|
+
for (const name of binaries) {
|
|
60
|
+
if (!fs.lstatSync(path.join(extractDir, name)).isFile()) {
|
|
61
|
+
throw new Error("Release binary must be a regular file");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
60
64
|
fs.mkdirSync(vendorDir, { recursive: true });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
for (const name of binaries) {
|
|
66
|
+
// Launch the plugin directly: JobDB restricts PATH and owns its process.
|
|
67
|
+
const destinationDir = name === "gitvend" ? vendorDir : path.join(packageRoot, "bin");
|
|
68
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
69
|
+
const temporary = path.join(destinationDir, `.${name}-${crypto.randomBytes(8).toString("hex")}`);
|
|
70
|
+
staged.push([temporary, path.join(destinationDir, name)]);
|
|
71
|
+
fs.copyFileSync(path.join(extractDir, name), temporary, fs.constants.COPYFILE_EXCL);
|
|
72
|
+
fs.chmodSync(temporary, 0o755);
|
|
73
|
+
}
|
|
74
|
+
for (const [temporary, destination] of staged) fs.renameSync(temporary, destination);
|
|
65
75
|
} finally {
|
|
66
|
-
|
|
76
|
+
for (const [temporary] of staged) fs.rmSync(temporary, { force: true });
|
|
67
77
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
68
78
|
}
|
|
69
79
|
}
|