diff --git a/tests/local_agent_grpc.rs b/tests/local_agent_grpc.rs --- a/tests/local_agent_grpc.rs +++ b/tests/local_agent_grpc.rs @@ -1,499 +1,548 @@ -#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] - -use std::error::Error; - -use chrono::{Duration, Utc}; -use ssh_key::{PublicKey, public::Ed25519PublicKey}; -use syncode_identity_api_grpc::wire::{ - AddLocalAgentSshKeyRequest, CheckCapabilityRequest, CreateAccessTokenRequest, - CreateLocalAgentRequest, EnrollLocalAgentRequest, IssueLocalAgentRepositoryApiTokenRequest, - IssueLocalAgentRepositoryTokenRequest, IssueUserRepositoryApiTokenRequest, - IssueUserRepositoryTokenRequest, LocalAgentHeartbeatRequest, PrincipalKind, - ResolveSshKeyRequest, ResourceKind, RevokeLocalAgentRequest, RevokeLocalAgentSshKeyRequest, - ValidateSessionRequest, -}; -use syncode_identity_api_grpc::{ - IdentityServer, IdentityService, LocalAgentServer, LocalAgentService, -}; -use syncode_identity_model::{CoarsePreset, GrantPrincipalKind, Resource}; -use syncode_identity_storage::Postgres; -use tonic::Request; -use uuid::Uuid; - -type TestResult = Result>; - -fn url() -> String { - std::env::var("SYNCODE_IDENTITY_TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgres:///syncode_identity_test".to_owned()) -} - -fn local_agent_server(store: &Postgres) -> TestResult { - let mailer = syncode_identity_mailer::Mailer::new(syncode_identity_mailer::Config { - host: "smtp.example.invalid".to_owned(), - port: 587, - username: String::new(), - password: String::new(), - from: "test@example.invalid".to_owned(), - from_name: "Test".to_owned(), - })?; - let forge = - syncode_identity_gitea_client::GiteaClient::new(syncode_identity_gitea_client::Config { - internal_url: "https://forge.test.invalid".to_owned(), - internal_token: "test".to_owned(), - }); - let settings: std::sync::Arc = - std::sync::Arc::new(syncode_identity_application::SettingsApplication::new( - std::sync::Arc::new(store.clone()), - std::sync::Arc::new(forge.clone()), - std::sync::Arc::new(forge), - std::sync::Arc::new(mailer), - "https://identity.test.invalid".to_owned(), - )); - let local_agents: std::sync::Arc = - std::sync::Arc::new(syncode_identity_application::LocalAgentApplication::new( - std::sync::Arc::new(store.clone()), - )); - Ok(LocalAgentServer::new(local_agents, settings)) -} - -#[tokio::test] -async fn access_token_never_exceeds_its_owner() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let local_agents = local_agent_server(&store)?; - let identity = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( - store.clone(), - )), - )); - let username = format!("token-owner-{}", Uuid::new_v4()); - let owner = store.insert_user(&username).await?; - let repository = store - .seed_repository("user", owner, "token-repository", "private") - .await?; - store - .insert_grant( - GrantPrincipalKind::User, - owner, - Resource::repository(repository.into()), - &CoarsePreset::Write.expand(), - owner, - ) - .await?; - let session_token = format!("test-token-{}", Uuid::new_v4()); - store - .issue_session(owner, &session_token, Utc::now() + Duration::hours(1)) - .await?; - - let token = local_agents - .create_access_token(Request::new(CreateAccessTokenRequest { - session_token, - name: "automation".to_owned(), - capabilities: vec!["pr:open".to_owned(), "pr:merge".to_owned()], - expires_in_seconds: 30 * 24 * 60 * 60, - })) - .await? - .into_inner() - .token; - assert!(token.starts_with("syn_pat_")); - let validated = identity - .validate_session(Request::new(ValidateSessionRequest { - session_token: token, - })) - .await? - .into_inner(); - assert_eq!(validated.audience, "api"); - assert_eq!(validated.resource_kind, ResourceKind::Unspecified as i32); - let open = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated.principal_id.clone(), - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "pr:open".to_owned(), - })) - .await? - .into_inner(); - assert!(open.allowed); - let merge = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated.principal_id, - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "pr:merge".to_owned(), - })) - .await? - .into_inner(); - assert!(!merge.allowed); - Ok(()) -} - -#[tokio::test] -async fn local_agent_is_fenced_restricted_and_revoked() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let local_agents = local_agent_server(&store)?; - let identity = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( - store.clone(), - )), - )); - let username = format!("agent-owner-{}", Uuid::new_v4()); - let owner = store.insert_user(&username).await?; - let repository = store - .seed_repository("user", owner, "agent-repository", "private") - .await?; - let other_repository = store - .seed_repository("user", owner, "other-agent-repository", "private") - .await?; - store - .insert_grant( - GrantPrincipalKind::User, - owner, - Resource::repository(repository.into()), - &CoarsePreset::Admin.expand(), - owner, - ) - .await?; - store - .insert_grant( - GrantPrincipalKind::User, - owner, - Resource::repository(other_repository.into()), - &CoarsePreset::Admin.expand(), - owner, - ) - .await?; - let session_token = format!("test-token-{}", Uuid::new_v4()); - store - .issue_session(owner, &session_token, Utc::now() + Duration::hours(1)) - .await?; - let mut public_key_bytes = [0_u8; 32]; - public_key_bytes[..16].copy_from_slice(Uuid::new_v4().as_bytes()); - public_key_bytes[16..].copy_from_slice(Uuid::new_v4().as_bytes()); - let public_key = PublicKey::from(Ed25519PublicKey(public_key_bytes)).to_openssh()?; - - let agent_id = local_agents - .create(Request::new(CreateLocalAgentRequest { - session_token: session_token.clone(), - owner: username.clone(), - name: "codex".to_owned(), - definition: "codex".to_owned(), - restriction: vec!["write".to_owned()], - })) - .await? - .into_inner() - .agent_id; - let enrolled = local_agents - .enroll(Request::new(EnrollLocalAgentRequest { - session_token: session_token.clone(), - owner: username.clone(), - name: "codex".to_owned(), - instance_host: "workstation.test".to_owned(), - })) - .await? - .into_inner(); - assert_eq!(enrolled.agent_id, agent_id); - - let heartbeat = local_agents - .heartbeat(Request::new(LocalAgentHeartbeatRequest { - agent_id: agent_id.clone(), - instance_host: "workstation.test".to_owned(), - lease_term: enrolled.lease_term, - fencing_token: enrolled.fencing_token.clone(), - credential: enrolled.credential.clone(), - })) - .await? - .into_inner(); - local_agents - .heartbeat(Request::new(LocalAgentHeartbeatRequest { - agent_id: agent_id.clone(), - instance_host: "workstation.test".to_owned(), - lease_term: enrolled.lease_term, - fencing_token: enrolled.fencing_token.clone(), - credential: heartbeat.credential.clone(), - })) - .await?; - - let user_repository_token = local_agents - .issue_user_repository_token(Request::new(IssueUserRepositoryTokenRequest { - session_token: session_token.clone(), - repository_owner: username.clone(), - repository_name: "agent-repository".to_owned(), - })) - .await? - .into_inner() - .token; - let validated_user_repository_token = identity - .validate_session(Request::new(ValidateSessionRequest { - session_token: user_repository_token, - })) - .await? - .into_inner(); - assert_eq!(validated_user_repository_token.audience, "git_http"); - assert_eq!( - validated_user_repository_token.resource_id, - repository.to_string() - ); - let user_repository_read = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated_user_repository_token.principal_id, - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "repo:read".to_owned(), - })) - .await? - .into_inner(); - assert!(user_repository_read.allowed); - - let user_api_token = local_agents - .issue_user_repository_api_token(Request::new(IssueUserRepositoryApiTokenRequest { - session_token: session_token.clone(), - repository_owner: username.clone(), - repository_name: "agent-repository".to_owned(), - capabilities: vec!["issues:write".to_owned(), "pr:merge".to_owned()], - })) - .await? - .into_inner() - .token; - let validated_user_api_token = identity - .validate_session(Request::new(ValidateSessionRequest { - session_token: user_api_token, - })) - .await? - .into_inner(); - assert_eq!(validated_user_api_token.audience, "api"); - assert_eq!(validated_user_api_token.resource_id, repository.to_string()); - let user_merge = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated_user_api_token.principal_id, - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "pr:merge".to_owned(), - })) - .await? - .into_inner(); - assert!(user_merge.allowed); - - assert!( - local_agents - .issue_local_agent_repository_api_token(Request::new( - IssueLocalAgentRepositoryApiTokenRequest { - agent_id: agent_id.clone(), - instance_host: "workstation.test".to_owned(), - lease_term: enrolled.lease_term, - fencing_token: enrolled.fencing_token.clone(), - credential: heartbeat.credential.clone(), - repository_owner: username.clone(), - repository_name: "agent-repository".to_owned(), - capabilities: vec!["pr:merge".to_owned()], - }, - )) - .await - .is_err() - ); - let agent_api_token = local_agents - .issue_local_agent_repository_api_token(Request::new( - IssueLocalAgentRepositoryApiTokenRequest { - agent_id: agent_id.clone(), - instance_host: "workstation.test".to_owned(), - lease_term: enrolled.lease_term, - fencing_token: enrolled.fencing_token.clone(), - credential: heartbeat.credential.clone(), - repository_owner: username.clone(), - repository_name: "agent-repository".to_owned(), - capabilities: vec!["pr:open".to_owned()], - }, - )) - .await? - .into_inner() - .token; - let validated_agent_api_token = identity - .validate_session(Request::new(ValidateSessionRequest { - session_token: agent_api_token, - })) - .await? - .into_inner(); - assert_eq!(validated_agent_api_token.audience, "api"); - let agent_open = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated_agent_api_token.principal_id.clone(), - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "pr:open".to_owned(), - })) - .await? - .into_inner(); - assert!(agent_open.allowed); - let agent_merge = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated_agent_api_token.principal_id, - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "pr:merge".to_owned(), - })) - .await? - .into_inner(); - assert!(!agent_merge.allowed); - - let repository_token = local_agents - .issue_repository_token(Request::new(IssueLocalAgentRepositoryTokenRequest { - agent_id: agent_id.clone(), - instance_host: "workstation.test".to_owned(), - lease_term: enrolled.lease_term, - fencing_token: enrolled.fencing_token.clone(), - credential: heartbeat.credential, - repository_owner: username.clone(), - repository_name: "agent-repository".to_owned(), - })) - .await? - .into_inner() - .token; - let validated_token = identity - .validate_session(Request::new(ValidateSessionRequest { - session_token: repository_token.clone(), - })) - .await? - .into_inner(); - assert_eq!( - validated_token.principal_kind, - PrincipalKind::AccessToken as i32 - ); - assert_eq!(validated_token.owner_user_id, owner.to_string()); - assert_eq!(validated_token.audience, "git_http"); - assert_eq!( - validated_token.resource_kind, - ResourceKind::Repository as i32 - ); - assert_eq!(validated_token.resource_id, repository.to_string()); - let token_write = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated_token.principal_id.clone(), - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "code:push".to_owned(), - })) - .await? - .into_inner(); - assert!(token_write.allowed); - let token_other_repo = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated_token.principal_id, - principal_kind: PrincipalKind::AccessToken as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: other_repository.to_string(), - capability: "code:push".to_owned(), - })) - .await? - .into_inner(); - assert!(!token_other_repo.allowed); - - let write = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: agent_id.clone(), - principal_kind: PrincipalKind::LocalAgent as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "code:push".to_owned(), - })) - .await? - .into_inner(); - assert!(write.allowed); - let admin = identity - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: agent_id.clone(), - principal_kind: PrincipalKind::LocalAgent as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "admin".to_owned(), - })) - .await? - .into_inner(); - assert!(!admin.allowed); - - let key = local_agents - .add_ssh_key(Request::new(AddLocalAgentSshKeyRequest { - session_token: session_token.clone(), - agent_id: agent_id.clone(), - public_key, - })) - .await? - .into_inner(); - let resolved = identity - .resolve_ssh_key(Request::new(ResolveSshKeyRequest { - fingerprint: key.fingerprint.clone(), - })) - .await? - .into_inner(); - assert_eq!(resolved.principal_id, agent_id); - assert_eq!(resolved.principal_kind, PrincipalKind::LocalAgent as i32); - local_agents - .revoke_ssh_key(Request::new(RevokeLocalAgentSshKeyRequest { - session_token: session_token.clone(), - key_id: key.key_id, - })) - .await?; - assert!( - identity - .resolve_ssh_key(Request::new(ResolveSshKeyRequest { - fingerprint: key.fingerprint, - })) - .await - .is_err() - ); - - let replacement = local_agents - .enroll(Request::new(EnrollLocalAgentRequest { - session_token: session_token.clone(), - owner: username, - name: "codex".to_owned(), - instance_host: "workstation.test".to_owned(), - })) - .await? - .into_inner(); - assert!(replacement.lease_term > enrolled.lease_term); - assert!( - local_agents - .heartbeat(Request::new(LocalAgentHeartbeatRequest { - agent_id: agent_id.clone(), - instance_host: "workstation.test".to_owned(), - lease_term: enrolled.lease_term, - fencing_token: enrolled.fencing_token, - credential: enrolled.credential, - })) - .await - .is_err() - ); - - local_agents - .revoke(Request::new(RevokeLocalAgentRequest { - session_token, - agent_id: agent_id.clone(), - })) - .await?; - assert!( - local_agents - .heartbeat(Request::new(LocalAgentHeartbeatRequest { - agent_id, - instance_host: "workstation.test".to_owned(), - lease_term: replacement.lease_term, - fencing_token: replacement.fencing_token, - credential: replacement.credential, - })) - .await - .is_err() - ); - assert!( - identity - .validate_session(Request::new(ValidateSessionRequest { - session_token: repository_token, - })) - .await - .is_err() - ); - Ok(()) -} +#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] + +use std::error::Error; + +use chrono::{Duration, Utc}; +use ssh_key::{PublicKey, public::Ed25519PublicKey}; +use syncode_identity_api_grpc::wire::{ + AddLocalAgentSshKeyRequest, CheckCapabilityRequest, CreateAccessTokenRequest, + CreateLocalAgentRequest, EnrollLocalAgentRequest, IssueLocalAgentRepositoryApiTokenRequest, + IssueLocalAgentRepositoryTokenRequest, IssueUserRepositoryApiTokenRequest, + IssueUserRepositoryTokenRequest, LocalAgentHeartbeatRequest, PrincipalKind, + ResolveSshKeyRequest, ResourceKind, RevokeLocalAgentRequest, RevokeLocalAgentSshKeyRequest, + ValidateSessionRequest, +}; +use syncode_identity_api_grpc::{ + IdentityServer, IdentityService, LocalAgentServer, LocalAgentService, +}; +use syncode_identity_model::{CoarsePreset, GrantPrincipalKind, Resource}; +use syncode_identity_storage::Postgres; +use tonic::Request; +use uuid::Uuid; + +type TestResult = Result>; + +fn url() -> String { + std::env::var("SYNCODE_IDENTITY_TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgres:///syncode_identity_test".to_owned()) +} + +fn local_agent_server(store: &Postgres) -> TestResult { + let mailer = syncode_identity_mailer::Mailer::new(syncode_identity_mailer::Config { + host: "smtp.example.invalid".to_owned(), + port: 587, + username: String::new(), + password: String::new(), + from: "test@example.invalid".to_owned(), + from_name: "Test".to_owned(), + })?; + let forge = + syncode_identity_gitea_client::GiteaClient::new(syncode_identity_gitea_client::Config { + internal_url: "https://forge.test.invalid".to_owned(), + internal_token: "test".to_owned(), + }); + let settings: std::sync::Arc = + std::sync::Arc::new(syncode_identity_application::SettingsApplication::new( + std::sync::Arc::new(store.clone()), + std::sync::Arc::new(forge.clone()), + std::sync::Arc::new(forge), + std::sync::Arc::new(mailer), + "https://identity.test.invalid".to_owned(), + )); + let local_agents: std::sync::Arc = + std::sync::Arc::new(syncode_identity_application::LocalAgentApplication::new( + std::sync::Arc::new(store.clone()), + )); + Ok(LocalAgentServer::new(local_agents, settings)) +} + +#[tokio::test] +async fn access_token_never_exceeds_its_owner() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let local_agents = local_agent_server(&store)?; + let identity = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( + store.clone(), + )), + )); + let username = format!("token-owner-{}", Uuid::new_v4()); + let owner = store.insert_user(&username).await?; + let repository = store + .seed_repository("user", owner, "token-repository", "private") + .await?; + store + .insert_grant( + GrantPrincipalKind::User, + owner, + Resource::repository(repository.into()), + &CoarsePreset::Write.expand(), + owner, + ) + .await?; + let session_token = format!("test-token-{}", Uuid::new_v4()); + store + .issue_session(owner, &session_token, Utc::now() + Duration::hours(1)) + .await?; + + let token = local_agents + .create_access_token(Request::new(CreateAccessTokenRequest { + session_token: session_token.clone(), + name: "automation".to_owned(), + capabilities: vec!["pr:open".to_owned(), "pr:merge".to_owned()], + expires_in_seconds: 30 * 24 * 60 * 60, + })) + .await? + .into_inner() + .token; + assert!(token.starts_with("syn_pat_")); + let validated = identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: token, + })) + .await? + .into_inner(); + assert_eq!(validated.audience, "api"); + assert_eq!(validated.resource_kind, ResourceKind::Unspecified as i32); + let open = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated.principal_id.clone(), + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "pr:open".to_owned(), + })) + .await? + .into_inner(); + assert!(open.allowed); + let merge = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated.principal_id, + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "pr:merge".to_owned(), + })) + .await? + .into_inner(); + assert!(!merge.allowed); + + let repository_token = local_agents + .issue_user_repository_token(Request::new(IssueUserRepositoryTokenRequest { + session_token, + repository_owner: username, + repository_name: "token-repository".to_owned(), + })) + .await? + .into_inner() + .token; + let validated_repository_token = identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: repository_token, + })) + .await? + .into_inner(); + let push = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_repository_token.principal_id.clone(), + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "code:push".to_owned(), + })) + .await? + .into_inner(); + assert!(push.allowed); + let force_push = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_repository_token.principal_id, + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "code:force-push".to_owned(), + })) + .await? + .into_inner(); + assert!(!force_push.allowed); + Ok(()) +} + +#[tokio::test] +async fn local_agent_is_fenced_restricted_and_revoked() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let local_agents = local_agent_server(&store)?; + let identity = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( + store.clone(), + )), + )); + let username = format!("agent-owner-{}", Uuid::new_v4()); + let owner = store.insert_user(&username).await?; + let repository = store + .seed_repository("user", owner, "agent-repository", "private") + .await?; + let other_repository = store + .seed_repository("user", owner, "other-agent-repository", "private") + .await?; + store + .insert_grant( + GrantPrincipalKind::User, + owner, + Resource::repository(repository.into()), + &CoarsePreset::Admin.expand(), + owner, + ) + .await?; + store + .insert_grant( + GrantPrincipalKind::User, + owner, + Resource::repository(other_repository.into()), + &CoarsePreset::Admin.expand(), + owner, + ) + .await?; + let session_token = format!("test-token-{}", Uuid::new_v4()); + store + .issue_session(owner, &session_token, Utc::now() + Duration::hours(1)) + .await?; + let mut public_key_bytes = [0_u8; 32]; + public_key_bytes[..16].copy_from_slice(Uuid::new_v4().as_bytes()); + public_key_bytes[16..].copy_from_slice(Uuid::new_v4().as_bytes()); + let public_key = PublicKey::from(Ed25519PublicKey(public_key_bytes)).to_openssh()?; + + let agent_id = local_agents + .create(Request::new(CreateLocalAgentRequest { + session_token: session_token.clone(), + owner: username.clone(), + name: "codex".to_owned(), + definition: "codex".to_owned(), + restriction: vec!["write".to_owned()], + })) + .await? + .into_inner() + .agent_id; + let enrolled = local_agents + .enroll(Request::new(EnrollLocalAgentRequest { + session_token: session_token.clone(), + owner: username.clone(), + name: "codex".to_owned(), + instance_host: "workstation.test".to_owned(), + })) + .await? + .into_inner(); + assert_eq!(enrolled.agent_id, agent_id); + + let heartbeat = local_agents + .heartbeat(Request::new(LocalAgentHeartbeatRequest { + agent_id: agent_id.clone(), + instance_host: "workstation.test".to_owned(), + lease_term: enrolled.lease_term, + fencing_token: enrolled.fencing_token.clone(), + credential: enrolled.credential.clone(), + })) + .await? + .into_inner(); + local_agents + .heartbeat(Request::new(LocalAgentHeartbeatRequest { + agent_id: agent_id.clone(), + instance_host: "workstation.test".to_owned(), + lease_term: enrolled.lease_term, + fencing_token: enrolled.fencing_token.clone(), + credential: heartbeat.credential.clone(), + })) + .await?; + + let user_repository_token = local_agents + .issue_user_repository_token(Request::new(IssueUserRepositoryTokenRequest { + session_token: session_token.clone(), + repository_owner: username.clone(), + repository_name: "agent-repository".to_owned(), + })) + .await? + .into_inner() + .token; + let validated_user_repository_token = identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: user_repository_token, + })) + .await? + .into_inner(); + assert_eq!(validated_user_repository_token.audience, "git_http"); + assert_eq!( + validated_user_repository_token.resource_id, + repository.to_string() + ); + let user_repository_read = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_user_repository_token.principal_id.clone(), + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "repo:read".to_owned(), + })) + .await? + .into_inner(); + assert!(user_repository_read.allowed); + let user_repository_write = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_user_repository_token.principal_id, + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "code:push".to_owned(), + })) + .await? + .into_inner(); + assert!(user_repository_write.allowed); + + let user_api_token = local_agents + .issue_user_repository_api_token(Request::new(IssueUserRepositoryApiTokenRequest { + session_token: session_token.clone(), + repository_owner: username.clone(), + repository_name: "agent-repository".to_owned(), + capabilities: vec!["issues:write".to_owned(), "pr:merge".to_owned()], + })) + .await? + .into_inner() + .token; + let validated_user_api_token = identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: user_api_token, + })) + .await? + .into_inner(); + assert_eq!(validated_user_api_token.audience, "api"); + assert_eq!(validated_user_api_token.resource_id, repository.to_string()); + let user_merge = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_user_api_token.principal_id, + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "pr:merge".to_owned(), + })) + .await? + .into_inner(); + assert!(user_merge.allowed); + + assert!( + local_agents + .issue_local_agent_repository_api_token(Request::new( + IssueLocalAgentRepositoryApiTokenRequest { + agent_id: agent_id.clone(), + instance_host: "workstation.test".to_owned(), + lease_term: enrolled.lease_term, + fencing_token: enrolled.fencing_token.clone(), + credential: heartbeat.credential.clone(), + repository_owner: username.clone(), + repository_name: "agent-repository".to_owned(), + capabilities: vec!["pr:merge".to_owned()], + }, + )) + .await + .is_err() + ); + let agent_api_token = local_agents + .issue_local_agent_repository_api_token(Request::new( + IssueLocalAgentRepositoryApiTokenRequest { + agent_id: agent_id.clone(), + instance_host: "workstation.test".to_owned(), + lease_term: enrolled.lease_term, + fencing_token: enrolled.fencing_token.clone(), + credential: heartbeat.credential.clone(), + repository_owner: username.clone(), + repository_name: "agent-repository".to_owned(), + capabilities: vec!["pr:open".to_owned()], + }, + )) + .await? + .into_inner() + .token; + let validated_agent_api_token = identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: agent_api_token, + })) + .await? + .into_inner(); + assert_eq!(validated_agent_api_token.audience, "api"); + let agent_open = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_agent_api_token.principal_id.clone(), + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "pr:open".to_owned(), + })) + .await? + .into_inner(); + assert!(agent_open.allowed); + let agent_merge = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_agent_api_token.principal_id, + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "pr:merge".to_owned(), + })) + .await? + .into_inner(); + assert!(!agent_merge.allowed); + + let repository_token = local_agents + .issue_repository_token(Request::new(IssueLocalAgentRepositoryTokenRequest { + agent_id: agent_id.clone(), + instance_host: "workstation.test".to_owned(), + lease_term: enrolled.lease_term, + fencing_token: enrolled.fencing_token.clone(), + credential: heartbeat.credential, + repository_owner: username.clone(), + repository_name: "agent-repository".to_owned(), + })) + .await? + .into_inner() + .token; + let validated_token = identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: repository_token.clone(), + })) + .await? + .into_inner(); + assert_eq!( + validated_token.principal_kind, + PrincipalKind::AccessToken as i32 + ); + assert_eq!(validated_token.owner_user_id, owner.to_string()); + assert_eq!(validated_token.audience, "git_http"); + assert_eq!( + validated_token.resource_kind, + ResourceKind::Repository as i32 + ); + assert_eq!(validated_token.resource_id, repository.to_string()); + let token_write = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_token.principal_id.clone(), + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "code:push".to_owned(), + })) + .await? + .into_inner(); + assert!(token_write.allowed); + let token_other_repo = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated_token.principal_id, + principal_kind: PrincipalKind::AccessToken as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: other_repository.to_string(), + capability: "code:push".to_owned(), + })) + .await? + .into_inner(); + assert!(!token_other_repo.allowed); + + let write = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: agent_id.clone(), + principal_kind: PrincipalKind::LocalAgent as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "code:push".to_owned(), + })) + .await? + .into_inner(); + assert!(write.allowed); + let admin = identity + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: agent_id.clone(), + principal_kind: PrincipalKind::LocalAgent as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "admin".to_owned(), + })) + .await? + .into_inner(); + assert!(!admin.allowed); + + let key = local_agents + .add_ssh_key(Request::new(AddLocalAgentSshKeyRequest { + session_token: session_token.clone(), + agent_id: agent_id.clone(), + public_key, + })) + .await? + .into_inner(); + let resolved = identity + .resolve_ssh_key(Request::new(ResolveSshKeyRequest { + fingerprint: key.fingerprint.clone(), + })) + .await? + .into_inner(); + assert_eq!(resolved.principal_id, agent_id); + assert_eq!(resolved.principal_kind, PrincipalKind::LocalAgent as i32); + local_agents + .revoke_ssh_key(Request::new(RevokeLocalAgentSshKeyRequest { + session_token: session_token.clone(), + key_id: key.key_id, + })) + .await?; + assert!( + identity + .resolve_ssh_key(Request::new(ResolveSshKeyRequest { + fingerprint: key.fingerprint, + })) + .await + .is_err() + ); + + let replacement = local_agents + .enroll(Request::new(EnrollLocalAgentRequest { + session_token: session_token.clone(), + owner: username, + name: "codex".to_owned(), + instance_host: "workstation.test".to_owned(), + })) + .await? + .into_inner(); + assert!(replacement.lease_term > enrolled.lease_term); + assert!( + local_agents + .heartbeat(Request::new(LocalAgentHeartbeatRequest { + agent_id: agent_id.clone(), + instance_host: "workstation.test".to_owned(), + lease_term: enrolled.lease_term, + fencing_token: enrolled.fencing_token, + credential: enrolled.credential, + })) + .await + .is_err() + ); + + local_agents + .revoke(Request::new(RevokeLocalAgentRequest { + session_token, + agent_id: agent_id.clone(), + })) + .await?; + assert!( + local_agents + .heartbeat(Request::new(LocalAgentHeartbeatRequest { + agent_id, + instance_host: "workstation.test".to_owned(), + lease_term: replacement.lease_term, + fencing_token: replacement.fencing_token, + credential: replacement.credential, + })) + .await + .is_err() + ); + assert!( + identity + .validate_session(Request::new(ValidateSessionRequest { + session_token: repository_token, + })) + .await + .is_err() + ); + Ok(()) +} diff --git a/crates/storage/src/access_tokens.rs b/crates/storage/src/access_tokens.rs --- a/crates/storage/src/access_tokens.rs +++ b/crates/storage/src/access_tokens.rs @@ -1,228 +1,229 @@ -use std::collections::BTreeSet; - -use chrono::{DateTime, Utc}; -use syncode_identity_model::{Capability, Resource}; -use uuid::Uuid; - -use crate::sessions::hash_token; -use crate::{Postgres, StoreError, decode_capabilities}; - -#[derive(Clone, Debug)] -pub struct AccessTokenRecord { - pub id: Uuid, - pub owner_user_id: Uuid, - pub local_agent_id: Option, - pub capabilities: BTreeSet, - pub resource: Option, - pub audience: String, - pub expires_at: DateTime, -} - -struct AccessTokenRow { - id: Uuid, - owner_user_id: Uuid, - local_agent_id: Option, - capabilities: Vec, - resource_kind: Option, - resource_id: Option, - audience: String, - expires_at: DateTime, -} - -impl Postgres { - pub async fn issue_user_repository_token( - &self, - user_id: Uuid, - repository_id: Uuid, - token: &str, - expires_at: DateTime, - ) -> Result { - let id = Uuid::new_v4(); - sqlx::query_scalar!( - "INSERT INTO access_token ( - id, user_id, local_agent_id, name, token_hash, capabilities, - resource_kind, resource_id, audience, expires_at, created_at - ) - SELECT $1, account.id, NULL, 'syn-repository-clone', $4, - ARRAY['repo:read'], 'repository', $3, 'git_http', $5, now() - FROM \"user\" account - WHERE account.id = $2 AND account.deleted_at IS NULL - RETURNING access_token.id", - id, - user_id, - repository_id, - hash_token(token), - expires_at - ) - .fetch_one(&self.pool) - .await - .map_err(Into::into) - } - - pub async fn issue_user_repository_api_token( - &self, - user_id: Uuid, - repository_id: Uuid, - capabilities: &BTreeSet, - token: &str, - expires_at: DateTime, - ) -> Result { - let id = Uuid::new_v4(); - let capability_strings = capabilities - .iter() - .map(ToString::to_string) - .collect::>(); - sqlx::query_scalar!( - "INSERT INTO access_token ( - id, user_id, local_agent_id, name, token_hash, capabilities, - resource_kind, resource_id, audience, expires_at, created_at - ) - SELECT $1, account.id, NULL, 'syn-repository-api', $5, $3, - 'repository', $4, 'api', $6, now() - FROM \"user\" account - WHERE account.id = $2 AND account.deleted_at IS NULL - RETURNING access_token.id", - id, - user_id, - &capability_strings, - repository_id, - hash_token(token), - expires_at - ) - .fetch_one(&self.pool) - .await - .map_err(Into::into) - } - - pub async fn issue_local_agent_repository_api_token( - &self, - local_agent_id: Uuid, - repository_id: Uuid, - capabilities: &BTreeSet, - token: &str, - expires_at: DateTime, - ) -> Result { - let id = Uuid::new_v4(); - let capability_strings = capabilities - .iter() - .map(ToString::to_string) - .collect::>(); - sqlx::query_scalar!( - "INSERT INTO access_token ( - id, user_id, local_agent_id, name, token_hash, capabilities, - resource_kind, resource_id, audience, expires_at, created_at - ) - SELECT $1, NULL, agent.id, 'syn-local-agent-repository-api', $5, $3, - 'repository', $4, 'api', $6, now() - FROM local_agent agent - WHERE agent.id = $2 AND agent.status = 'active' AND agent.revoked_at IS NULL - RETURNING access_token.id", - id, - local_agent_id, - &capability_strings, - repository_id, - hash_token(token), - expires_at - ) - .fetch_one(&self.pool) - .await - .map_err(Into::into) - } - - pub async fn issue_local_agent_repository_token( - &self, - local_agent_id: Uuid, - repository_id: Uuid, - token: &str, - expires_at: DateTime, - ) -> Result { - let id = Uuid::new_v4(); - sqlx::query_scalar!( - "INSERT INTO access_token ( - id, user_id, local_agent_id, name, token_hash, capabilities, - resource_kind, resource_id, audience, expires_at, created_at - ) - SELECT $1, NULL, agent.id, 'local-agent-repository', $4, - agent.restriction, 'repository', $3, 'git_http', $5, now() - FROM local_agent agent - WHERE agent.id = $2 AND agent.status = 'active' AND agent.revoked_at IS NULL - RETURNING access_token.id", - id, - local_agent_id, - repository_id, - hash_token(token), - expires_at - ) - .fetch_one(&self.pool) - .await - .map_err(Into::into) - } - - pub async fn load_access_token( - &self, - token_id: Uuid, - ) -> Result, StoreError> { - let row = sqlx::query_as!( - AccessTokenRow, - r#"SELECT token.id AS "id!", COALESCE(token.user_id, agent.owner_user_id) AS "owner_user_id!", - token.local_agent_id AS "local_agent_id?", token.capabilities AS "capabilities!", - token.resource_kind::text AS "resource_kind?", token.resource_id AS "resource_id?", - token.audience AS "audience!", token.expires_at AS "expires_at!" - FROM access_token token - LEFT JOIN local_agent agent ON agent.id = token.local_agent_id - WHERE token.id = $1 AND token.revoked_at IS NULL - AND token.expires_at > now() - AND (token.local_agent_id IS NULL OR ( - agent.status = 'active' AND agent.revoked_at IS NULL - ))"#, - token_id - ) - .fetch_optional(&self.pool) - .await?; - row.map(access_token_record).transpose() - } - - pub async fn validate_access_token( - &self, - token: &str, - ) -> Result, StoreError> { - let row = sqlx::query_as!( - AccessTokenRow, - r#"SELECT token.id AS "id!", COALESCE(token.user_id, agent.owner_user_id) AS "owner_user_id!", - token.local_agent_id AS "local_agent_id?", token.capabilities AS "capabilities!", - token.resource_kind::text AS "resource_kind?", token.resource_id AS "resource_id?", - token.audience AS "audience!", token.expires_at AS "expires_at!" - FROM access_token token - LEFT JOIN local_agent agent ON agent.id = token.local_agent_id - WHERE token.token_hash = $1 AND token.revoked_at IS NULL - AND token.expires_at > now() - AND (token.local_agent_id IS NULL OR ( - agent.status = 'active' AND agent.revoked_at IS NULL - ))"#, - hash_token(token) - ) - .fetch_optional(&self.pool) - .await?; - row.map(access_token_record).transpose() - } -} - -fn access_token_record(row: AccessTokenRow) -> Result { - let resource = match (row.resource_kind.as_deref(), row.resource_id) { - (None, None) => None, - (Some("repository"), Some(id)) => Some(Resource::repository(id.into())), - (Some("organization"), Some(id)) => Some(Resource::organization(id.into())), - (Some("instance"), None) => Some(Resource::instance()), - (Some(kind), _) => return Err(StoreError::UnknownKind(kind.to_owned())), - (None, Some(_)) => return Err(StoreError::UnknownKind("missing resource kind".to_owned())), - }; - Ok(AccessTokenRecord { - id: row.id, - owner_user_id: row.owner_user_id, - local_agent_id: row.local_agent_id, - capabilities: decode_capabilities(row.capabilities)?, - resource, - audience: row.audience, - expires_at: row.expires_at, - }) -} +use std::collections::BTreeSet; + +use chrono::{DateTime, Utc}; +use syncode_identity_model::{Capability, Resource}; +use uuid::Uuid; + +use crate::sessions::hash_token; +use crate::{Postgres, StoreError, decode_capabilities}; + +#[derive(Clone, Debug)] +pub struct AccessTokenRecord { + pub id: Uuid, + pub owner_user_id: Uuid, + pub local_agent_id: Option, + pub capabilities: BTreeSet, + pub resource: Option, + pub audience: String, + pub expires_at: DateTime, +} + +struct AccessTokenRow { + id: Uuid, + owner_user_id: Uuid, + local_agent_id: Option, + capabilities: Vec, + resource_kind: Option, + resource_id: Option, + audience: String, + expires_at: DateTime, +} + +impl Postgres { + pub async fn issue_user_repository_token( + &self, + user_id: Uuid, + repository_id: Uuid, + token: &str, + expires_at: DateTime, + ) -> Result { + let id = Uuid::new_v4(); + sqlx::query_scalar!( + "INSERT INTO access_token ( + id, user_id, local_agent_id, name, token_hash, capabilities, + resource_kind, resource_id, audience, expires_at, created_at + ) + SELECT $1, account.id, NULL, 'syn-repository-clone', $4, + ARRAY['repo:read', 'code:push', 'code:force-push'], + 'repository', $3, 'git_http', $5, now() + FROM \"user\" account + WHERE account.id = $2 AND account.deleted_at IS NULL + RETURNING access_token.id", + id, + user_id, + repository_id, + hash_token(token), + expires_at + ) + .fetch_one(&self.pool) + .await + .map_err(Into::into) + } + + pub async fn issue_user_repository_api_token( + &self, + user_id: Uuid, + repository_id: Uuid, + capabilities: &BTreeSet, + token: &str, + expires_at: DateTime, + ) -> Result { + let id = Uuid::new_v4(); + let capability_strings = capabilities + .iter() + .map(ToString::to_string) + .collect::>(); + sqlx::query_scalar!( + "INSERT INTO access_token ( + id, user_id, local_agent_id, name, token_hash, capabilities, + resource_kind, resource_id, audience, expires_at, created_at + ) + SELECT $1, account.id, NULL, 'syn-repository-api', $5, $3, + 'repository', $4, 'api', $6, now() + FROM \"user\" account + WHERE account.id = $2 AND account.deleted_at IS NULL + RETURNING access_token.id", + id, + user_id, + &capability_strings, + repository_id, + hash_token(token), + expires_at + ) + .fetch_one(&self.pool) + .await + .map_err(Into::into) + } + + pub async fn issue_local_agent_repository_api_token( + &self, + local_agent_id: Uuid, + repository_id: Uuid, + capabilities: &BTreeSet, + token: &str, + expires_at: DateTime, + ) -> Result { + let id = Uuid::new_v4(); + let capability_strings = capabilities + .iter() + .map(ToString::to_string) + .collect::>(); + sqlx::query_scalar!( + "INSERT INTO access_token ( + id, user_id, local_agent_id, name, token_hash, capabilities, + resource_kind, resource_id, audience, expires_at, created_at + ) + SELECT $1, NULL, agent.id, 'syn-local-agent-repository-api', $5, $3, + 'repository', $4, 'api', $6, now() + FROM local_agent agent + WHERE agent.id = $2 AND agent.status = 'active' AND agent.revoked_at IS NULL + RETURNING access_token.id", + id, + local_agent_id, + &capability_strings, + repository_id, + hash_token(token), + expires_at + ) + .fetch_one(&self.pool) + .await + .map_err(Into::into) + } + + pub async fn issue_local_agent_repository_token( + &self, + local_agent_id: Uuid, + repository_id: Uuid, + token: &str, + expires_at: DateTime, + ) -> Result { + let id = Uuid::new_v4(); + sqlx::query_scalar!( + "INSERT INTO access_token ( + id, user_id, local_agent_id, name, token_hash, capabilities, + resource_kind, resource_id, audience, expires_at, created_at + ) + SELECT $1, NULL, agent.id, 'local-agent-repository', $4, + agent.restriction, 'repository', $3, 'git_http', $5, now() + FROM local_agent agent + WHERE agent.id = $2 AND agent.status = 'active' AND agent.revoked_at IS NULL + RETURNING access_token.id", + id, + local_agent_id, + repository_id, + hash_token(token), + expires_at + ) + .fetch_one(&self.pool) + .await + .map_err(Into::into) + } + + pub async fn load_access_token( + &self, + token_id: Uuid, + ) -> Result, StoreError> { + let row = sqlx::query_as!( + AccessTokenRow, + r#"SELECT token.id AS "id!", COALESCE(token.user_id, agent.owner_user_id) AS "owner_user_id!", + token.local_agent_id AS "local_agent_id?", token.capabilities AS "capabilities!", + token.resource_kind::text AS "resource_kind?", token.resource_id AS "resource_id?", + token.audience AS "audience!", token.expires_at AS "expires_at!" + FROM access_token token + LEFT JOIN local_agent agent ON agent.id = token.local_agent_id + WHERE token.id = $1 AND token.revoked_at IS NULL + AND token.expires_at > now() + AND (token.local_agent_id IS NULL OR ( + agent.status = 'active' AND agent.revoked_at IS NULL + ))"#, + token_id + ) + .fetch_optional(&self.pool) + .await?; + row.map(access_token_record).transpose() + } + + pub async fn validate_access_token( + &self, + token: &str, + ) -> Result, StoreError> { + let row = sqlx::query_as!( + AccessTokenRow, + r#"SELECT token.id AS "id!", COALESCE(token.user_id, agent.owner_user_id) AS "owner_user_id!", + token.local_agent_id AS "local_agent_id?", token.capabilities AS "capabilities!", + token.resource_kind::text AS "resource_kind?", token.resource_id AS "resource_id?", + token.audience AS "audience!", token.expires_at AS "expires_at!" + FROM access_token token + LEFT JOIN local_agent agent ON agent.id = token.local_agent_id + WHERE token.token_hash = $1 AND token.revoked_at IS NULL + AND token.expires_at > now() + AND (token.local_agent_id IS NULL OR ( + agent.status = 'active' AND agent.revoked_at IS NULL + ))"#, + hash_token(token) + ) + .fetch_optional(&self.pool) + .await?; + row.map(access_token_record).transpose() + } +} + +fn access_token_record(row: AccessTokenRow) -> Result { + let resource = match (row.resource_kind.as_deref(), row.resource_id) { + (None, None) => None, + (Some("repository"), Some(id)) => Some(Resource::repository(id.into())), + (Some("organization"), Some(id)) => Some(Resource::organization(id.into())), + (Some("instance"), None) => Some(Resource::instance()), + (Some(kind), _) => return Err(StoreError::UnknownKind(kind.to_owned())), + (None, Some(_)) => return Err(StoreError::UnknownKind("missing resource kind".to_owned())), + }; + Ok(AccessTokenRecord { + id: row.id, + owner_user_id: row.owner_user_id, + local_agent_id: row.local_agent_id, + capabilities: decode_capabilities(row.capabilities)?, + resource, + audience: row.audience, + expires_at: row.expires_at, + }) +} diff --git a/.sqlx/query-b1a10424732b7210e51747df4147ba94bfa7992ae17edb1eb1204cb1ed05b443.json b/.sqlx/query-b1a10424732b7210e51747df4147ba94bfa7992ae17edb1eb1204cb1ed05b443.json --- a/.sqlx/query-b1a10424732b7210e51747df4147ba94bfa7992ae17edb1eb1204cb1ed05b443.json +++ /dev/null @@ -1,32 +1,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO access_token (\n id, user_id, local_agent_id, name, token_hash, capabilities,\n resource_kind, resource_id, audience, expires_at, created_at\n )\n SELECT $1, account.id, NULL, 'syn-repository-clone', $4,\n ARRAY['repo:read'], 'repository', $3, 'git_http', $5, now()\n FROM \"user\" account\n WHERE account.id = $2 AND account.deleted_at IS NULL\n RETURNING access_token.id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid", - "origin": { - "Table": { - "table": "access_token", - "name": "id" - } - } - } - ], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Uuid", - "Text", - "Timestamptz" - ] - }, - "nullable": [ - false - ] - }, - "hash": "b1a10424732b7210e51747df4147ba94bfa7992ae17edb1eb1204cb1ed05b443" -} diff --git a/.sqlx/query-d93e26d00910a3370283ccb1d5ed2eb8ba57af632f2f789ae57e814e7c4edbec.json b/.sqlx/query-d93e26d00910a3370283ccb1d5ed2eb8ba57af632f2f789ae57e814e7c4edbec.json --- /dev/null +++ b/.sqlx/query-d93e26d00910a3370283ccb1d5ed2eb8ba57af632f2f789ae57e814e7c4edbec.json @@ -1,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO access_token (\n id, user_id, local_agent_id, name, token_hash, capabilities,\n resource_kind, resource_id, audience, expires_at, created_at\n )\n SELECT $1, account.id, NULL, 'syn-repository-clone', $4,\n ARRAY['repo:read', 'code:push', 'code:force-push'],\n 'repository', $3, 'git_http', $5, now()\n FROM \"user\" account\n WHERE account.id = $2 AND account.deleted_at IS NULL\n RETURNING access_token.id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid", + "origin": { + "Table": { + "table": "access_token", + "name": "id" + } + } + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d93e26d00910a3370283ccb1d5ed2eb8ba57af632f2f789ae57e814e7c4edbec" +} diff --git a/crates/storage/migrations/0026_user_git_push_tokens.sql b/crates/storage/migrations/0026_user_git_push_tokens.sql --- /dev/null +++ b/crates/storage/migrations/0026_user_git_push_tokens.sql @@ -1,0 +1,43 @@ +ALTER TABLE access_token + DROP CONSTRAINT access_token_git_http_scope_check; + +UPDATE access_token +SET capabilities = ARRAY['repo:read', 'code:push', 'code:force-push'] +WHERE audience = 'git_http' + AND user_id IS NOT NULL + AND local_agent_id IS NULL + AND name = 'syn-repository-clone'; + +ALTER TABLE access_token + ADD CONSTRAINT access_token_git_http_scope_check CHECK ( + audience != 'git_http' + OR ( + resource_kind = 'repository' + AND resource_id IS NOT NULL + AND ( + ( + local_agent_id IS NOT NULL + AND user_id IS NULL + AND name = 'local-agent-repository' + ) + OR ( + user_id IS NOT NULL + AND local_agent_id IS NULL + AND ( + ( + name = 'syn-repository-clone' + AND capabilities = ARRAY[ + 'repo:read', + 'code:push', + 'code:force-push' + ] + ) + OR ( + name = 'syncode-workflow-repository' + AND capabilities = ARRAY['repo:read'] + ) + ) + ) + ) + ) + );