Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

A workspace token for all of its workspace's repositories is held to that workspace. Identity resolved such a token with no reach (only a token for chosen repositories had one), so the API's resource-owner check never applied to it: with issues or pull-requests write it could open issues, comment or open pull requests on any other workspace's public repository as the workspace, which the docs said it could not. apply_reach now gives every workspace token a reach of its own workspace, all of its repositories or the ones chosen (workspace_token_reach), the shape a person's token made for one workspace already has; outside it, it reads public repositories as anyone may and changes nothing (token:resource-owner). Inside its workspace nothing changes. Checked: cargo test -p g1t-identity --lib (177, new: a workspace token never writes outside its workspace, for all or chosen repositories; one for all covers its own repositories only) and its wasm32 check.

syntaqxcommitted Parent2a24415Browse files
1 file+46−50/1 viewed
+46−5
254254 status: Option<String>,
255255 }
256256
257+/// What a workspace's own token reaches: that workspace only, all of its
258+/// repositories or the ones chosen. Never none: without a reach a token
259+/// is not held to a workspace, and could open issues or comment on any
260+/// public repository as the workspace.
261+fn workspace_token_reach(slug: String, selection: RepositorySelection, repo_ids: Vec<String>) -> TokenReach {
262+ let selection = if selection == RepositorySelection::Selected { selection } else { RepositorySelection::All };
263+ TokenReach { workspace: Some(slug), repositories: selection, repo_ids }
264+}
265+
257266 impl Identity {
258267 /// Cuts `user`, resolved from the token `token_id`, down to what the
259268 /// token reaches. `personal` is whether it is a person's token (not a
266275 // A workspace's own token: Write on its repositories, or Admin
267276 // when it holds Repositories: admin; the selected ones only
268277 // when it has a selection.
269− let selected = facts.selection() == RepositorySelection::Selected;
270− let repo_ids = if selected { self.token_repo_ids(token_id).await? } else { Vec::new() };
278+ let selection = facts.selection();
279+ let repo_ids = if selection == RepositorySelection::Selected { self.token_repo_ids(token_id).await? } else { Vec::new() };
271280 let slug = user.username.clone();
272281 if let Some(access) = user.token.as_deref_mut() {
273282 access.admin = facts.admin.is_some_and(|admin| admin >= 1.0);
274− if selected {
275− access.reach = Some(TokenReach { workspace: Some(slug), repositories: RepositorySelection::Selected, repo_ids });
276− }
283+ access.reach = Some(workspace_token_reach(slug, selection, repo_ids));
277284 }
278285 return Ok(());
279286 }
12661273
12671274 const DAY: u64 = 86_400_000;
12681275
1276+ fn workspace_token(selection: RepositorySelection) -> g1t_contracts::scopes::TokenAccess {
1277+ g1t_contracts::scopes::TokenAccess {
1278+ scopes: Some(vec!["repo:read".into(), "code:write".into(), "issues:write".into()]),
1279+ reach: Some(workspace_token_reach("acme".into(), selection, vec!["rep_1".into()])),
1280+ ..Default::default()
1281+ }
1282+ }
1283+
1284+ #[test]
1285+ fn a_workspace_token_never_writes_outside_its_workspace() {
1286+ use g1t_contracts::scopes::decide;
1287+ for selection in [RepositorySelection::All, RepositorySelection::Selected] {
1288+ let token = workspace_token(selection);
1289+ let issue = |repo: &str| serde_json::json!({ "repo": repo, "title": "spam" });
1290+ let elsewhere = decide(&token, "create_issue", &issue("other/public"));
1291+ assert!(!elsewhere.allowed, "{selection:?}");
1292+ assert_eq!(elsewhere.rule, "token:resource-owner");
1293+ assert!(decide(&token, "create_issue", &issue("acme/rocket")).allowed, "{selection:?}");
1294+ // Reading a public repository elsewhere is as anyone may.
1295+ assert!(decide(&token, "get_repo", &serde_json::json!({ "repo": "other/public" })).allowed, "{selection:?}");
1296+ }
1297+ }
1298+
1299+ #[test]
1300+ fn a_workspace_token_for_all_repositories_covers_its_own_only() {
1301+ let all = workspace_token(RepositorySelection::All);
1302+ assert!(all.covers_repo("rep_9", "acme"));
1303+ assert!(all.covers_repo("rep_9", "ACME"));
1304+ assert!(!all.covers_repo("rep_9", "other"));
1305+ let selected = workspace_token(RepositorySelection::Selected);
1306+ assert!(selected.covers_repo("rep_1", "acme"));
1307+ assert!(!selected.covers_repo("rep_9", "acme"));
1308+ }
1309+
12691310 #[test]
12701311 fn asks_credential_skips_the_rules_for_held_tokens_but_not_revoking() {
12711312 let closed = TokenPolicy { allow_tokens_for_this_workspace: false, ..TokenPolicy::default() };

Comments

Sign in to comment.