Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ github-app = "deployments"
repositories = ["myorg/alfa", "myorg/beta"]
role = "github-workflow"
required-claims = { repository = "myorg/gamma" }
permissions = { contents = "read" }
```

See `idcat.toml.example` for a fuller configuration with multiple roles and GitHub Apps.
Expand All @@ -55,8 +56,10 @@ combination, with additional required token claims. Set either `repository = "ow
or `repositories = ["owner/name", "owner/other"]`; the request may match any configured
repository pattern. For example, a request for `deployments` on `myorg/alfa` can require
the token to satisfy `github-workflow` and also
carry `repository = "myorg/gamma"`. App-level `allowed-roles` still grant access to every
repository installation for that GitHub App.
carry `repository = "myorg/gamma"`. Every `[[installation-policy]]` must also declare a
non-empty `permissions` table naming the GitHub permissions the minted token gets, so a
policy can never hand out the App's full installation permissions. App-level
`allowed-roles` still grant access to every repository installation for that GitHub App.

Mount the private keys as files. For example, in Kubernetes this could be a Secret volume mounted at `private-key-directory`, but `idcat` only reads files from the filesystem.

Expand Down
14 changes: 12 additions & 2 deletions idcat.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ role = "github-workflow"
[installation-policy.required-claims]
repository = "myorg/gamma"

[installation-policy.permissions]
contents = "read"

[[installation-policy]]
github-app = "deployments"
repositories = ["myorg/beta", "myorg/delta"]
Expand All @@ -77,6 +80,9 @@ role = "github-workflow"
[installation-policy.required-claims]
repository = "myorg/epsilon"

[installation-policy.permissions]
contents = "write"

# `repository` and each `repositories` entry accepts a glob where `*` matches
# any characters (including `/`); so `"myorg/*"` matches any repo under
# `myorg/`, `"*"` matches any repo at all, and a literal like `"myorg/alfa"`
Expand All @@ -95,8 +101,12 @@ repository = "myorg/*"
role = "github-workflow"
allow-self-access = true

# A `[installation-policy.permissions]` table down-scopes what the minted
# token can DO. Absent/empty means the App's full installation permissions.
[installation-policy.permissions]
contents = "read"

# The `[installation-policy.permissions]` table is required and must name at
# least one permission — it down-scopes what the minted token can DO, so
# there is no way to ask for the App's full installation permissions.
# Keys are GitHub permission names in snake_case (forwarded to GitHub verbatim
# — deliberately NOT kebab-case like the rest of this config); values are
# read/write/admin. Unrecognised names or values only emit a startup warning
Expand Down
68 changes: 64 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ struct RawInstallationPolicyConfig {
#[serde(default)]
allow_self_access: bool,
// Keys are GitHub permission names (snake_case), not kebab-case.
#[serde(default)]
permissions: BTreeMap<String, String>,
}

Expand Down Expand Up @@ -331,6 +330,14 @@ impl Config {
);
}
}
if installation_policy.permissions.is_empty() {
anyhow::bail!(
"installation-policy for github-app '{}' repository '{}' role '{}' must define at least one permission",
installation_policy.github_app,
installation_policy.repositories_label(),
installation_policy.role
);
}
for (name, value) in &installation_policy.permissions {
if !known_github_permissions().contains(name.as_str()) {
warn!(
Expand Down Expand Up @@ -418,6 +425,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repository = "myorg/*"
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
environment = "production"
Expand Down Expand Up @@ -449,6 +457,7 @@ github-app = "deployments"
repository = "myorg/*"
role = "github-workflow"
allow-self-access = true
permissions = { contents = "read" }
"#,
)
.unwrap();
Expand Down Expand Up @@ -477,6 +486,7 @@ github-app = "deployments"
repository = "*"
role = "github-workflow"
allow-self-access = true
permissions = { contents = "read" }
"#,
)
.unwrap();
Expand Down Expand Up @@ -505,6 +515,7 @@ github-app = "deployments"
repository = "myorg/alfa"
role = "github-workflow"
allow-self-access = true
permissions = { contents = "read" }
"#,
)
.unwrap();
Expand Down Expand Up @@ -535,6 +546,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repositories = ["myorg/alfa", "myorg/bravo"]
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/gamma"
Expand Down Expand Up @@ -570,6 +582,7 @@ github-app = "deployments"
repository = "myorg/alfa"
repositories = ["myorg/bravo"]
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/gamma"
Expand Down Expand Up @@ -603,6 +616,7 @@ secret-key = "private-key.pem"
[[installation-policy]]
github-app = "deployments"
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/gamma"
Expand Down Expand Up @@ -637,6 +651,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repositories = []
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/gamma"
Expand Down Expand Up @@ -672,6 +687,7 @@ github-app = "deployments"
repository = "myorg/*"
role = "github-workflow"
allow-self-access = true
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/alfa"
Expand Down Expand Up @@ -706,6 +722,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repository = "myorg/alfa"
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = ["myorg/alfa", "myorg/bravo"]
Expand Down Expand Up @@ -747,6 +764,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repository = "owner-only-no-slash"
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/gamma"
Expand Down Expand Up @@ -860,7 +878,40 @@ pull_requests = "write"
}

#[test]
fn installation_policy_permissions_default_empty_when_absent() {
fn rejects_installation_policy_without_permissions() {
let error = toml::from_str::<Config>(
r#"
[[role]]
name = "github-workflow"
audience = "idcat"
issuer = "https://token.actions.githubusercontent.com"
validation-key = "shared-secret"
algorithms = ["HS256"]

[[github-app]]
name = "deployments"
app-id = 42
secret-key = "private-key.pem"

[[installation-policy]]
github-app = "deployments"
repository = "myorg/alfa"
role = "github-workflow"

[installation-policy.required-claims]
repository = "myorg/gamma"
"#,
)
.expect_err("installation-policy without permissions must be rejected");

assert!(
error.to_string().contains("missing field `permissions`"),
"expected missing permissions error, got: {error}"
);
}

#[test]
fn rejects_installation_policy_with_empty_permissions() {
let config: Config = toml::from_str(
r#"
[[role]]
Expand All @@ -879,15 +930,22 @@ secret-key = "private-key.pem"
github-app = "deployments"
repository = "myorg/alfa"
role = "github-workflow"
permissions = {}

[installation-policy.required-claims]
repository = "myorg/gamma"
"#,
)
.unwrap();

let policy = &config.installation_policies[0];
assert!(policy.permissions.is_empty());
let error = config
.validate(false)
.expect_err("empty permissions table must be rejected");

assert_eq!(
error.to_string(),
"installation-policy for github-app 'deployments' repository 'myorg/alfa' role 'github-workflow' must define at least one permission"
);
}

#[test]
Expand Down Expand Up @@ -1144,6 +1202,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repository = "myorg/alfa"
role = "github-workflow"
permissions = { contents = "read" }

[installation-policy.required-claims]
repository = "myorg/gamma"
Expand Down Expand Up @@ -1221,6 +1280,7 @@ secret-key = "private-key.pem"
github-app = "deployments"
repository = "myorg/alfa"
role = "github-workflow"
permissions = { contents = "read" }
"#,
)
.unwrap();
Expand Down