Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ 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.

By default an `[[installation-policy]]` token is scoped to the requested repository. Set
`all-repositories = true` to mint an installation-wide token instead — useful for
organization-level resources (such as org-owned packages) that a single-repository token
cannot read. `repository`/`repositories` is still required for the authorization match, and
`[installation-policy.permissions]` still applies, so the token stays narrow.

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.

```sh
Expand Down
21 changes: 19 additions & 2 deletions idcat.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ allow-self-access = true
# 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
# and are still forwarded; GitHub is authoritative. The token is always scoped
# to the requested repository, as for any installation-policy.
# and are still forwarded; GitHub is authoritative. The token is scoped to the
# requested repository unless `all-repositories = true` (see below).
#
# Example — a CI workflow may mint a read-only token for its own repo:
[[installation-policy]]
Expand All @@ -113,3 +113,20 @@ allow-self-access = true
[installation-policy.permissions]
contents = "read"
pull_requests = "read"

# `all-repositories = true` mints an installation-wide token (all repositories)
# instead of one scoped to the requested repository. `repository`/`repositories`
# is still required and used for the authorization match. Combine with
# `[installation-policy.permissions]` to keep the token narrow. Use this for
# organization-level resources (e.g. org-owned packages) that a single-repository
# token cannot read.
#
# Example — a CI workflow may mint an org-wide read-only packages token:
[[installation-policy]]
github-app = "deployments"
repository = "myorg/*"
role = "github-workflow"
all-repositories = true

[installation-policy.permissions]
packages = "read"
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ pub struct InstallationPolicyConfig {
pub required_claims: BTreeMap<String, authzoo::ClaimRequirement>,
pub allow_self_access: bool,
pub permissions: BTreeMap<String, String>,
// When true, mint an installation-wide token (all repositories) instead of one scoped to the
// requested repository. `repository`/`repositories` is still required and used for the
// authorization match. Useful for organization-level resources (e.g. org-owned packages) that a
// single-repository token cannot read.
pub all_repositories: bool,
}

#[derive(Deserialize)]
Expand All @@ -102,6 +107,8 @@ struct RawInstallationPolicyConfig {
// Keys are GitHub permission names (snake_case), not kebab-case.
#[serde(default)]
permissions: BTreeMap<String, String>,
#[serde(default)]
all_repositories: bool,
}

impl<'de> Deserialize<'de> for InstallationPolicyConfig {
Expand Down Expand Up @@ -132,6 +139,7 @@ impl<'de> Deserialize<'de> for InstallationPolicyConfig {
required_claims: raw.required_claims,
allow_self_access: raw.allow_self_access,
permissions: raw.permissions,
all_repositories: raw.all_repositories,
})
}
}
Expand Down
42 changes: 41 additions & 1 deletion src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,13 @@ impl AppState {
)
});
if let Some(installation_policy) = installation_policy_match {
let repositories = if installation_policy.all_repositories {
RepoScope::All
} else {
RepoScope::OnlyRequested
};
return Ok(TokenScope {
repositories: RepoScope::OnlyRequested,
repositories,
permissions: installation_policy.permissions.clone(),
});
}
Expand Down Expand Up @@ -380,6 +385,7 @@ mod tests {
required_claims,
allow_self_access: false,
permissions: BTreeMap::new(),
all_repositories: false,
}],
);
state.token_validator = TokenValidator::new(vec![github_workflow_role()], false).unwrap();
Expand Down Expand Up @@ -414,6 +420,7 @@ mod tests {
required_claims,
allow_self_access: false,
permissions: BTreeMap::new(),
all_repositories: false,
}],
);
state.token_validator = TokenValidator::new(vec![github_workflow_role()], false).unwrap();
Expand Down Expand Up @@ -527,6 +534,38 @@ mod tests {
);
}

#[test]
fn authorize_github_app_returns_all_repos_when_installation_policy_opts_in() {
let mut policy = workflow_self_scoping_policy();
policy.all_repositories = true;
policy
.permissions
.insert("packages".to_string(), "read".to_string());
let mut state = test_state_with_installation_policies(
vec![GithubAppConfig {
name: "default".to_string(),
app_id: 42,
secret_key: "private-key.pem".to_string(),
webhook_target: None,
webhook_validation_secret_file: None,
allowed_roles: Vec::new(),
}],
vec![policy],
);
state.token_validator = TokenValidator::new(vec![github_workflow_role()], false).unwrap();

let token = github_workflow_token("myorg/alfa");
let github_app = state.github_app("default").unwrap().clone();
let scope = state
.authorize_github_app(&github_app, "myorg/alfa", Some(&token))
.unwrap();
assert_eq!(scope.repositories, RepoScope::All);
assert_eq!(
scope.permissions.get("packages").map(String::as_str),
Some("read")
);
}

fn workflow_self_scoping_policy() -> InstallationPolicyConfig {
InstallationPolicyConfig {
github_app: "default".to_string(),
Expand All @@ -535,6 +574,7 @@ mod tests {
required_claims: BTreeMap::new(),
allow_self_access: true,
permissions: BTreeMap::new(),
all_repositories: false,
}
}

Expand Down