diff --git a/tests/identity_grpc.rs b/tests/identity_grpc.rs --- a/tests/identity_grpc.rs +++ b/tests/identity_grpc.rs @@ -1,569 +1,577 @@ -#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] - -use std::error::Error; - -use chrono::{Duration, Utc}; -use syncode_identity_api_grpc::wire::{ - ArchiveRepositoryProjectionRequest, ArchiveRepositoryRequest, CapabilityQuery, - CheckCapabilitiesRequest, CheckCapabilityRequest, DeleteRepositoryRequest, - ListPermittedResourcesRequest, PrincipalKind, ProvisionRepositoryRequest, - RegisterRepositoryRequest, RepositoryOwnerKind, RepositoryVisibility, ResolveRepositoryRequest, - ResourceKind, UnarchiveRepositoryRequest, UpdateRepositoryMetadataRequest, - UpdateRepositoryVisibilityRequest, ValidateSessionRequest, -}; -use syncode_identity_api_grpc::{IdentityServer, IdentityService}; -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()) -} - -#[tokio::test] -async fn repository_registration_preserves_the_supplied_id_and_is_idempotent() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let server = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( - store.clone(), - )), - )); - let username = format!("registered-owner-{}", Uuid::new_v4()); - let owner = store.insert_user(&username).await?; - let repository_id = Uuid::new_v4(); - let name = format!("registered-repository-{}", Uuid::new_v4()); - let request = RegisterRepositoryRequest { - repository_id: repository_id.to_string(), - owner_kind: RepositoryOwnerKind::User as i32, - owner_id: owner.to_string(), - name: name.clone(), - visibility: RepositoryVisibility::Private as i32, - creator_id: owner.to_string(), - }; - - server - .register_repository(Request::new(request.clone())) - .await?; - server.register_repository(Request::new(request)).await?; - - let resolved = server - .resolve_repository(Request::new(ResolveRepositoryRequest { - owner: username.clone(), - name: name.clone(), - })) - .await? - .into_inner(); - assert_eq!(resolved.repository_id, repository_id.to_string()); - - let organization_slug = format!("registered-org-{}", Uuid::new_v4()); - let organization_id = store.insert_organization(&organization_slug).await?; - let administrative_team = store.insert_team(organization_id, "maintainers").await?; - store.add_team_member(administrative_team, owner).await?; - store - .seed_organization_admin_grant(organization_id, administrative_team, owner) - .await?; - let organization_repository_id = Uuid::new_v4(); - let organization_repository_name = format!("org-repository-{}", Uuid::new_v4()); - server - .register_repository(Request::new(RegisterRepositoryRequest { - repository_id: organization_repository_id.to_string(), - owner_kind: RepositoryOwnerKind::Organization as i32, - owner_id: organization_id.to_string(), - name: organization_repository_name.clone(), - visibility: RepositoryVisibility::Private as i32, - creator_id: owner.to_string(), - })) - .await?; - let organization_repository = server - .resolve_repository(Request::new(ResolveRepositoryRequest { - owner: organization_slug, - name: organization_repository_name, - })) - .await? - .into_inner(); - assert_eq!( - organization_repository.repository_id, - organization_repository_id.to_string() - ); - - assert_eq!( - store.resolve_repository(&username, &name).await?, - Some(repository_id) - ); - let allowed = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository_id.to_string(), - capability: "owner".to_owned(), - })) - .await? - .into_inner(); - assert!(allowed.allowed); - server - .archive_repository_projection(Request::new(ArchiveRepositoryProjectionRequest { - repository_id: repository_id.to_string(), - })) - .await?; - let archived = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository_id.to_string(), - capability: "owner".to_owned(), - })) - .await? - .into_inner(); - assert!(!archived.allowed); - server - .unarchive_repository(Request::new(UnarchiveRepositoryRequest { - repository_id: repository_id.to_string(), - })) - .await?; - let restored = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository_id.to_string(), - capability: "owner".to_owned(), - })) - .await? - .into_inner(); - assert!(restored.allowed); - server - .delete_repository(Request::new(DeleteRepositoryRequest { - repository_id: repository_id.to_string(), - })) - .await?; - assert!(store.resolve_repository(&username, &name).await?.is_none()); - let missing = server - .resolve_repository(Request::new(ResolveRepositoryRequest { - owner: username, - name, - })) - .await - .expect_err("deleted repository must not resolve"); - assert_eq!(missing.code(), tonic::Code::NotFound); - Ok(()) -} - -#[tokio::test] -async fn repository_provisioning_is_idempotent_and_grants_its_owner() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let server = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new(store)), - )); - let store = Postgres::connect(&url(), 4).await?; - let username = format!("repository-owner-{}", Uuid::new_v4()); - let persisted_owner = store.insert_user(&username).await?; - - let request = ProvisionRepositoryRequest { - owner_kind: RepositoryOwnerKind::User as i32, - owner_id: persisted_owner.to_string(), - name: format!("repository-{}", Uuid::new_v4()), - visibility: RepositoryVisibility::Private as i32, - creator_id: persisted_owner.to_string(), - }; - let first = server - .provision_repository(Request::new(request.clone())) - .await? - .into_inner(); - let second = server - .provision_repository(Request::new(request)) - .await? - .into_inner(); - assert_eq!(first.repository_id, second.repository_id); - - let allowed = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: persisted_owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: first.repository_id.clone(), - capability: "owner".to_owned(), - })) - .await? - .into_inner(); - assert!(allowed.allowed); - - let outsider = store - .insert_user(&format!("repository-outsider-{}", Uuid::new_v4())) - .await?; - let private_read = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: outsider.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: first.repository_id, - capability: "repo:read".to_owned(), - })) - .await? - .into_inner(); - assert!(!private_read.allowed); - - let public_repository = server - .provision_repository(Request::new(ProvisionRepositoryRequest { - owner_kind: RepositoryOwnerKind::User as i32, - owner_id: persisted_owner.to_string(), - name: format!("public-repository-{}", Uuid::new_v4()), - visibility: RepositoryVisibility::Public as i32, - creator_id: persisted_owner.to_string(), - })) - .await? - .into_inner(); - let public_read = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: outsider.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: public_repository.repository_id.clone(), - capability: "repo:read".to_owned(), - })) - .await? - .into_inner(); - assert!(public_read.allowed); - - server - .update_repository_visibility(Request::new(UpdateRepositoryVisibilityRequest { - repository_id: public_repository.repository_id.clone(), - visibility: RepositoryVisibility::Private as i32, - })) - .await?; - let private_after_update = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: outsider.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: public_repository.repository_id.clone(), - capability: "repo:read".to_owned(), - })) - .await? - .into_inner(); - assert!(!private_after_update.allowed); - - let new_owner_name = format!("transferred-owner-{}", Uuid::new_v4()); - let new_owner = store.insert_user(&new_owner_name).await?; - let transferred_name = format!("transferred-repository-{}", Uuid::new_v4()); - server - .update_repository_metadata(Request::new(UpdateRepositoryMetadataRequest { - repository_id: public_repository.repository_id.clone(), - owner_kind: RepositoryOwnerKind::User as i32, - owner_id: new_owner.to_string(), - name: transferred_name.clone(), - visibility: RepositoryVisibility::Private as i32, - actor_id: persisted_owner.to_string(), - })) - .await?; - assert_eq!( - store - .resolve_repository(&new_owner_name, &transferred_name) - .await?, - Some(public_repository.repository_id.parse()?) - ); - - // Coordinates changing is a fact other planes depend on; identity has to - // announce it, not just apply it. - let repository_uuid: Uuid = public_repository.repository_id.parse()?; - let announced: Vec<_> = store - .claim_identity_events(64, 30) - .await? - .into_iter() - .filter(|event| event.resource_id == repository_uuid) - .collect(); - let topics: Vec<&str> = announced - .iter() - .map(|event| event.event_type.as_str()) - .collect(); - assert!( - topics.contains(&"identity.repository.renamed"), - "rename was not announced: {topics:?}" - ); - assert!( - topics.contains(&"identity.repository.transferred"), - "transfer was not announced: {topics:?}" - ); - assert!( - announced.iter().all(|event| event.attempts >= 1), - "claiming counts as an attempt" - ); - assert!( - store - .claim_identity_events(64, 30) - .await? - .iter() - .all(|event| event.resource_id != repository_uuid), - "a claimed event stays hidden while its lease holds" - ); - let old_owner_access = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: persisted_owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: public_repository.repository_id.clone(), - capability: "owner".to_owned(), - })) - .await? - .into_inner(); - assert!(!old_owner_access.allowed); - let new_owner_merge = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: new_owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: public_repository.repository_id.clone(), - capability: "pr:merge".to_owned(), - })) - .await? - .into_inner(); - assert!(new_owner_merge.allowed); - - server - .archive_repository(Request::new(ArchiveRepositoryRequest { - repository_id: public_repository.repository_id.clone(), - })) - .await?; - assert!( - store - .resolve_repository(&new_owner_name, &transferred_name) - .await? - .is_none() - ); - let archived_access = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: new_owner.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: public_repository.repository_id, - capability: "admin".to_owned(), - })) - .await? - .into_inner(); - assert!(!archived_access.allowed); - - Ok(()) -} - -#[tokio::test] -async fn organization_repository_is_granted_to_its_administrative_team() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let server = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( - store.clone(), - )), - )); - let administrator = store - .insert_user(&format!("organization-admin-{}", Uuid::new_v4())) - .await?; - let organization = store - .insert_organization(&format!("repository-org-{}", Uuid::new_v4())) - .await?; - let administrative_team = store.insert_team(organization, "maintainers").await?; - store - .add_team_member(administrative_team, administrator) - .await?; - store - .seed_organization_admin_grant(organization, administrative_team, administrator) - .await?; - - let provisioned = server - .provision_repository(Request::new(ProvisionRepositoryRequest { - owner_kind: RepositoryOwnerKind::Organization as i32, - owner_id: organization.to_string(), - name: format!("repository-{}", Uuid::new_v4()), - visibility: RepositoryVisibility::Public as i32, - creator_id: administrator.to_string(), - })) - .await? - .into_inner(); - let allowed = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: administrator.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: provisioned.repository_id, - capability: "owner".to_owned(), - })) - .await? - .into_inner(); - - assert!(allowed.allowed); - Ok(()) -} - -#[tokio::test] -async fn validate_session_then_check_capability() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let server = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( - store.clone(), - )), - )); - - let owner = store - .insert_user(&format!("owner-{}", Uuid::new_v4())) - .await?; - let user = store - .insert_user(&format!("user-{}", Uuid::new_v4())) - .await?; - let repository = Uuid::new_v4(); - store - .insert_grant( - GrantPrincipalKind::User, - user, - Resource::repository(repository.into()), - &CoarsePreset::Write.expand(), - owner, - ) - .await?; - - let token = format!("test-token-{}", Uuid::new_v4()); - store - .issue_session(user, &token, Utc::now() + Duration::hours(1)) - .await?; - - let validated = server - .validate_session(Request::new(ValidateSessionRequest { - session_token: token, - })) - .await? - .into_inner(); - assert_eq!(validated.principal_id, user.to_string()); - assert_eq!(validated.principal_kind, PrincipalKind::User as i32); - - let allowed = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated.principal_id.clone(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "write".to_owned(), - })) - .await? - .into_inner(); - assert!(allowed.allowed); - - let denied = server - .check_capability(Request::new(CheckCapabilityRequest { - principal_id: validated.principal_id, - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - resource_id: repository.to_string(), - capability: "admin".to_owned(), - })) - .await? - .into_inner(); - assert!(!denied.allowed); - - let rejected = server - .validate_session(Request::new(ValidateSessionRequest { - session_token: "not-a-real-token".to_owned(), - })) - .await; - assert!(rejected.is_err()); - - Ok(()) -} - -/// Listing must answer what one round trip of `check_capability` per resource -/// would answer, and must not lose the public-visibility half of the decision. -#[tokio::test] -async fn lists_permitted_resources_and_checks_capabilities_in_one_call() -> TestResult { - let store = Postgres::connect(&url(), 4).await?; - let server = IdentityServer::new(std::sync::Arc::new( - syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( - store.clone(), - )), - )); - - let owner = store - .insert_user(&format!("grantor-{}", Uuid::new_v4())) - .await?; - let user = store - .insert_user(&format!("grantee-{}", Uuid::new_v4())) - .await?; - let granted = Uuid::new_v4(); - let ungranted = Uuid::new_v4(); - store - .insert_grant( - GrantPrincipalKind::User, - user, - Resource::repository(granted.into()), - &CoarsePreset::Write.expand(), - owner, - ) - .await?; - - let permitted = server - .list_permitted_resources(Request::new(ListPermittedResourcesRequest { - principal_id: user.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - capability: "write".to_owned(), - })) - .await? - .into_inner(); - assert_eq!(vec![granted.to_string()], permitted.resource_ids); - assert!( - !permitted.includes_public, - "write is not something a public repository hands out" - ); - - let readable = server - .list_permitted_resources(Request::new(ListPermittedResourcesRequest { - principal_id: user.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - capability: "read".to_owned(), - })) - .await? - .into_inner(); - assert!( - readable.includes_public, - "public repositories are readable without a grant, and the caller has to be told" - ); - - let batch = server - .check_capabilities(Request::new(CheckCapabilitiesRequest { - principal_id: user.to_string(), - principal_kind: PrincipalKind::User as i32, - queries: vec![ - CapabilityQuery { - resource_kind: ResourceKind::Repository as i32, - resource_id: granted.to_string(), - capability: "write".to_owned(), - }, - CapabilityQuery { - resource_kind: ResourceKind::Repository as i32, - resource_id: ungranted.to_string(), - capability: "write".to_owned(), - }, - ], - })) - .await? - .into_inner(); - assert_eq!( - vec![true, false], - batch.allowed, - "decisions come back in the order they were asked" - ); - - // Ownership is not grant-derived, so listing by it must refuse rather than - // return a list that looks complete and is not. - let refused = server - .list_permitted_resources(Request::new(ListPermittedResourcesRequest { - principal_id: user.to_string(), - principal_kind: PrincipalKind::User as i32, - resource_kind: ResourceKind::Repository as i32, - capability: "owner".to_owned(), - })) - .await; - assert!(refused.is_err(), "listing by ownership must be refused"); - Ok(()) -} +#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] + +use std::error::Error; + +use chrono::{Duration, Utc}; +use syncode_identity_api_grpc::wire::{ + ArchiveRepositoryProjectionRequest, ArchiveRepositoryRequest, CapabilityQuery, + CheckCapabilitiesRequest, CheckCapabilityRequest, DeleteRepositoryRequest, + GetRepositoryCoordinatesRequest, ListPermittedResourcesRequest, PrincipalKind, + ProvisionRepositoryRequest, RegisterRepositoryRequest, RepositoryOwnerKind, + RepositoryVisibility, ResolveRepositoryRequest, ResourceKind, UnarchiveRepositoryRequest, + UpdateRepositoryMetadataRequest, UpdateRepositoryVisibilityRequest, ValidateSessionRequest, +}; +use syncode_identity_api_grpc::{IdentityServer, IdentityService}; +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()) +} + +#[tokio::test] +async fn repository_registration_preserves_the_supplied_id_and_is_idempotent() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let server = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( + store.clone(), + )), + )); + let username = format!("registered-owner-{}", Uuid::new_v4()); + let owner = store.insert_user(&username).await?; + let repository_id = Uuid::new_v4(); + let name = format!("registered-repository-{}", Uuid::new_v4()); + let request = RegisterRepositoryRequest { + repository_id: repository_id.to_string(), + owner_kind: RepositoryOwnerKind::User as i32, + owner_id: owner.to_string(), + name: name.clone(), + visibility: RepositoryVisibility::Private as i32, + creator_id: owner.to_string(), + }; + + server + .register_repository(Request::new(request.clone())) + .await?; + server.register_repository(Request::new(request)).await?; + + let resolved = server + .resolve_repository(Request::new(ResolveRepositoryRequest { + owner: username.clone(), + name: name.clone(), + })) + .await? + .into_inner(); + assert_eq!(resolved.repository_id, repository_id.to_string()); + let coordinates = server + .get_repository_coordinates(Request::new(GetRepositoryCoordinatesRequest { + repository_id: repository_id.to_string(), + })) + .await? + .into_inner(); + assert_eq!(coordinates.owner, username); + assert_eq!(coordinates.name, name); + + let organization_slug = format!("registered-org-{}", Uuid::new_v4()); + let organization_id = store.insert_organization(&organization_slug).await?; + let administrative_team = store.insert_team(organization_id, "maintainers").await?; + store.add_team_member(administrative_team, owner).await?; + store + .seed_organization_admin_grant(organization_id, administrative_team, owner) + .await?; + let organization_repository_id = Uuid::new_v4(); + let organization_repository_name = format!("org-repository-{}", Uuid::new_v4()); + server + .register_repository(Request::new(RegisterRepositoryRequest { + repository_id: organization_repository_id.to_string(), + owner_kind: RepositoryOwnerKind::Organization as i32, + owner_id: organization_id.to_string(), + name: organization_repository_name.clone(), + visibility: RepositoryVisibility::Private as i32, + creator_id: owner.to_string(), + })) + .await?; + let organization_repository = server + .resolve_repository(Request::new(ResolveRepositoryRequest { + owner: organization_slug, + name: organization_repository_name, + })) + .await? + .into_inner(); + assert_eq!( + organization_repository.repository_id, + organization_repository_id.to_string() + ); + + assert_eq!( + store.resolve_repository(&username, &name).await?, + Some(repository_id) + ); + let allowed = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository_id.to_string(), + capability: "owner".to_owned(), + })) + .await? + .into_inner(); + assert!(allowed.allowed); + server + .archive_repository_projection(Request::new(ArchiveRepositoryProjectionRequest { + repository_id: repository_id.to_string(), + })) + .await?; + let archived = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository_id.to_string(), + capability: "owner".to_owned(), + })) + .await? + .into_inner(); + assert!(!archived.allowed); + server + .unarchive_repository(Request::new(UnarchiveRepositoryRequest { + repository_id: repository_id.to_string(), + })) + .await?; + let restored = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository_id.to_string(), + capability: "owner".to_owned(), + })) + .await? + .into_inner(); + assert!(restored.allowed); + server + .delete_repository(Request::new(DeleteRepositoryRequest { + repository_id: repository_id.to_string(), + })) + .await?; + assert!(store.resolve_repository(&username, &name).await?.is_none()); + let missing = server + .resolve_repository(Request::new(ResolveRepositoryRequest { + owner: username, + name, + })) + .await + .expect_err("deleted repository must not resolve"); + assert_eq!(missing.code(), tonic::Code::NotFound); + Ok(()) +} + +#[tokio::test] +async fn repository_provisioning_is_idempotent_and_grants_its_owner() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let server = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new(store)), + )); + let store = Postgres::connect(&url(), 4).await?; + let username = format!("repository-owner-{}", Uuid::new_v4()); + let persisted_owner = store.insert_user(&username).await?; + + let request = ProvisionRepositoryRequest { + owner_kind: RepositoryOwnerKind::User as i32, + owner_id: persisted_owner.to_string(), + name: format!("repository-{}", Uuid::new_v4()), + visibility: RepositoryVisibility::Private as i32, + creator_id: persisted_owner.to_string(), + }; + let first = server + .provision_repository(Request::new(request.clone())) + .await? + .into_inner(); + let second = server + .provision_repository(Request::new(request)) + .await? + .into_inner(); + assert_eq!(first.repository_id, second.repository_id); + + let allowed = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: persisted_owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: first.repository_id.clone(), + capability: "owner".to_owned(), + })) + .await? + .into_inner(); + assert!(allowed.allowed); + + let outsider = store + .insert_user(&format!("repository-outsider-{}", Uuid::new_v4())) + .await?; + let private_read = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: outsider.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: first.repository_id, + capability: "repo:read".to_owned(), + })) + .await? + .into_inner(); + assert!(!private_read.allowed); + + let public_repository = server + .provision_repository(Request::new(ProvisionRepositoryRequest { + owner_kind: RepositoryOwnerKind::User as i32, + owner_id: persisted_owner.to_string(), + name: format!("public-repository-{}", Uuid::new_v4()), + visibility: RepositoryVisibility::Public as i32, + creator_id: persisted_owner.to_string(), + })) + .await? + .into_inner(); + let public_read = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: outsider.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: public_repository.repository_id.clone(), + capability: "repo:read".to_owned(), + })) + .await? + .into_inner(); + assert!(public_read.allowed); + + server + .update_repository_visibility(Request::new(UpdateRepositoryVisibilityRequest { + repository_id: public_repository.repository_id.clone(), + visibility: RepositoryVisibility::Private as i32, + })) + .await?; + let private_after_update = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: outsider.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: public_repository.repository_id.clone(), + capability: "repo:read".to_owned(), + })) + .await? + .into_inner(); + assert!(!private_after_update.allowed); + + let new_owner_name = format!("transferred-owner-{}", Uuid::new_v4()); + let new_owner = store.insert_user(&new_owner_name).await?; + let transferred_name = format!("transferred-repository-{}", Uuid::new_v4()); + server + .update_repository_metadata(Request::new(UpdateRepositoryMetadataRequest { + repository_id: public_repository.repository_id.clone(), + owner_kind: RepositoryOwnerKind::User as i32, + owner_id: new_owner.to_string(), + name: transferred_name.clone(), + visibility: RepositoryVisibility::Private as i32, + actor_id: persisted_owner.to_string(), + })) + .await?; + assert_eq!( + store + .resolve_repository(&new_owner_name, &transferred_name) + .await?, + Some(public_repository.repository_id.parse()?) + ); + + // Coordinates changing is a fact other planes depend on; identity has to + // announce it, not just apply it. + let repository_uuid: Uuid = public_repository.repository_id.parse()?; + let announced: Vec<_> = store + .claim_identity_events(64, 30) + .await? + .into_iter() + .filter(|event| event.resource_id == repository_uuid) + .collect(); + let topics: Vec<&str> = announced + .iter() + .map(|event| event.event_type.as_str()) + .collect(); + assert!( + topics.contains(&"identity.repository.renamed"), + "rename was not announced: {topics:?}" + ); + assert!( + topics.contains(&"identity.repository.transferred"), + "transfer was not announced: {topics:?}" + ); + assert!( + announced.iter().all(|event| event.attempts >= 1), + "claiming counts as an attempt" + ); + assert!( + store + .claim_identity_events(64, 30) + .await? + .iter() + .all(|event| event.resource_id != repository_uuid), + "a claimed event stays hidden while its lease holds" + ); + let old_owner_access = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: persisted_owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: public_repository.repository_id.clone(), + capability: "owner".to_owned(), + })) + .await? + .into_inner(); + assert!(!old_owner_access.allowed); + let new_owner_merge = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: new_owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: public_repository.repository_id.clone(), + capability: "pr:merge".to_owned(), + })) + .await? + .into_inner(); + assert!(new_owner_merge.allowed); + + server + .archive_repository(Request::new(ArchiveRepositoryRequest { + repository_id: public_repository.repository_id.clone(), + })) + .await?; + assert!( + store + .resolve_repository(&new_owner_name, &transferred_name) + .await? + .is_none() + ); + let archived_access = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: new_owner.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: public_repository.repository_id, + capability: "admin".to_owned(), + })) + .await? + .into_inner(); + assert!(!archived_access.allowed); + + Ok(()) +} + +#[tokio::test] +async fn organization_repository_is_granted_to_its_administrative_team() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let server = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( + store.clone(), + )), + )); + let administrator = store + .insert_user(&format!("organization-admin-{}", Uuid::new_v4())) + .await?; + let organization = store + .insert_organization(&format!("repository-org-{}", Uuid::new_v4())) + .await?; + let administrative_team = store.insert_team(organization, "maintainers").await?; + store + .add_team_member(administrative_team, administrator) + .await?; + store + .seed_organization_admin_grant(organization, administrative_team, administrator) + .await?; + + let provisioned = server + .provision_repository(Request::new(ProvisionRepositoryRequest { + owner_kind: RepositoryOwnerKind::Organization as i32, + owner_id: organization.to_string(), + name: format!("repository-{}", Uuid::new_v4()), + visibility: RepositoryVisibility::Public as i32, + creator_id: administrator.to_string(), + })) + .await? + .into_inner(); + let allowed = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: administrator.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: provisioned.repository_id, + capability: "owner".to_owned(), + })) + .await? + .into_inner(); + + assert!(allowed.allowed); + Ok(()) +} + +#[tokio::test] +async fn validate_session_then_check_capability() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let server = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( + store.clone(), + )), + )); + + let owner = store + .insert_user(&format!("owner-{}", Uuid::new_v4())) + .await?; + let user = store + .insert_user(&format!("user-{}", Uuid::new_v4())) + .await?; + let repository = Uuid::new_v4(); + store + .insert_grant( + GrantPrincipalKind::User, + user, + Resource::repository(repository.into()), + &CoarsePreset::Write.expand(), + owner, + ) + .await?; + + let token = format!("test-token-{}", Uuid::new_v4()); + store + .issue_session(user, &token, Utc::now() + Duration::hours(1)) + .await?; + + let validated = server + .validate_session(Request::new(ValidateSessionRequest { + session_token: token, + })) + .await? + .into_inner(); + assert_eq!(validated.principal_id, user.to_string()); + assert_eq!(validated.principal_kind, PrincipalKind::User as i32); + + let allowed = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated.principal_id.clone(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "write".to_owned(), + })) + .await? + .into_inner(); + assert!(allowed.allowed); + + let denied = server + .check_capability(Request::new(CheckCapabilityRequest { + principal_id: validated.principal_id, + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + resource_id: repository.to_string(), + capability: "admin".to_owned(), + })) + .await? + .into_inner(); + assert!(!denied.allowed); + + let rejected = server + .validate_session(Request::new(ValidateSessionRequest { + session_token: "not-a-real-token".to_owned(), + })) + .await; + assert!(rejected.is_err()); + + Ok(()) +} + +/// Listing must answer what one round trip of `check_capability` per resource +/// would answer, and must not lose the public-visibility half of the decision. +#[tokio::test] +async fn lists_permitted_resources_and_checks_capabilities_in_one_call() -> TestResult { + let store = Postgres::connect(&url(), 4).await?; + let server = IdentityServer::new(std::sync::Arc::new( + syncode_identity_application::IdentityBridgeApplication::new(std::sync::Arc::new( + store.clone(), + )), + )); + + let owner = store + .insert_user(&format!("grantor-{}", Uuid::new_v4())) + .await?; + let user = store + .insert_user(&format!("grantee-{}", Uuid::new_v4())) + .await?; + let granted = Uuid::new_v4(); + let ungranted = Uuid::new_v4(); + store + .insert_grant( + GrantPrincipalKind::User, + user, + Resource::repository(granted.into()), + &CoarsePreset::Write.expand(), + owner, + ) + .await?; + + let permitted = server + .list_permitted_resources(Request::new(ListPermittedResourcesRequest { + principal_id: user.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + capability: "write".to_owned(), + })) + .await? + .into_inner(); + assert_eq!(vec![granted.to_string()], permitted.resource_ids); + assert!( + !permitted.includes_public, + "write is not something a public repository hands out" + ); + + let readable = server + .list_permitted_resources(Request::new(ListPermittedResourcesRequest { + principal_id: user.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + capability: "read".to_owned(), + })) + .await? + .into_inner(); + assert!( + readable.includes_public, + "public repositories are readable without a grant, and the caller has to be told" + ); + + let batch = server + .check_capabilities(Request::new(CheckCapabilitiesRequest { + principal_id: user.to_string(), + principal_kind: PrincipalKind::User as i32, + queries: vec![ + CapabilityQuery { + resource_kind: ResourceKind::Repository as i32, + resource_id: granted.to_string(), + capability: "write".to_owned(), + }, + CapabilityQuery { + resource_kind: ResourceKind::Repository as i32, + resource_id: ungranted.to_string(), + capability: "write".to_owned(), + }, + ], + })) + .await? + .into_inner(); + assert_eq!( + vec![true, false], + batch.allowed, + "decisions come back in the order they were asked" + ); + + // Ownership is not grant-derived, so listing by it must refuse rather than + // return a list that looks complete and is not. + let refused = server + .list_permitted_resources(Request::new(ListPermittedResourcesRequest { + principal_id: user.to_string(), + principal_kind: PrincipalKind::User as i32, + resource_kind: ResourceKind::Repository as i32, + capability: "owner".to_owned(), + })) + .await; + assert!(refused.is_err(), "listing by ownership must be refused"); + Ok(()) +} diff --git a/crates/api-grpc/proto/identity.proto b/crates/api-grpc/proto/identity.proto --- a/crates/api-grpc/proto/identity.proto +++ b/crates/api-grpc/proto/identity.proto @@ -1,789 +1,799 @@ -syntax = "proto3"; - -package syncode.identity.v1; - -option go_package = "gitea.dev/modules/identityclient/pb"; - -service Identity { - rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse); - rpc CheckCapability(CheckCapabilityRequest) returns (CheckCapabilityResponse); - rpc CheckCapabilities(CheckCapabilitiesRequest) returns (CheckCapabilitiesResponse); - rpc ListPermittedResources(ListPermittedResourcesRequest) returns (ListPermittedResourcesResponse); - rpc ResolveSSHKey(ResolveSSHKeyRequest) returns (ResolveSSHKeyResponse); - rpc ListUserSigningKeys(ListUserSigningKeysRequest) returns (ListUserSigningKeysResponse); - rpc CheckBranchProtectionBypass(CheckBranchProtectionBypassRequest) returns (CheckBranchProtectionBypassResponse); - rpc ListBranchProtectionRules(ListBranchProtectionRulesRequest) returns (ListBranchProtectionRulesResponse); - rpc ResolveRepository(ResolveRepositoryRequest) returns (ResolveRepositoryResponse); - rpc RegisterRepository(RegisterRepositoryRequest) returns (RegisterRepositoryResponse); - rpc ProvisionRepository(ProvisionRepositoryRequest) returns (ProvisionRepositoryResponse); - rpc UpdateRepositoryVisibility(UpdateRepositoryVisibilityRequest) returns (UpdateRepositoryVisibilityResponse); - rpc UpdateRepositoryMetadata(UpdateRepositoryMetadataRequest) returns (UpdateRepositoryMetadataResponse); - rpc ArchiveRepository(ArchiveRepositoryRequest) returns (ArchiveRepositoryResponse); - rpc ArchiveRepositoryProjection(ArchiveRepositoryProjectionRequest) returns (ArchiveRepositoryProjectionResponse); - rpc UnarchiveRepository(UnarchiveRepositoryRequest) returns (UnarchiveRepositoryResponse); - rpc DeleteRepository(DeleteRepositoryRequest) returns (DeleteRepositoryResponse); -} - -message ResolveRepositoryRequest { - string owner = 1; - string name = 2; -} - -message ResolveRepositoryResponse { - string repository_id = 1; -} - -enum RepositoryOwnerKind { - REPOSITORY_OWNER_KIND_UNSPECIFIED = 0; - REPOSITORY_OWNER_KIND_USER = 1; - REPOSITORY_OWNER_KIND_ORGANIZATION = 2; -} - -enum RepositoryVisibility { - REPOSITORY_VISIBILITY_UNSPECIFIED = 0; - REPOSITORY_VISIBILITY_PUBLIC = 1; - REPOSITORY_VISIBILITY_PRIVATE = 2; - REPOSITORY_VISIBILITY_LIMITED = 3; -} - -message RegisterRepositoryRequest { - string repository_id = 1; - RepositoryOwnerKind owner_kind = 2; - string owner_id = 3; - string name = 4; - RepositoryVisibility visibility = 5; - string creator_id = 6; -} - -message RegisterRepositoryResponse {} - -message ProvisionRepositoryRequest { - RepositoryOwnerKind owner_kind = 1; - string owner_id = 2; - string name = 3; - RepositoryVisibility visibility = 4; - string creator_id = 5; -} - -message ProvisionRepositoryResponse { - string repository_id = 1; -} - -message UpdateRepositoryVisibilityRequest { - string repository_id = 1; - RepositoryVisibility visibility = 2; -} - -message UpdateRepositoryVisibilityResponse {} - -message UpdateRepositoryMetadataRequest { - string repository_id = 1; - RepositoryOwnerKind owner_kind = 2; - string owner_id = 3; - string name = 4; - RepositoryVisibility visibility = 5; - string actor_id = 6; -} - -message UpdateRepositoryMetadataResponse {} - -message ArchiveRepositoryRequest { - string repository_id = 1; -} - -message ArchiveRepositoryResponse {} - -message ArchiveRepositoryProjectionRequest { - string repository_id = 1; -} - -message ArchiveRepositoryProjectionResponse {} - -message UnarchiveRepositoryRequest { - string repository_id = 1; -} - -message UnarchiveRepositoryResponse {} - -message DeleteRepositoryRequest { - string repository_id = 1; -} - -message DeleteRepositoryResponse {} - -service LocalAgent { - rpc Create(CreateLocalAgentRequest) returns (CreateLocalAgentResponse); - rpc Enroll(EnrollLocalAgentRequest) returns (EnrollLocalAgentResponse); - rpc Heartbeat(LocalAgentHeartbeatRequest) returns (LocalAgentHeartbeatResponse); - rpc Revoke(RevokeLocalAgentRequest) returns (RevokeLocalAgentResponse); - rpc AddSSHKey(AddLocalAgentSSHKeyRequest) returns (AddLocalAgentSSHKeyResponse); - rpc RevokeSSHKey(RevokeLocalAgentSSHKeyRequest) returns (RevokeLocalAgentSSHKeyResponse); - rpc IssueUserRepositoryToken(IssueUserRepositoryTokenRequest) returns (IssueLocalAgentRepositoryTokenResponse); - rpc IssueRepositoryToken(IssueLocalAgentRepositoryTokenRequest) returns (IssueLocalAgentRepositoryTokenResponse); - rpc IssueUserRepositoryAPIToken(IssueUserRepositoryAPITokenRequest) returns (IssueRepositoryAPITokenResponse); - rpc IssueLocalAgentRepositoryAPIToken(IssueLocalAgentRepositoryAPITokenRequest) returns (IssueRepositoryAPITokenResponse); - rpc CreateAccessToken(CreateAccessTokenRequest) returns (CreateAccessTokenResponse); - rpc Logout(LocalAgentLogoutRequest) returns (LocalAgentLogoutResponse); -} - -service Settings { - rpc GetAccount(GetAccountRequest) returns (GetAccountResponse); - rpc UpdateProfile(UpdateProfileRequest) returns (OperationResponse); - rpc UnlinkIdentity(UnlinkIdentityRequest) returns (OperationResponse); - rpc AddEmail(AddEmailRequest) returns (OperationResponse); - rpc SetPrimaryEmail(SetPrimaryEmailRequest) returns (OperationResponse); - rpc RemoveEmail(RemoveEmailRequest) returns (OperationResponse); - rpc RequestAccountDeletion(RequestAccountDeletionRequest) returns (OperationResponse); - rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse); - rpc CreateOrganization(CreateOrganizationRequest) returns (Organization); - rpc UpdateOrganization(UpdateOrganizationRequest) returns (Organization); - rpc DeleteOrganization(DeleteOrganizationRequest) returns (OperationResponse); - rpc ListTeams(ListTeamsRequest) returns (ListTeamsResponse); - rpc CreateTeam(CreateTeamRequest) returns (Team); - rpc RenameTeam(RenameTeamRequest) returns (OperationResponse); - rpc DeleteTeam(DeleteTeamRequest) returns (OperationResponse); - rpc AddTeamMember(AddTeamMemberRequest) returns (OperationResponse); - rpc RemoveTeamMember(RemoveTeamMemberRequest) returns (OperationResponse); - rpc GrantTeamRepositoryAccess(GrantTeamRepositoryAccessRequest) returns (GrantInfo); - rpc RevokeGrant(RevokeGrantRequest) returns (OperationResponse); - rpc ListPlatformAgents(ListPlatformAgentsRequest) returns (ListPlatformAgentsResponse); - rpc CreatePlatformAgent(CreatePlatformAgentRequest) returns (PlatformAgentInfo); - rpc GrantPlatformAgentAccess(GrantPlatformAgentAccessRequest) returns (GrantInfo); - rpc RevokePlatformAgent(RevokePlatformAgentRequest) returns (OperationResponse); - rpc AddUserSSHKey(AddUserSSHKeyRequest) returns (AddUserSSHKeyResponse); - rpc RevokeUserSSHKey(RevokeUserSSHKeyRequest) returns (OperationResponse); - rpc AddUserSigningKey(AddUserSigningKeyRequest) returns (AddUserSigningKeyResponse); - rpc RevokeUserSigningKey(RevokeUserSigningKeyRequest) returns (OperationResponse); - rpc RevokeAccessToken(RevokeAccessTokenRequest) returns (OperationResponse); -} - -service ForgeOperations { - rpc CreatePullRequest(CreatePullRequestOperation) returns (PullRequestInfo); - rpc ListPullRequests(ListPullRequestsOperation) returns (ListPullRequestsResponse); - rpc GetPullRequest(GetPullRequestOperation) returns (PullRequestInfo); - rpc MergePullRequest(MergePullRequestOperation) returns (OperationResponse); - rpc ListIssues(ListIssuesOperation) returns (ListIssuesResponse); - rpc CreateIssueComment(CreateIssueCommentOperation) returns (IssueCommentInfo); -} - -enum ForgeListState { - FORGE_LIST_STATE_UNSPECIFIED = 0; - FORGE_LIST_STATE_OPEN = 1; - FORGE_LIST_STATE_CLOSED = 2; - FORGE_LIST_STATE_ALL = 3; -} - -enum ForgeItemState { - FORGE_ITEM_STATE_UNSPECIFIED = 0; - FORGE_ITEM_STATE_OPEN = 1; - FORGE_ITEM_STATE_CLOSED = 2; -} - -enum ForgeMergeMethod { - FORGE_MERGE_METHOD_UNSPECIFIED = 0; - FORGE_MERGE_METHOD_MERGE = 1; - FORGE_MERGE_METHOD_REBASE = 2; - FORGE_MERGE_METHOD_REBASE_MERGE = 3; - FORGE_MERGE_METHOD_SQUASH = 4; - FORGE_MERGE_METHOD_FAST_FORWARD_ONLY = 5; -} - -message ForgeRepository { - string owner = 1; - string name = 2; -} - -message CreatePullRequestOperation { - string api_token = 1; - ForgeRepository repository = 2; - string head = 3; - string base = 4; - string title = 5; - string body = 6; -} - -message ListPullRequestsOperation { - string api_token = 1; - ForgeRepository repository = 2; - ForgeListState state = 3; -} - -message GetPullRequestOperation { - string api_token = 1; - ForgeRepository repository = 2; - uint64 number = 3; -} - -message MergePullRequestOperation { - string api_token = 1; - ForgeRepository repository = 2; - uint64 number = 3; - ForgeMergeMethod method = 4; - bool delete_branch = 5; -} - -message PullRequestInfo { - uint64 number = 1; - string title = 2; - ForgeItemState state = 3; - string web_url = 4; - optional string body = 5; - string head = 6; - string base = 7; - bool merged = 8; -} - -message ListPullRequestsResponse { repeated PullRequestInfo pull_requests = 1; } - -message ListIssuesOperation { - string api_token = 1; - ForgeRepository repository = 2; - ForgeListState state = 3; -} - -message IssueInfo { - uint64 number = 1; - string title = 2; - ForgeItemState state = 3; - string web_url = 4; -} - -message ListIssuesResponse { repeated IssueInfo issues = 1; } - -message CreateIssueCommentOperation { - string api_token = 1; - ForgeRepository repository = 2; - uint64 number = 3; - string body = 4; -} - -message IssueCommentInfo { - uint64 id = 1; - string web_url = 2; -} - -enum PrincipalKind { - PRINCIPAL_KIND_UNSPECIFIED = 0; - PRINCIPAL_KIND_USER = 1; - PRINCIPAL_KIND_PLATFORM_AGENT = 2; - PRINCIPAL_KIND_LOCAL_AGENT = 3; - PRINCIPAL_KIND_ACCESS_TOKEN = 4; -} - -enum ResourceKind { - RESOURCE_KIND_UNSPECIFIED = 0; - RESOURCE_KIND_ORGANIZATION = 1; - RESOURCE_KIND_REPOSITORY = 2; - RESOURCE_KIND_INSTANCE = 3; -} - -message ValidateSessionRequest { - string session_token = 1; -} - -message ValidateSessionResponse { - string principal_id = 1; - PrincipalKind principal_kind = 2; - int64 expires_at_unix = 3; - string owner_user_id = 4; - repeated string capabilities = 5; - string audience = 6; - ResourceKind resource_kind = 7; - string resource_id = 8; -} - -message CheckCapabilityRequest { - string principal_id = 1; - PrincipalKind principal_kind = 2; - ResourceKind resource_kind = 3; - // Empty when resource_kind is RESOURCE_KIND_INSTANCE. - string resource_id = 4; - string capability = 5; -} - -message CheckCapabilityResponse { - bool allowed = 1; -} - -message CapabilityQuery { - ResourceKind resource_kind = 1; - // Empty when resource_kind is RESOURCE_KIND_INSTANCE. - string resource_id = 2; - string capability = 3; -} - -message CheckCapabilitiesRequest { - string principal_id = 1; - PrincipalKind principal_kind = 2; - repeated CapabilityQuery queries = 3; -} - -message CheckCapabilitiesResponse { - // One decision per query, in the order the queries were given. - repeated bool allowed = 1; -} - -message ListPermittedResourcesRequest { - string principal_id = 1; - PrincipalKind principal_kind = 2; - ResourceKind resource_kind = 3; - string capability = 4; -} - -message ListPermittedResourcesResponse { - // Resources the principal holds a grant on. - repeated string resource_ids = 1; - // True when every public repository also qualifies. Read access to a public - // repository comes from its visibility, not from a grant, so a caller that - // ignores this hides public repositories from someone entitled to see them. - bool includes_public = 2; -} - -message ResolveSSHKeyRequest { - string fingerprint = 1; -} - -message ResolveSSHKeyResponse { - string principal_id = 1; - PrincipalKind principal_kind = 2; - string owner_user_id = 3; -} - -message ListUserSigningKeysRequest { - string user_id = 1; -} - -message SigningKey { - string key_type = 1; - string key_id = 2; - string public_key = 3; -} - -message ListUserSigningKeysResponse { - repeated SigningKey keys = 1; -} - -message CheckBranchProtectionBypassRequest { - string repository_id = 1; - string pattern = 2; - string principal_id = 3; - PrincipalKind principal_kind = 4; -} - -message CheckBranchProtectionBypassResponse { - bool allowed = 1; - bool protected = 2; - string matched_pattern = 3; - int32 require_review_count = 4; - repeated string required_status_checks = 5; -} - -message ListBranchProtectionRulesRequest { - string repository_id = 1; -} - -message BranchProtectionRule { - string pattern = 1; - int32 require_review_count = 2; - repeated string required_status_checks = 3; -} - -message ListBranchProtectionRulesResponse { - repeated BranchProtectionRule rules = 1; -} - -message CreateLocalAgentRequest { - string session_token = 1; - string owner = 2; - string name = 3; - string definition = 4; - repeated string restriction = 5; -} - -message CreateLocalAgentResponse { - string agent_id = 1; -} - -message EnrollLocalAgentRequest { - string session_token = 1; - string owner = 2; - string name = 3; - string instance_host = 4; -} - -message EnrollLocalAgentResponse { - string agent_id = 1; - int64 lease_term = 2; - string fencing_token = 3; - string credential = 4; - int64 lease_expires_at_unix = 5; - uint32 heartbeat_interval_seconds = 6; -} - -message LocalAgentHeartbeatRequest { - string agent_id = 1; - string instance_host = 2; - int64 lease_term = 3; - string fencing_token = 4; - string credential = 5; -} - -message LocalAgentHeartbeatResponse { - string credential = 1; - int64 lease_expires_at_unix = 2; - uint32 heartbeat_interval_seconds = 3; -} - -message RevokeLocalAgentRequest { - string session_token = 1; - string agent_id = 2; -} - -message RevokeLocalAgentResponse {} - -message AddLocalAgentSSHKeyRequest { - string session_token = 1; - string agent_id = 2; - string public_key = 3; -} - -message AddLocalAgentSSHKeyResponse { - string key_id = 1; - string fingerprint = 2; -} - -message RevokeLocalAgentSSHKeyRequest { - string session_token = 1; - string key_id = 2; -} - -message RevokeLocalAgentSSHKeyResponse {} - -message IssueLocalAgentRepositoryTokenRequest { - string agent_id = 1; - string instance_host = 2; - int64 lease_term = 3; - string fencing_token = 4; - string credential = 5; - string repository_owner = 6; - string repository_name = 7; -} - -message IssueUserRepositoryTokenRequest { - string session_token = 1; - string repository_owner = 2; - string repository_name = 3; -} - -message IssueLocalAgentRepositoryTokenResponse { - string token = 1; - int64 expires_at_unix = 2; -} - -message IssueUserRepositoryAPITokenRequest { - string session_token = 1; - string repository_owner = 2; - string repository_name = 3; - repeated string capabilities = 4; -} - -message IssueLocalAgentRepositoryAPITokenRequest { - string agent_id = 1; - string instance_host = 2; - int64 lease_term = 3; - string fencing_token = 4; - string credential = 5; - string repository_owner = 6; - string repository_name = 7; - repeated string capabilities = 8; -} - -message IssueRepositoryAPITokenResponse { - string token = 1; - int64 expires_at_unix = 2; -} - -message CreateAccessTokenRequest { - string session_token = 1; - string name = 2; - repeated string capabilities = 3; - int64 expires_in_seconds = 4; -} - -message CreateAccessTokenResponse { - string token = 1; - int64 expires_at_unix = 2; -} - -message LocalAgentLogoutRequest { string session_token = 1; } - -message LocalAgentLogoutResponse {} - -message OperationResponse {} - -message GetAccountRequest { string session_token = 1; } - -message LinkedIdentityInfo { - string id = 1; - string provider = 2; - string provider_display_name = 3; - optional string external_username = 4; - optional string avatar_url = 5; - int64 linked_at_unix = 6; -} - -message UserEmailInfo { - string address = 1; - bool verified = 2; - bool is_primary = 3; - repeated string sources = 4; -} - -message PendingEmailVerificationInfo { - string address = 1; - int64 expires_at_unix = 2; -} - -message UserSSHKeyInfo { - string id = 1; - string title = 2; - string fingerprint = 3; - string public_key = 4; - int64 added_at_unix = 5; -} - -message UserSigningKeyInfo { - string id = 1; - string title = 2; - string key_type = 3; - string key_id = 4; - string public_key = 5; - int64 added_at_unix = 6; -} - -message AccessTokenInfo { - string id = 1; - string name = 2; - repeated string capabilities = 3; - int64 expires_at_unix = 4; - int64 created_at_unix = 5; -} - -message GetAccountResponse { - string id = 1; - string username = 2; - optional string display_name = 3; - repeated LinkedIdentityInfo linked_identities = 4; - repeated UserEmailInfo emails = 5; - repeated PendingEmailVerificationInfo pending_email_verifications = 6; - repeated UserSSHKeyInfo ssh_keys = 7; - repeated UserSigningKeyInfo signing_keys = 8; - repeated AccessTokenInfo access_tokens = 9; -} - -message UpdateProfileRequest { - string session_token = 1; - optional string display_name = 2; -} - -message UnlinkIdentityRequest { - string session_token = 1; - string linked_identity_id = 2; -} - -message AddEmailRequest { - string session_token = 1; - string address = 2; -} - -message SetPrimaryEmailRequest { - string session_token = 1; - string address = 2; -} - -message RemoveEmailRequest { - string session_token = 1; - string address = 2; -} - -message RequestAccountDeletionRequest { string session_token = 1; } - -message Organization { - string id = 1; - string slug = 2; - optional string display_name = 3; -} - -message ListOrganizationsRequest { string session_token = 1; } -message ListOrganizationsResponse { repeated Organization organizations = 1; } - -message CreateOrganizationRequest { - string session_token = 1; - string slug = 2; - optional string display_name = 3; -} - -message UpdateOrganizationRequest { - string session_token = 1; - string slug = 2; - optional string display_name = 3; -} - -message DeleteOrganizationRequest { - string session_token = 1; - string slug = 2; -} - -message TeamMember { - string id = 1; - string username = 2; - optional string display_name = 3; -} - -message Team { - string id = 1; - string name = 2; - repeated TeamMember members = 3; -} - -message ListTeamsRequest { - string session_token = 1; - string organization_slug = 2; -} - -message ListTeamsResponse { repeated Team teams = 1; } - -message CreateTeamRequest { - string session_token = 1; - string organization_slug = 2; - string name = 3; -} - -message RenameTeamRequest { - string session_token = 1; - string team_id = 2; - string name = 3; -} - -message DeleteTeamRequest { - string session_token = 1; - string team_id = 2; -} - -message AddTeamMemberRequest { - string session_token = 1; - string team_id = 2; - string username = 3; -} - -message RemoveTeamMemberRequest { - string session_token = 1; - string team_id = 2; - string user_id = 3; -} - -enum GrantPreset { - GRANT_PRESET_UNSPECIFIED = 0; - GRANT_PRESET_READ = 1; - GRANT_PRESET_WRITE = 2; - GRANT_PRESET_ADMIN = 3; -} - -message GrantInfo { - string id = 1; - repeated string capabilities = 2; -} - -message GrantTeamRepositoryAccessRequest { - string session_token = 1; - string team_id = 2; - string repository_name = 3; - GrantPreset preset = 4; -} - -message RevokeGrantRequest { - string session_token = 1; - string grant_id = 2; -} - -enum AgentPolicyMode { - AGENT_POLICY_MODE_UNSPECIFIED = 0; - AGENT_POLICY_MODE_RECORD_ONLY = 1; -} - -message PlatformAgentInfo { - string id = 1; - string name = 2; - string definition = 3; - string status = 4; -} - -message ListPlatformAgentsRequest { - string session_token = 1; - string organization_slug = 2; -} - -message ListPlatformAgentsResponse { - repeated PlatformAgentInfo agents = 1; -} - -message CreatePlatformAgentRequest { - string session_token = 1; - string organization_slug = 2; - string name = 3; - string definition = 4; - AgentPolicyMode data_policy = 5; - AgentPolicyMode execution_policy = 6; - AgentPolicyMode approval_policy = 7; - AgentPolicyMode budget = 8; -} - -message GrantPlatformAgentAccessRequest { - string session_token = 1; - string agent_id = 2; - optional string repository_name = 3; - GrantPreset preset = 4; -} - -message RevokePlatformAgentRequest { - string session_token = 1; - string agent_id = 2; -} - -message AddUserSSHKeyRequest { - string session_token = 1; - string title = 2; - string public_key = 3; -} - -message AddUserSSHKeyResponse { - string id = 1; - string fingerprint = 2; -} - -message RevokeUserSSHKeyRequest { - string session_token = 1; - string key_id = 2; -} - -message AddUserSigningKeyRequest { - string session_token = 1; - string title = 2; - string key_type = 3; - string public_key = 4; -} - -message AddUserSigningKeyResponse { - string id = 1; - string key_id = 2; -} - -message RevokeUserSigningKeyRequest { - string session_token = 1; - string key_id = 2; -} - -message RevokeAccessTokenRequest { - string session_token = 1; - string token_id = 2; -} +syntax = "proto3"; + +package syncode.identity.v1; + +option go_package = "gitea.dev/modules/identityclient/pb"; + +service Identity { + rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse); + rpc CheckCapability(CheckCapabilityRequest) returns (CheckCapabilityResponse); + rpc CheckCapabilities(CheckCapabilitiesRequest) returns (CheckCapabilitiesResponse); + rpc ListPermittedResources(ListPermittedResourcesRequest) returns (ListPermittedResourcesResponse); + rpc ResolveSSHKey(ResolveSSHKeyRequest) returns (ResolveSSHKeyResponse); + rpc ListUserSigningKeys(ListUserSigningKeysRequest) returns (ListUserSigningKeysResponse); + rpc CheckBranchProtectionBypass(CheckBranchProtectionBypassRequest) returns (CheckBranchProtectionBypassResponse); + rpc ListBranchProtectionRules(ListBranchProtectionRulesRequest) returns (ListBranchProtectionRulesResponse); + rpc ResolveRepository(ResolveRepositoryRequest) returns (ResolveRepositoryResponse); + rpc GetRepositoryCoordinates(GetRepositoryCoordinatesRequest) returns (GetRepositoryCoordinatesResponse); + rpc RegisterRepository(RegisterRepositoryRequest) returns (RegisterRepositoryResponse); + rpc ProvisionRepository(ProvisionRepositoryRequest) returns (ProvisionRepositoryResponse); + rpc UpdateRepositoryVisibility(UpdateRepositoryVisibilityRequest) returns (UpdateRepositoryVisibilityResponse); + rpc UpdateRepositoryMetadata(UpdateRepositoryMetadataRequest) returns (UpdateRepositoryMetadataResponse); + rpc ArchiveRepository(ArchiveRepositoryRequest) returns (ArchiveRepositoryResponse); + rpc ArchiveRepositoryProjection(ArchiveRepositoryProjectionRequest) returns (ArchiveRepositoryProjectionResponse); + rpc UnarchiveRepository(UnarchiveRepositoryRequest) returns (UnarchiveRepositoryResponse); + rpc DeleteRepository(DeleteRepositoryRequest) returns (DeleteRepositoryResponse); +} + +message ResolveRepositoryRequest { + string owner = 1; + string name = 2; +} + +message ResolveRepositoryResponse { + string repository_id = 1; +} + +message GetRepositoryCoordinatesRequest { + string repository_id = 1; +} + +message GetRepositoryCoordinatesResponse { + string owner = 1; + string name = 2; +} + +enum RepositoryOwnerKind { + REPOSITORY_OWNER_KIND_UNSPECIFIED = 0; + REPOSITORY_OWNER_KIND_USER = 1; + REPOSITORY_OWNER_KIND_ORGANIZATION = 2; +} + +enum RepositoryVisibility { + REPOSITORY_VISIBILITY_UNSPECIFIED = 0; + REPOSITORY_VISIBILITY_PUBLIC = 1; + REPOSITORY_VISIBILITY_PRIVATE = 2; + REPOSITORY_VISIBILITY_LIMITED = 3; +} + +message RegisterRepositoryRequest { + string repository_id = 1; + RepositoryOwnerKind owner_kind = 2; + string owner_id = 3; + string name = 4; + RepositoryVisibility visibility = 5; + string creator_id = 6; +} + +message RegisterRepositoryResponse {} + +message ProvisionRepositoryRequest { + RepositoryOwnerKind owner_kind = 1; + string owner_id = 2; + string name = 3; + RepositoryVisibility visibility = 4; + string creator_id = 5; +} + +message ProvisionRepositoryResponse { + string repository_id = 1; +} + +message UpdateRepositoryVisibilityRequest { + string repository_id = 1; + RepositoryVisibility visibility = 2; +} + +message UpdateRepositoryVisibilityResponse {} + +message UpdateRepositoryMetadataRequest { + string repository_id = 1; + RepositoryOwnerKind owner_kind = 2; + string owner_id = 3; + string name = 4; + RepositoryVisibility visibility = 5; + string actor_id = 6; +} + +message UpdateRepositoryMetadataResponse {} + +message ArchiveRepositoryRequest { + string repository_id = 1; +} + +message ArchiveRepositoryResponse {} + +message ArchiveRepositoryProjectionRequest { + string repository_id = 1; +} + +message ArchiveRepositoryProjectionResponse {} + +message UnarchiveRepositoryRequest { + string repository_id = 1; +} + +message UnarchiveRepositoryResponse {} + +message DeleteRepositoryRequest { + string repository_id = 1; +} + +message DeleteRepositoryResponse {} + +service LocalAgent { + rpc Create(CreateLocalAgentRequest) returns (CreateLocalAgentResponse); + rpc Enroll(EnrollLocalAgentRequest) returns (EnrollLocalAgentResponse); + rpc Heartbeat(LocalAgentHeartbeatRequest) returns (LocalAgentHeartbeatResponse); + rpc Revoke(RevokeLocalAgentRequest) returns (RevokeLocalAgentResponse); + rpc AddSSHKey(AddLocalAgentSSHKeyRequest) returns (AddLocalAgentSSHKeyResponse); + rpc RevokeSSHKey(RevokeLocalAgentSSHKeyRequest) returns (RevokeLocalAgentSSHKeyResponse); + rpc IssueUserRepositoryToken(IssueUserRepositoryTokenRequest) returns (IssueLocalAgentRepositoryTokenResponse); + rpc IssueRepositoryToken(IssueLocalAgentRepositoryTokenRequest) returns (IssueLocalAgentRepositoryTokenResponse); + rpc IssueUserRepositoryAPIToken(IssueUserRepositoryAPITokenRequest) returns (IssueRepositoryAPITokenResponse); + rpc IssueLocalAgentRepositoryAPIToken(IssueLocalAgentRepositoryAPITokenRequest) returns (IssueRepositoryAPITokenResponse); + rpc CreateAccessToken(CreateAccessTokenRequest) returns (CreateAccessTokenResponse); + rpc Logout(LocalAgentLogoutRequest) returns (LocalAgentLogoutResponse); +} + +service Settings { + rpc GetAccount(GetAccountRequest) returns (GetAccountResponse); + rpc UpdateProfile(UpdateProfileRequest) returns (OperationResponse); + rpc UnlinkIdentity(UnlinkIdentityRequest) returns (OperationResponse); + rpc AddEmail(AddEmailRequest) returns (OperationResponse); + rpc SetPrimaryEmail(SetPrimaryEmailRequest) returns (OperationResponse); + rpc RemoveEmail(RemoveEmailRequest) returns (OperationResponse); + rpc RequestAccountDeletion(RequestAccountDeletionRequest) returns (OperationResponse); + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse); + rpc CreateOrganization(CreateOrganizationRequest) returns (Organization); + rpc UpdateOrganization(UpdateOrganizationRequest) returns (Organization); + rpc DeleteOrganization(DeleteOrganizationRequest) returns (OperationResponse); + rpc ListTeams(ListTeamsRequest) returns (ListTeamsResponse); + rpc CreateTeam(CreateTeamRequest) returns (Team); + rpc RenameTeam(RenameTeamRequest) returns (OperationResponse); + rpc DeleteTeam(DeleteTeamRequest) returns (OperationResponse); + rpc AddTeamMember(AddTeamMemberRequest) returns (OperationResponse); + rpc RemoveTeamMember(RemoveTeamMemberRequest) returns (OperationResponse); + rpc GrantTeamRepositoryAccess(GrantTeamRepositoryAccessRequest) returns (GrantInfo); + rpc RevokeGrant(RevokeGrantRequest) returns (OperationResponse); + rpc ListPlatformAgents(ListPlatformAgentsRequest) returns (ListPlatformAgentsResponse); + rpc CreatePlatformAgent(CreatePlatformAgentRequest) returns (PlatformAgentInfo); + rpc GrantPlatformAgentAccess(GrantPlatformAgentAccessRequest) returns (GrantInfo); + rpc RevokePlatformAgent(RevokePlatformAgentRequest) returns (OperationResponse); + rpc AddUserSSHKey(AddUserSSHKeyRequest) returns (AddUserSSHKeyResponse); + rpc RevokeUserSSHKey(RevokeUserSSHKeyRequest) returns (OperationResponse); + rpc AddUserSigningKey(AddUserSigningKeyRequest) returns (AddUserSigningKeyResponse); + rpc RevokeUserSigningKey(RevokeUserSigningKeyRequest) returns (OperationResponse); + rpc RevokeAccessToken(RevokeAccessTokenRequest) returns (OperationResponse); +} + +service ForgeOperations { + rpc CreatePullRequest(CreatePullRequestOperation) returns (PullRequestInfo); + rpc ListPullRequests(ListPullRequestsOperation) returns (ListPullRequestsResponse); + rpc GetPullRequest(GetPullRequestOperation) returns (PullRequestInfo); + rpc MergePullRequest(MergePullRequestOperation) returns (OperationResponse); + rpc ListIssues(ListIssuesOperation) returns (ListIssuesResponse); + rpc CreateIssueComment(CreateIssueCommentOperation) returns (IssueCommentInfo); +} + +enum ForgeListState { + FORGE_LIST_STATE_UNSPECIFIED = 0; + FORGE_LIST_STATE_OPEN = 1; + FORGE_LIST_STATE_CLOSED = 2; + FORGE_LIST_STATE_ALL = 3; +} + +enum ForgeItemState { + FORGE_ITEM_STATE_UNSPECIFIED = 0; + FORGE_ITEM_STATE_OPEN = 1; + FORGE_ITEM_STATE_CLOSED = 2; +} + +enum ForgeMergeMethod { + FORGE_MERGE_METHOD_UNSPECIFIED = 0; + FORGE_MERGE_METHOD_MERGE = 1; + FORGE_MERGE_METHOD_REBASE = 2; + FORGE_MERGE_METHOD_REBASE_MERGE = 3; + FORGE_MERGE_METHOD_SQUASH = 4; + FORGE_MERGE_METHOD_FAST_FORWARD_ONLY = 5; +} + +message ForgeRepository { + string owner = 1; + string name = 2; +} + +message CreatePullRequestOperation { + string api_token = 1; + ForgeRepository repository = 2; + string head = 3; + string base = 4; + string title = 5; + string body = 6; +} + +message ListPullRequestsOperation { + string api_token = 1; + ForgeRepository repository = 2; + ForgeListState state = 3; +} + +message GetPullRequestOperation { + string api_token = 1; + ForgeRepository repository = 2; + uint64 number = 3; +} + +message MergePullRequestOperation { + string api_token = 1; + ForgeRepository repository = 2; + uint64 number = 3; + ForgeMergeMethod method = 4; + bool delete_branch = 5; +} + +message PullRequestInfo { + uint64 number = 1; + string title = 2; + ForgeItemState state = 3; + string web_url = 4; + optional string body = 5; + string head = 6; + string base = 7; + bool merged = 8; +} + +message ListPullRequestsResponse { repeated PullRequestInfo pull_requests = 1; } + +message ListIssuesOperation { + string api_token = 1; + ForgeRepository repository = 2; + ForgeListState state = 3; +} + +message IssueInfo { + uint64 number = 1; + string title = 2; + ForgeItemState state = 3; + string web_url = 4; +} + +message ListIssuesResponse { repeated IssueInfo issues = 1; } + +message CreateIssueCommentOperation { + string api_token = 1; + ForgeRepository repository = 2; + uint64 number = 3; + string body = 4; +} + +message IssueCommentInfo { + uint64 id = 1; + string web_url = 2; +} + +enum PrincipalKind { + PRINCIPAL_KIND_UNSPECIFIED = 0; + PRINCIPAL_KIND_USER = 1; + PRINCIPAL_KIND_PLATFORM_AGENT = 2; + PRINCIPAL_KIND_LOCAL_AGENT = 3; + PRINCIPAL_KIND_ACCESS_TOKEN = 4; +} + +enum ResourceKind { + RESOURCE_KIND_UNSPECIFIED = 0; + RESOURCE_KIND_ORGANIZATION = 1; + RESOURCE_KIND_REPOSITORY = 2; + RESOURCE_KIND_INSTANCE = 3; +} + +message ValidateSessionRequest { + string session_token = 1; +} + +message ValidateSessionResponse { + string principal_id = 1; + PrincipalKind principal_kind = 2; + int64 expires_at_unix = 3; + string owner_user_id = 4; + repeated string capabilities = 5; + string audience = 6; + ResourceKind resource_kind = 7; + string resource_id = 8; +} + +message CheckCapabilityRequest { + string principal_id = 1; + PrincipalKind principal_kind = 2; + ResourceKind resource_kind = 3; + // Empty when resource_kind is RESOURCE_KIND_INSTANCE. + string resource_id = 4; + string capability = 5; +} + +message CheckCapabilityResponse { + bool allowed = 1; +} + +message CapabilityQuery { + ResourceKind resource_kind = 1; + // Empty when resource_kind is RESOURCE_KIND_INSTANCE. + string resource_id = 2; + string capability = 3; +} + +message CheckCapabilitiesRequest { + string principal_id = 1; + PrincipalKind principal_kind = 2; + repeated CapabilityQuery queries = 3; +} + +message CheckCapabilitiesResponse { + // One decision per query, in the order the queries were given. + repeated bool allowed = 1; +} + +message ListPermittedResourcesRequest { + string principal_id = 1; + PrincipalKind principal_kind = 2; + ResourceKind resource_kind = 3; + string capability = 4; +} + +message ListPermittedResourcesResponse { + // Resources the principal holds a grant on. + repeated string resource_ids = 1; + // True when every public repository also qualifies. Read access to a public + // repository comes from its visibility, not from a grant, so a caller that + // ignores this hides public repositories from someone entitled to see them. + bool includes_public = 2; +} + +message ResolveSSHKeyRequest { + string fingerprint = 1; +} + +message ResolveSSHKeyResponse { + string principal_id = 1; + PrincipalKind principal_kind = 2; + string owner_user_id = 3; +} + +message ListUserSigningKeysRequest { + string user_id = 1; +} + +message SigningKey { + string key_type = 1; + string key_id = 2; + string public_key = 3; +} + +message ListUserSigningKeysResponse { + repeated SigningKey keys = 1; +} + +message CheckBranchProtectionBypassRequest { + string repository_id = 1; + string pattern = 2; + string principal_id = 3; + PrincipalKind principal_kind = 4; +} + +message CheckBranchProtectionBypassResponse { + bool allowed = 1; + bool protected = 2; + string matched_pattern = 3; + int32 require_review_count = 4; + repeated string required_status_checks = 5; +} + +message ListBranchProtectionRulesRequest { + string repository_id = 1; +} + +message BranchProtectionRule { + string pattern = 1; + int32 require_review_count = 2; + repeated string required_status_checks = 3; +} + +message ListBranchProtectionRulesResponse { + repeated BranchProtectionRule rules = 1; +} + +message CreateLocalAgentRequest { + string session_token = 1; + string owner = 2; + string name = 3; + string definition = 4; + repeated string restriction = 5; +} + +message CreateLocalAgentResponse { + string agent_id = 1; +} + +message EnrollLocalAgentRequest { + string session_token = 1; + string owner = 2; + string name = 3; + string instance_host = 4; +} + +message EnrollLocalAgentResponse { + string agent_id = 1; + int64 lease_term = 2; + string fencing_token = 3; + string credential = 4; + int64 lease_expires_at_unix = 5; + uint32 heartbeat_interval_seconds = 6; +} + +message LocalAgentHeartbeatRequest { + string agent_id = 1; + string instance_host = 2; + int64 lease_term = 3; + string fencing_token = 4; + string credential = 5; +} + +message LocalAgentHeartbeatResponse { + string credential = 1; + int64 lease_expires_at_unix = 2; + uint32 heartbeat_interval_seconds = 3; +} + +message RevokeLocalAgentRequest { + string session_token = 1; + string agent_id = 2; +} + +message RevokeLocalAgentResponse {} + +message AddLocalAgentSSHKeyRequest { + string session_token = 1; + string agent_id = 2; + string public_key = 3; +} + +message AddLocalAgentSSHKeyResponse { + string key_id = 1; + string fingerprint = 2; +} + +message RevokeLocalAgentSSHKeyRequest { + string session_token = 1; + string key_id = 2; +} + +message RevokeLocalAgentSSHKeyResponse {} + +message IssueLocalAgentRepositoryTokenRequest { + string agent_id = 1; + string instance_host = 2; + int64 lease_term = 3; + string fencing_token = 4; + string credential = 5; + string repository_owner = 6; + string repository_name = 7; +} + +message IssueUserRepositoryTokenRequest { + string session_token = 1; + string repository_owner = 2; + string repository_name = 3; +} + +message IssueLocalAgentRepositoryTokenResponse { + string token = 1; + int64 expires_at_unix = 2; +} + +message IssueUserRepositoryAPITokenRequest { + string session_token = 1; + string repository_owner = 2; + string repository_name = 3; + repeated string capabilities = 4; +} + +message IssueLocalAgentRepositoryAPITokenRequest { + string agent_id = 1; + string instance_host = 2; + int64 lease_term = 3; + string fencing_token = 4; + string credential = 5; + string repository_owner = 6; + string repository_name = 7; + repeated string capabilities = 8; +} + +message IssueRepositoryAPITokenResponse { + string token = 1; + int64 expires_at_unix = 2; +} + +message CreateAccessTokenRequest { + string session_token = 1; + string name = 2; + repeated string capabilities = 3; + int64 expires_in_seconds = 4; +} + +message CreateAccessTokenResponse { + string token = 1; + int64 expires_at_unix = 2; +} + +message LocalAgentLogoutRequest { string session_token = 1; } + +message LocalAgentLogoutResponse {} + +message OperationResponse {} + +message GetAccountRequest { string session_token = 1; } + +message LinkedIdentityInfo { + string id = 1; + string provider = 2; + string provider_display_name = 3; + optional string external_username = 4; + optional string avatar_url = 5; + int64 linked_at_unix = 6; +} + +message UserEmailInfo { + string address = 1; + bool verified = 2; + bool is_primary = 3; + repeated string sources = 4; +} + +message PendingEmailVerificationInfo { + string address = 1; + int64 expires_at_unix = 2; +} + +message UserSSHKeyInfo { + string id = 1; + string title = 2; + string fingerprint = 3; + string public_key = 4; + int64 added_at_unix = 5; +} + +message UserSigningKeyInfo { + string id = 1; + string title = 2; + string key_type = 3; + string key_id = 4; + string public_key = 5; + int64 added_at_unix = 6; +} + +message AccessTokenInfo { + string id = 1; + string name = 2; + repeated string capabilities = 3; + int64 expires_at_unix = 4; + int64 created_at_unix = 5; +} + +message GetAccountResponse { + string id = 1; + string username = 2; + optional string display_name = 3; + repeated LinkedIdentityInfo linked_identities = 4; + repeated UserEmailInfo emails = 5; + repeated PendingEmailVerificationInfo pending_email_verifications = 6; + repeated UserSSHKeyInfo ssh_keys = 7; + repeated UserSigningKeyInfo signing_keys = 8; + repeated AccessTokenInfo access_tokens = 9; +} + +message UpdateProfileRequest { + string session_token = 1; + optional string display_name = 2; +} + +message UnlinkIdentityRequest { + string session_token = 1; + string linked_identity_id = 2; +} + +message AddEmailRequest { + string session_token = 1; + string address = 2; +} + +message SetPrimaryEmailRequest { + string session_token = 1; + string address = 2; +} + +message RemoveEmailRequest { + string session_token = 1; + string address = 2; +} + +message RequestAccountDeletionRequest { string session_token = 1; } + +message Organization { + string id = 1; + string slug = 2; + optional string display_name = 3; +} + +message ListOrganizationsRequest { string session_token = 1; } +message ListOrganizationsResponse { repeated Organization organizations = 1; } + +message CreateOrganizationRequest { + string session_token = 1; + string slug = 2; + optional string display_name = 3; +} + +message UpdateOrganizationRequest { + string session_token = 1; + string slug = 2; + optional string display_name = 3; +} + +message DeleteOrganizationRequest { + string session_token = 1; + string slug = 2; +} + +message TeamMember { + string id = 1; + string username = 2; + optional string display_name = 3; +} + +message Team { + string id = 1; + string name = 2; + repeated TeamMember members = 3; +} + +message ListTeamsRequest { + string session_token = 1; + string organization_slug = 2; +} + +message ListTeamsResponse { repeated Team teams = 1; } + +message CreateTeamRequest { + string session_token = 1; + string organization_slug = 2; + string name = 3; +} + +message RenameTeamRequest { + string session_token = 1; + string team_id = 2; + string name = 3; +} + +message DeleteTeamRequest { + string session_token = 1; + string team_id = 2; +} + +message AddTeamMemberRequest { + string session_token = 1; + string team_id = 2; + string username = 3; +} + +message RemoveTeamMemberRequest { + string session_token = 1; + string team_id = 2; + string user_id = 3; +} + +enum GrantPreset { + GRANT_PRESET_UNSPECIFIED = 0; + GRANT_PRESET_READ = 1; + GRANT_PRESET_WRITE = 2; + GRANT_PRESET_ADMIN = 3; +} + +message GrantInfo { + string id = 1; + repeated string capabilities = 2; +} + +message GrantTeamRepositoryAccessRequest { + string session_token = 1; + string team_id = 2; + string repository_name = 3; + GrantPreset preset = 4; +} + +message RevokeGrantRequest { + string session_token = 1; + string grant_id = 2; +} + +enum AgentPolicyMode { + AGENT_POLICY_MODE_UNSPECIFIED = 0; + AGENT_POLICY_MODE_RECORD_ONLY = 1; +} + +message PlatformAgentInfo { + string id = 1; + string name = 2; + string definition = 3; + string status = 4; +} + +message ListPlatformAgentsRequest { + string session_token = 1; + string organization_slug = 2; +} + +message ListPlatformAgentsResponse { + repeated PlatformAgentInfo agents = 1; +} + +message CreatePlatformAgentRequest { + string session_token = 1; + string organization_slug = 2; + string name = 3; + string definition = 4; + AgentPolicyMode data_policy = 5; + AgentPolicyMode execution_policy = 6; + AgentPolicyMode approval_policy = 7; + AgentPolicyMode budget = 8; +} + +message GrantPlatformAgentAccessRequest { + string session_token = 1; + string agent_id = 2; + optional string repository_name = 3; + GrantPreset preset = 4; +} + +message RevokePlatformAgentRequest { + string session_token = 1; + string agent_id = 2; +} + +message AddUserSSHKeyRequest { + string session_token = 1; + string title = 2; + string public_key = 3; +} + +message AddUserSSHKeyResponse { + string id = 1; + string fingerprint = 2; +} + +message RevokeUserSSHKeyRequest { + string session_token = 1; + string key_id = 2; +} + +message AddUserSigningKeyRequest { + string session_token = 1; + string title = 2; + string key_type = 3; + string public_key = 4; +} + +message AddUserSigningKeyResponse { + string id = 1; + string key_id = 2; +} + +message RevokeUserSigningKeyRequest { + string session_token = 1; + string key_id = 2; +} + +message RevokeAccessTokenRequest { + string session_token = 1; + string token_id = 2; +} diff --git a/crates/api-grpc/src/service.rs b/crates/api-grpc/src/service.rs --- a/crates/api-grpc/src/service.rs +++ b/crates/api-grpc/src/service.rs @@ -1,244 +1,249 @@ -use std::sync::Arc; - -use syncode_identity_application::IdentityBridgeUseCases; -use tonic::{Request, Response, Status}; - -use self::conversion::{ - application_error, application_principal, parse_resource, parse_resource_kind, - wire_principal_kind, wire_resource, -}; - -use crate::wire::identity_server::Identity; -use crate::wire::{ - ArchiveRepositoryProjectionRequest, ArchiveRepositoryProjectionResponse, - ArchiveRepositoryRequest, ArchiveRepositoryResponse, CheckBranchProtectionBypassRequest, - CheckBranchProtectionBypassResponse, CheckCapabilitiesRequest, CheckCapabilitiesResponse, - CheckCapabilityRequest, CheckCapabilityResponse, DeleteRepositoryRequest, - DeleteRepositoryResponse, ListBranchProtectionRulesRequest, ListBranchProtectionRulesResponse, - ListPermittedResourcesRequest, ListPermittedResourcesResponse, ListUserSigningKeysRequest, - ListUserSigningKeysResponse, PrincipalKind, ProvisionRepositoryRequest, - ProvisionRepositoryResponse, RegisterRepositoryRequest, RegisterRepositoryResponse, - ResolveRepositoryRequest, ResolveRepositoryResponse, ResolveSshKeyRequest, - ResolveSshKeyResponse, UnarchiveRepositoryRequest, UnarchiveRepositoryResponse, - UpdateRepositoryMetadataRequest, UpdateRepositoryMetadataResponse, - UpdateRepositoryVisibilityRequest, UpdateRepositoryVisibilityResponse, ValidateSessionRequest, - ValidateSessionResponse, -}; - -mod access; -mod conversion; -mod repository; -mod server; - -#[derive(Clone)] -pub struct IdentityServer { - application: Arc, -} - -#[tonic::async_trait] -impl Identity for IdentityServer { - async fn resolve_repository( - &self, - request: Request, - ) -> Result, Status> { - repository::resolve(self, request).await - } - - async fn register_repository( - &self, - request: Request, - ) -> Result, Status> { - repository::register(self, request).await - } - - async fn validate_session( - &self, - request: Request, - ) -> Result, Status> { - let token = request.into_inner().session_token; - let credential = self - .application - .validate_credential(&token) - .await - .map_err(application_error)?; - let (resource_kind, resource_id) = wire_resource(credential.resource); - Ok(Response::new(ValidateSessionResponse { - principal_id: credential.principal.get().to_string(), - principal_kind: wire_principal_kind(credential.principal.kind()) as i32, - expires_at_unix: credential.expires_at.timestamp(), - owner_user_id: credential.owner_user_id.to_string(), - capabilities: credential - .capabilities - .iter() - .map(ToString::to_string) - .collect(), - audience: credential.audience, - resource_kind: resource_kind as i32, - resource_id, - })) - } - - async fn check_capability( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - let resource = parse_resource(request.resource_kind(), &request.resource_id)?; - - let principal = application_principal(request.principal_kind(), &request.principal_id)?; - let capability = request - .capability - .parse() - .map_err(|_| Status::invalid_argument("unsupported capability"))?; - let allowed = self - .application - .check_capability(principal, resource, capability) - .await - .map_err(application_error)?; - - Ok(Response::new(CheckCapabilityResponse { allowed })) - } - - async fn check_capabilities( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - let principal = application_principal(request.principal_kind(), &request.principal_id)?; - let mut pairs = Vec::with_capacity(request.queries.len()); - for query in &request.queries { - let resource = parse_resource(query.resource_kind(), &query.resource_id)?; - let capability = query - .capability - .parse() - .map_err(|_| Status::invalid_argument("unsupported capability"))?; - pairs.push((resource, capability)); - } - let allowed = self - .application - .check_capabilities(principal, &pairs) - .await - .map_err(application_error)?; - Ok(Response::new(CheckCapabilitiesResponse { allowed })) - } - - async fn list_permitted_resources( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - let principal = application_principal(request.principal_kind(), &request.principal_id)?; - let resource_kind = parse_resource_kind(request.resource_kind())?; - let capability = request - .capability - .parse() - .map_err(|_| Status::invalid_argument("unsupported capability"))?; - let permitted = self - .application - .list_permitted_resources(principal, resource_kind, capability) - .await - .map_err(application_error)?; - Ok(Response::new(ListPermittedResourcesResponse { - resource_ids: permitted - .resource_ids - .iter() - .map(ToString::to_string) - .collect(), - includes_public: permitted.includes_public, - })) - } - - async fn resolve_ssh_key( - &self, - request: Request, - ) -> Result, Status> { - let fingerprint = request.into_inner().fingerprint; - if fingerprint.is_empty() { - return Err(Status::invalid_argument("fingerprint is required")); - } - let owner = self - .application - .resolve_ssh_key(&fingerprint) - .await - .map_err(application_error)?; - let principal_kind = match owner.owner { - syncode_identity_application::SshKeyOwnerId::User(_) => PrincipalKind::User, - syncode_identity_application::SshKeyOwnerId::LocalAgent(_) => PrincipalKind::LocalAgent, - }; - Ok(Response::new(ResolveSshKeyResponse { - principal_id: owner.owner.get().to_string(), - principal_kind: principal_kind as i32, - owner_user_id: owner.owner_user_id.to_string(), - })) - } - - async fn check_branch_protection_bypass( - &self, - request: Request, - ) -> Result, Status> { - access::check_branch_protection_bypass(self, request).await - } - - async fn list_branch_protection_rules( - &self, - request: Request, - ) -> Result, Status> { - access::list_branch_protection_rules(self, request).await - } - - async fn list_user_signing_keys( - &self, - request: Request, - ) -> Result, Status> { - access::list_user_signing_keys(self, request).await - } - - async fn provision_repository( - &self, - request: Request, - ) -> Result, Status> { - repository::provision(self, request).await - } - - async fn update_repository_visibility( - &self, - request: Request, - ) -> Result, Status> { - repository::update_visibility(self, request).await - } - - async fn update_repository_metadata( - &self, - request: Request, - ) -> Result, Status> { - repository::update_metadata(self, request).await - } - - async fn archive_repository( - &self, - request: Request, - ) -> Result, Status> { - repository::archive_repository(self, request).await - } - - async fn archive_repository_projection( - &self, - request: Request, - ) -> Result, Status> { - repository::archive(self, request).await - } - - async fn unarchive_repository( - &self, - request: Request, - ) -> Result, Status> { - repository::unarchive(self, request).await - } - - async fn delete_repository( - &self, - request: Request, - ) -> Result, Status> { - repository::delete(self, request).await - } -} +use std::sync::Arc; + +use syncode_identity_application::IdentityBridgeUseCases; +use tonic::{Request, Response, Status}; + +use self::conversion::{ + application_error, application_principal, parse_resource, parse_resource_kind, + wire_principal_kind, wire_resource, +}; + +use crate::wire::identity_server::Identity; +use crate::wire::{ + ArchiveRepositoryProjectionRequest, ArchiveRepositoryProjectionResponse, + ArchiveRepositoryRequest, ArchiveRepositoryResponse, CheckBranchProtectionBypassRequest, + CheckBranchProtectionBypassResponse, CheckCapabilitiesRequest, CheckCapabilitiesResponse, + CheckCapabilityRequest, CheckCapabilityResponse, DeleteRepositoryRequest, + DeleteRepositoryResponse, GetRepositoryCoordinatesRequest, GetRepositoryCoordinatesResponse, + ListBranchProtectionRulesRequest, ListBranchProtectionRulesResponse, + ListPermittedResourcesRequest, ListPermittedResourcesResponse, ListUserSigningKeysRequest, + ListUserSigningKeysResponse, PrincipalKind, ProvisionRepositoryRequest, + ProvisionRepositoryResponse, RegisterRepositoryRequest, RegisterRepositoryResponse, + ResolveRepositoryRequest, ResolveRepositoryResponse, ResolveSshKeyRequest, + ResolveSshKeyResponse, UnarchiveRepositoryRequest, UnarchiveRepositoryResponse, + UpdateRepositoryMetadataRequest, UpdateRepositoryMetadataResponse, + UpdateRepositoryVisibilityRequest, UpdateRepositoryVisibilityResponse, ValidateSessionRequest, + ValidateSessionResponse, +}; + +mod access; +mod conversion; +mod repository; +mod server; + +#[derive(Clone)] +pub struct IdentityServer { + application: Arc, +} + +#[tonic::async_trait] +impl Identity for IdentityServer { + async fn resolve_repository( + &self, + request: Request, + ) -> Result, Status> { + repository::resolve(self, request).await + } + async fn get_repository_coordinates( + &self, + request: Request, + ) -> Result, Status> { + repository::coordinates(self, request).await + } + async fn register_repository( + &self, + request: Request, + ) -> Result, Status> { + repository::register(self, request).await + } + async fn validate_session( + &self, + request: Request, + ) -> Result, Status> { + let token = request.into_inner().session_token; + let credential = self + .application + .validate_credential(&token) + .await + .map_err(application_error)?; + let (resource_kind, resource_id) = wire_resource(credential.resource); + Ok(Response::new(ValidateSessionResponse { + principal_id: credential.principal.get().to_string(), + principal_kind: wire_principal_kind(credential.principal.kind()) as i32, + expires_at_unix: credential.expires_at.timestamp(), + owner_user_id: credential.owner_user_id.to_string(), + capabilities: credential + .capabilities + .iter() + .map(ToString::to_string) + .collect(), + audience: credential.audience, + resource_kind: resource_kind as i32, + resource_id, + })) + } + + async fn check_capability( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let resource = parse_resource(request.resource_kind(), &request.resource_id)?; + + let principal = application_principal(request.principal_kind(), &request.principal_id)?; + let capability = request + .capability + .parse() + .map_err(|_| Status::invalid_argument("unsupported capability"))?; + let allowed = self + .application + .check_capability(principal, resource, capability) + .await + .map_err(application_error)?; + + Ok(Response::new(CheckCapabilityResponse { allowed })) + } + + async fn check_capabilities( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let principal = application_principal(request.principal_kind(), &request.principal_id)?; + let mut pairs = Vec::with_capacity(request.queries.len()); + for query in &request.queries { + let resource = parse_resource(query.resource_kind(), &query.resource_id)?; + let capability = query + .capability + .parse() + .map_err(|_| Status::invalid_argument("unsupported capability"))?; + pairs.push((resource, capability)); + } + let allowed = self + .application + .check_capabilities(principal, &pairs) + .await + .map_err(application_error)?; + Ok(Response::new(CheckCapabilitiesResponse { allowed })) + } + + async fn list_permitted_resources( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let principal = application_principal(request.principal_kind(), &request.principal_id)?; + let resource_kind = parse_resource_kind(request.resource_kind())?; + let capability = request + .capability + .parse() + .map_err(|_| Status::invalid_argument("unsupported capability"))?; + let permitted = self + .application + .list_permitted_resources(principal, resource_kind, capability) + .await + .map_err(application_error)?; + Ok(Response::new(ListPermittedResourcesResponse { + resource_ids: permitted + .resource_ids + .iter() + .map(ToString::to_string) + .collect(), + includes_public: permitted.includes_public, + })) + } + + async fn resolve_ssh_key( + &self, + request: Request, + ) -> Result, Status> { + let fingerprint = request.into_inner().fingerprint; + if fingerprint.is_empty() { + return Err(Status::invalid_argument("fingerprint is required")); + } + let owner = self + .application + .resolve_ssh_key(&fingerprint) + .await + .map_err(application_error)?; + let principal_kind = match owner.owner { + syncode_identity_application::SshKeyOwnerId::User(_) => PrincipalKind::User, + syncode_identity_application::SshKeyOwnerId::LocalAgent(_) => PrincipalKind::LocalAgent, + }; + Ok(Response::new(ResolveSshKeyResponse { + principal_id: owner.owner.get().to_string(), + principal_kind: principal_kind as i32, + owner_user_id: owner.owner_user_id.to_string(), + })) + } + + async fn check_branch_protection_bypass( + &self, + request: Request, + ) -> Result, Status> { + access::check_branch_protection_bypass(self, request).await + } + + async fn list_branch_protection_rules( + &self, + request: Request, + ) -> Result, Status> { + access::list_branch_protection_rules(self, request).await + } + + async fn list_user_signing_keys( + &self, + request: Request, + ) -> Result, Status> { + access::list_user_signing_keys(self, request).await + } + + async fn provision_repository( + &self, + request: Request, + ) -> Result, Status> { + repository::provision(self, request).await + } + + async fn update_repository_visibility( + &self, + request: Request, + ) -> Result, Status> { + repository::update_visibility(self, request).await + } + + async fn update_repository_metadata( + &self, + request: Request, + ) -> Result, Status> { + repository::update_metadata(self, request).await + } + + async fn archive_repository( + &self, + request: Request, + ) -> Result, Status> { + repository::archive_repository(self, request).await + } + + async fn archive_repository_projection( + &self, + request: Request, + ) -> Result, Status> { + repository::archive(self, request).await + } + + async fn unarchive_repository( + &self, + request: Request, + ) -> Result, Status> { + repository::unarchive(self, request).await + } + + async fn delete_repository( + &self, + request: Request, + ) -> Result, Status> { + repository::delete(self, request).await + } +} diff --git a/crates/application/src/bridge.rs b/crates/application/src/bridge.rs --- a/crates/application/src/bridge.rs +++ b/crates/application/src/bridge.rs @@ -1,234 +1,242 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use syncode_identity_model::{ - AccessTokenId, Capability, Grant, GrantPrincipal, LocalAgentId, PlatformAgentId, RepositoryId, - RepositoryName, RepositoryOwner, RepositoryOwnerId, RepositoryVisibility, Resource, UserId, - UserPrincipal, -}; - -use crate::ApplicationError; - -pub use access::PermittedResources; -mod access; -mod repository; -mod types; - -pub use types::*; -#[async_trait] -pub trait IdentityBridgeRepository: Send + Sync { - async fn validate_web_session( - &self, - token: &str, - ) -> Result)>, ApplicationError>; - async fn validate_access_token( - &self, - token: &str, - ) -> Result, ApplicationError>; - async fn load_access_token( - &self, - token_id: AccessTokenId, - ) -> Result, ApplicationError>; - async fn load_user_principal(&self, user_id: UserId) - -> Result; - async fn load_active_grants(&self, resource: Resource) -> Result, ApplicationError>; - /// `load_active_grants` inverted: what a principal can reach, so listing - /// many resources costs one query instead of one per resource. - async fn load_granted_resources( - &self, - principals: &[(syncode_identity_model::GrantPrincipalKind, uuid::Uuid)], - resource_kind: syncode_identity_model::ResourceKind, - ) -> Result< - Vec<( - uuid::Uuid, - std::collections::BTreeSet, - )>, - ApplicationError, - >; - async fn load_active_repository_visibility( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError>; - async fn load_active_repository_owner( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError>; - async fn platform_agent_is_active( - &self, - agent_id: PlatformAgentId, - ) -> Result; - async fn local_agent( - &self, - agent_id: LocalAgentId, - ) -> Result, ApplicationError>; - async fn resolve_ssh_key( - &self, - fingerprint: &str, - ) -> Result, ApplicationError>; - async fn branch_protection_bypass_allowed( - &self, - repository_id: RepositoryId, - pattern: &str, - principal: GrantPrincipal, - ) -> Result; - async fn active_branch_protection_rules( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError>; - async fn user_signing_keys( - &self, - user_id: UserId, - ) -> Result, ApplicationError>; - async fn resolve_repository( - &self, - owner: &RepositoryOwner, - name: &RepositoryName, - ) -> Result, ApplicationError>; - async fn provision_repository( - &self, - owner: RepositoryOwnerId, - name: &RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result; - async fn register_repository( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: &RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result<(), ApplicationError>; - async fn update_repository_visibility( - &self, - repository_id: RepositoryId, - visibility: RepositoryVisibility, - ) -> Result; - async fn update_repository_metadata( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: &RepositoryName, - visibility: RepositoryVisibility, - actor_id: UserId, - ) -> Result; - async fn archive_repository( - &self, - repository_id: RepositoryId, - ) -> Result; - async fn set_repository_archived( - &self, - repository_id: RepositoryId, - archived: bool, - ) -> Result; -} - -#[async_trait] -pub trait IdentityBridgeAccessUseCases: Send + Sync { - async fn validate_credential( - &self, - token: &str, - ) -> Result; - async fn check_capability( - &self, - principal: BridgePrincipalId, - resource: Resource, - capability: Capability, - ) -> Result; - /// One round trip for many resources. Same decision as `check_capability`, - /// answered per pair, in order. - async fn check_capabilities( - &self, - principal: BridgePrincipalId, - pairs: &[(Resource, Capability)], - ) -> Result, ApplicationError>; - async fn list_permitted_resources( - &self, - principal: BridgePrincipalId, - resource_kind: syncode_identity_model::ResourceKind, - capability: Capability, - ) -> Result; - async fn resolve_ssh_key(&self, fingerprint: &str) -> Result; - async fn check_branch_protection_bypass( - &self, - repository_id: RepositoryId, - pattern: &str, - principal: Option, - ) -> Result; - async fn list_branch_protection_rules( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError>; - async fn list_user_signing_keys( - &self, - user_id: UserId, - ) -> Result, ApplicationError>; -} - -#[async_trait] -pub trait IdentityBridgeRepositoryUseCases: Send + Sync { - async fn resolve_repository( - &self, - owner: RepositoryOwner, - name: RepositoryName, - ) -> Result; - async fn register_repository( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result<(), ApplicationError>; - async fn provision_repository( - &self, - owner: RepositoryOwnerId, - name: RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result; - async fn update_repository_visibility( - &self, - repository_id: RepositoryId, - visibility: RepositoryVisibility, - ) -> Result<(), ApplicationError>; - async fn update_repository_metadata( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: RepositoryName, - visibility: RepositoryVisibility, - actor_id: UserId, - ) -> Result<(), ApplicationError>; - async fn archive_repository(&self, repository_id: RepositoryId) - -> Result<(), ApplicationError>; - async fn set_repository_archived( - &self, - repository_id: RepositoryId, - archived: bool, - ) -> Result<(), ApplicationError>; - async fn delete_repository(&self, repository_id: RepositoryId) -> Result<(), ApplicationError>; -} - -pub trait IdentityBridgeUseCases: - IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases -{ -} - -impl IdentityBridgeUseCases for T where - T: IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases + ?Sized -{ -} - -#[derive(Clone)] -pub struct IdentityBridgeApplication { - repository: Arc, -} - -impl IdentityBridgeApplication { - #[must_use] - pub fn new(repository: Arc) -> Self { - Self { repository } - } -} +use std::sync::Arc; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use syncode_identity_model::{ + AccessTokenId, Capability, Grant, GrantPrincipal, LocalAgentId, PlatformAgentId, + RepositoryCoordinates, RepositoryId, RepositoryName, RepositoryOwner, RepositoryOwnerId, + RepositoryVisibility, Resource, UserId, UserPrincipal, +}; + +use crate::ApplicationError; + +pub use access::PermittedResources; +mod access; +mod repository; +mod types; + +pub use types::*; +#[async_trait] +pub trait IdentityBridgeRepository: Send + Sync { + async fn validate_web_session( + &self, + token: &str, + ) -> Result)>, ApplicationError>; + async fn validate_access_token( + &self, + token: &str, + ) -> Result, ApplicationError>; + async fn load_access_token( + &self, + token_id: AccessTokenId, + ) -> Result, ApplicationError>; + async fn load_user_principal(&self, user_id: UserId) + -> Result; + async fn load_active_grants(&self, resource: Resource) -> Result, ApplicationError>; + /// `load_active_grants` inverted: what a principal can reach, so listing + /// many resources costs one query instead of one per resource. + async fn load_granted_resources( + &self, + principals: &[(syncode_identity_model::GrantPrincipalKind, uuid::Uuid)], + resource_kind: syncode_identity_model::ResourceKind, + ) -> Result< + Vec<( + uuid::Uuid, + std::collections::BTreeSet, + )>, + ApplicationError, + >; + async fn load_active_repository_visibility( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError>; + async fn load_active_repository_owner( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError>; + async fn platform_agent_is_active( + &self, + agent_id: PlatformAgentId, + ) -> Result; + async fn local_agent( + &self, + agent_id: LocalAgentId, + ) -> Result, ApplicationError>; + async fn resolve_ssh_key( + &self, + fingerprint: &str, + ) -> Result, ApplicationError>; + async fn branch_protection_bypass_allowed( + &self, + repository_id: RepositoryId, + pattern: &str, + principal: GrantPrincipal, + ) -> Result; + async fn active_branch_protection_rules( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError>; + async fn user_signing_keys( + &self, + user_id: UserId, + ) -> Result, ApplicationError>; + async fn resolve_repository( + &self, + owner: &RepositoryOwner, + name: &RepositoryName, + ) -> Result, ApplicationError>; + async fn repository_coordinates( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError>; + async fn provision_repository( + &self, + owner: RepositoryOwnerId, + name: &RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result; + async fn register_repository( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: &RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result<(), ApplicationError>; + async fn update_repository_visibility( + &self, + repository_id: RepositoryId, + visibility: RepositoryVisibility, + ) -> Result; + async fn update_repository_metadata( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: &RepositoryName, + visibility: RepositoryVisibility, + actor_id: UserId, + ) -> Result; + async fn archive_repository( + &self, + repository_id: RepositoryId, + ) -> Result; + async fn set_repository_archived( + &self, + repository_id: RepositoryId, + archived: bool, + ) -> Result; +} + +#[async_trait] +pub trait IdentityBridgeAccessUseCases: Send + Sync { + async fn validate_credential( + &self, + token: &str, + ) -> Result; + async fn check_capability( + &self, + principal: BridgePrincipalId, + resource: Resource, + capability: Capability, + ) -> Result; + /// One round trip for many resources. Same decision as `check_capability`, + /// answered per pair, in order. + async fn check_capabilities( + &self, + principal: BridgePrincipalId, + pairs: &[(Resource, Capability)], + ) -> Result, ApplicationError>; + async fn list_permitted_resources( + &self, + principal: BridgePrincipalId, + resource_kind: syncode_identity_model::ResourceKind, + capability: Capability, + ) -> Result; + async fn resolve_ssh_key(&self, fingerprint: &str) -> Result; + async fn check_branch_protection_bypass( + &self, + repository_id: RepositoryId, + pattern: &str, + principal: Option, + ) -> Result; + async fn list_branch_protection_rules( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError>; + async fn list_user_signing_keys( + &self, + user_id: UserId, + ) -> Result, ApplicationError>; +} + +#[async_trait] +pub trait IdentityBridgeRepositoryUseCases: Send + Sync { + async fn resolve_repository( + &self, + owner: RepositoryOwner, + name: RepositoryName, + ) -> Result; + async fn repository_coordinates( + &self, + repository_id: RepositoryId, + ) -> Result; + async fn register_repository( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result<(), ApplicationError>; + async fn provision_repository( + &self, + owner: RepositoryOwnerId, + name: RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result; + async fn update_repository_visibility( + &self, + repository_id: RepositoryId, + visibility: RepositoryVisibility, + ) -> Result<(), ApplicationError>; + async fn update_repository_metadata( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: RepositoryName, + visibility: RepositoryVisibility, + actor_id: UserId, + ) -> Result<(), ApplicationError>; + async fn archive_repository(&self, repository_id: RepositoryId) + -> Result<(), ApplicationError>; + async fn set_repository_archived( + &self, + repository_id: RepositoryId, + archived: bool, + ) -> Result<(), ApplicationError>; + async fn delete_repository(&self, repository_id: RepositoryId) -> Result<(), ApplicationError>; +} + +pub trait IdentityBridgeUseCases: + IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases +{ +} + +impl IdentityBridgeUseCases for T where + T: IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases + ?Sized +{ +} + +#[derive(Clone)] +pub struct IdentityBridgeApplication { + repository: Arc, +} + +impl IdentityBridgeApplication { + #[must_use] + pub fn new(repository: Arc) -> Self { + Self { repository } + } +} diff --git a/crates/storage/src/bridge.rs b/crates/storage/src/bridge.rs --- a/crates/storage/src/bridge.rs +++ b/crates/storage/src/bridge.rs @@ -1,247 +1,250 @@ -use async_trait::async_trait; -use syncode_identity_application::{ - AccessTokenIdentity, ActiveBranchProtectionRule, ApplicationError, IdentityBridgeRepository, - LocalAgentIdentity, SigningKeyIdentity, SshKeyIdentity, -}; -use syncode_identity_model::{ - AccessTokenId, Grant, GrantPrincipal, LocalAgentId, PlatformAgentId, RepositoryId, - RepositoryName, RepositoryOwner, RepositoryOwnerId, RepositoryVisibility, Resource, UserId, - UserPrincipal, -}; - -use crate::{Postgres, application_persistence as persistence}; - -mod identities; -mod repository; -mod repository_grants; -mod repository_metadata; -mod repository_registration; -mod token; - -use token::{access_token, ssh_key}; - -#[async_trait] -impl IdentityBridgeRepository for Postgres { - async fn validate_web_session( - &self, - token: &str, - ) -> Result)>, ApplicationError> { - self.validate_session(token) - .await - .map(|session| session.map(|(id, expires_at)| (id.into(), expires_at))) - .map_err(persistence) - } - - async fn validate_access_token( - &self, - token: &str, - ) -> Result, ApplicationError> { - Postgres::validate_access_token(self, token) - .await - .map(|token| token.map(access_token)) - .map_err(persistence) - } - - async fn load_access_token( - &self, - token_id: AccessTokenId, - ) -> Result, ApplicationError> { - Postgres::load_access_token(self, token_id.get()) - .await - .map(|token| token.map(access_token)) - .map_err(persistence) - } - - async fn load_user_principal( - &self, - user_id: UserId, - ) -> Result { - Postgres::load_user_principal(self, user_id.get()) - .await - .map_err(persistence) - } - - async fn load_granted_resources( - &self, - principals: &[(syncode_identity_model::GrantPrincipalKind, uuid::Uuid)], - resource_kind: syncode_identity_model::ResourceKind, - ) -> Result< - Vec<( - uuid::Uuid, - std::collections::BTreeSet, - )>, - ApplicationError, - > { - Postgres::load_granted_resources(self, principals, resource_kind) - .await - .map_err(|error| ApplicationError::Persistence(error.to_string())) - } - - async fn load_active_grants(&self, resource: Resource) -> Result, ApplicationError> { - Postgres::load_active_grants(self, resource) - .await - .map_err(persistence) - } - - async fn load_active_repository_visibility( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError> { - self.bridge_repository_visibility(repository_id.get()).await - } - - async fn load_active_repository_owner( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError> { - self.bridge_repository_owner(repository_id.get()).await - } - - async fn platform_agent_is_active( - &self, - agent_id: PlatformAgentId, - ) -> Result { - Postgres::platform_agent_is_active(self, agent_id.get()) - .await - .map_err(persistence) - } - - async fn local_agent( - &self, - agent_id: LocalAgentId, - ) -> Result, ApplicationError> { - self.bridge_local_agent(agent_id.get()).await - } - - async fn resolve_ssh_key( - &self, - fingerprint: &str, - ) -> Result, ApplicationError> { - Postgres::resolve_ssh_key(self, fingerprint) - .await - .map_err(persistence)? - .map(ssh_key) - .transpose() - } - - async fn branch_protection_bypass_allowed( - &self, - repository_id: RepositoryId, - pattern: &str, - principal: GrantPrincipal, - ) -> Result { - Postgres::branch_protection_bypass_allowed( - self, - repository_id.get(), - pattern, - principal.kind(), - principal.get(), - ) - .await - .map_err(persistence) - } - - async fn active_branch_protection_rules( - &self, - repository_id: RepositoryId, - ) -> Result, ApplicationError> { - self.bridge_active_branch_protection_rules(repository_id.get()) - .await - } - - async fn user_signing_keys( - &self, - user_id: UserId, - ) -> Result, ApplicationError> { - self.bridge_user_signing_keys(user_id.get()).await - } - - async fn resolve_repository( - &self, - owner: &RepositoryOwner, - name: &RepositoryName, - ) -> Result, ApplicationError> { - self.bridge_resolve_repository(owner, name).await - } - - async fn provision_repository( - &self, - owner: RepositoryOwnerId, - name: &RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result { - self.bridge_provision_repository( - owner.kind(), - owner.get(), - name, - visibility, - creator_id.get(), - ) - .await - .map(RepositoryId::from) - } - - async fn register_repository( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: &RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result<(), ApplicationError> { - self.bridge_register_repository( - repository_id.get(), - owner.kind(), - owner.get(), - name, - visibility, - creator_id.get(), - ) - .await - } - - async fn update_repository_visibility( - &self, - repository_id: RepositoryId, - visibility: RepositoryVisibility, - ) -> Result { - self.bridge_update_repository_visibility(repository_id.get(), visibility) - .await - } - - async fn update_repository_metadata( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: &RepositoryName, - visibility: RepositoryVisibility, - actor_id: UserId, - ) -> Result { - self.bridge_update_repository_metadata( - repository_id.get(), - owner.kind(), - owner.get(), - name, - visibility, - actor_id.get(), - ) - .await - } - - async fn archive_repository( - &self, - repository_id: RepositoryId, - ) -> Result { - self.bridge_archive_repository(repository_id.get()).await - } - - async fn set_repository_archived( - &self, - repository_id: RepositoryId, - archived: bool, - ) -> Result { - self.bridge_set_repository_archived(repository_id.get(), archived) - .await - } -} +use async_trait::async_trait; +use syncode_identity_application::{ + AccessTokenIdentity, ActiveBranchProtectionRule, ApplicationError, IdentityBridgeRepository, + LocalAgentIdentity, SigningKeyIdentity, SshKeyIdentity, +}; +use syncode_identity_model::{ + AccessTokenId, Grant, GrantPrincipal, LocalAgentId, PlatformAgentId, RepositoryCoordinates, + RepositoryId, RepositoryName, RepositoryOwner, RepositoryOwnerId, RepositoryVisibility, + Resource, UserId, UserPrincipal, +}; + +use crate::{Postgres, application_persistence as persistence}; + +mod identities; +mod repository; +mod repository_grants; +mod repository_metadata; +mod repository_registration; +mod token; + +use token::{access_token, ssh_key}; + +#[async_trait] +impl IdentityBridgeRepository for Postgres { + async fn validate_web_session( + &self, + token: &str, + ) -> Result)>, ApplicationError> { + self.validate_session(token) + .await + .map(|session| session.map(|(id, expires_at)| (id.into(), expires_at))) + .map_err(persistence) + } + async fn validate_access_token( + &self, + token: &str, + ) -> Result, ApplicationError> { + Postgres::validate_access_token(self, token) + .await + .map(|token| token.map(access_token)) + .map_err(persistence) + } + + async fn load_access_token( + &self, + token_id: AccessTokenId, + ) -> Result, ApplicationError> { + Postgres::load_access_token(self, token_id.get()) + .await + .map(|token| token.map(access_token)) + .map_err(persistence) + } + + async fn load_user_principal( + &self, + user_id: UserId, + ) -> Result { + Postgres::load_user_principal(self, user_id.get()) + .await + .map_err(persistence) + } + + async fn load_granted_resources( + &self, + principals: &[(syncode_identity_model::GrantPrincipalKind, uuid::Uuid)], + resource_kind: syncode_identity_model::ResourceKind, + ) -> Result< + Vec<( + uuid::Uuid, + std::collections::BTreeSet, + )>, + ApplicationError, + > { + Postgres::load_granted_resources(self, principals, resource_kind) + .await + .map_err(|error| ApplicationError::Persistence(error.to_string())) + } + + async fn load_active_grants(&self, resource: Resource) -> Result, ApplicationError> { + Postgres::load_active_grants(self, resource) + .await + .map_err(persistence) + } + + async fn load_active_repository_visibility( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError> { + self.bridge_repository_visibility(repository_id.get()).await + } + + async fn load_active_repository_owner( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError> { + self.bridge_repository_owner(repository_id.get()).await + } + + async fn platform_agent_is_active( + &self, + agent_id: PlatformAgentId, + ) -> Result { + Postgres::platform_agent_is_active(self, agent_id.get()) + .await + .map_err(persistence) + } + + async fn local_agent( + &self, + agent_id: LocalAgentId, + ) -> Result, ApplicationError> { + self.bridge_local_agent(agent_id.get()).await + } + + async fn resolve_ssh_key( + &self, + fingerprint: &str, + ) -> Result, ApplicationError> { + Postgres::resolve_ssh_key(self, fingerprint) + .await + .map_err(persistence)? + .map(ssh_key) + .transpose() + } + + async fn branch_protection_bypass_allowed( + &self, + repository_id: RepositoryId, + pattern: &str, + principal: GrantPrincipal, + ) -> Result { + Postgres::branch_protection_bypass_allowed( + self, + repository_id.get(), + pattern, + principal.kind(), + principal.get(), + ) + .await + .map_err(persistence) + } + + async fn active_branch_protection_rules( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError> { + self.bridge_active_branch_protection_rules(repository_id.get()) + .await + } + async fn user_signing_keys( + &self, + user_id: UserId, + ) -> Result, ApplicationError> { + self.bridge_user_signing_keys(user_id.get()).await + } + async fn resolve_repository( + &self, + owner: &RepositoryOwner, + name: &RepositoryName, + ) -> Result, ApplicationError> { + self.bridge_resolve_repository(owner, name).await + } + async fn repository_coordinates( + &self, + repository_id: RepositoryId, + ) -> Result, ApplicationError> { + self.bridge_repository_coordinates(repository_id.get()) + .await + } + async fn provision_repository( + &self, + owner: RepositoryOwnerId, + name: &RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result { + self.bridge_provision_repository( + owner.kind(), + owner.get(), + name, + visibility, + creator_id.get(), + ) + .await + .map(RepositoryId::from) + } + + async fn register_repository( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: &RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result<(), ApplicationError> { + self.bridge_register_repository( + repository_id.get(), + owner.kind(), + owner.get(), + name, + visibility, + creator_id.get(), + ) + .await + } + + async fn update_repository_visibility( + &self, + repository_id: RepositoryId, + visibility: RepositoryVisibility, + ) -> Result { + self.bridge_update_repository_visibility(repository_id.get(), visibility) + .await + } + + async fn update_repository_metadata( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: &RepositoryName, + visibility: RepositoryVisibility, + actor_id: UserId, + ) -> Result { + self.bridge_update_repository_metadata( + repository_id.get(), + owner.kind(), + owner.get(), + name, + visibility, + actor_id.get(), + ) + .await + } + + async fn archive_repository( + &self, + repository_id: RepositoryId, + ) -> Result { + self.bridge_archive_repository(repository_id.get()).await + } + + async fn set_repository_archived( + &self, + repository_id: RepositoryId, + archived: bool, + ) -> Result { + self.bridge_set_repository_archived(repository_id.get(), archived) + .await + } +} diff --git a/crates/api-grpc/src/service/repository.rs b/crates/api-grpc/src/service/repository.rs --- a/crates/api-grpc/src/service/repository.rs +++ b/crates/api-grpc/src/service/repository.rs @@ -1,179 +1,195 @@ -use syncode_identity_model::{RepositoryName, RepositoryOwner}; -use tonic::{Request, Response, Status}; - -use super::IdentityServer; -use super::conversion::{ - application_error, model_repository_owner, model_repository_visibility, parse_uuid, -}; -use crate::wire::{ - ArchiveRepositoryProjectionRequest, ArchiveRepositoryProjectionResponse, - ArchiveRepositoryRequest, ArchiveRepositoryResponse, DeleteRepositoryRequest, - DeleteRepositoryResponse, ProvisionRepositoryRequest, ProvisionRepositoryResponse, - RegisterRepositoryRequest, RegisterRepositoryResponse, ResolveRepositoryRequest, - ResolveRepositoryResponse, UnarchiveRepositoryRequest, UnarchiveRepositoryResponse, - UpdateRepositoryMetadataRequest, UpdateRepositoryMetadataResponse, - UpdateRepositoryVisibilityRequest, UpdateRepositoryVisibilityResponse, -}; - -pub(super) async fn resolve( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - let request = request.into_inner(); - let owner = request - .owner - .parse::() - .map_err(|error| Status::invalid_argument(error.to_string()))?; - let name = request - .name - .parse::() - .map_err(|error| Status::invalid_argument(error.to_string()))?; - let repository_id = server - .application - .resolve_repository(owner, name) - .await - .map_err(application_error)?; - Ok(Response::new(ResolveRepositoryResponse { - repository_id: repository_id.to_string(), - })) -} - -pub(super) async fn provision( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - let request = request.into_inner(); - let owner = model_repository_owner(request.owner_kind(), &request.owner_id)?; - let visibility = model_repository_visibility(request.visibility())?; - let name = request - .name - .parse::() - .map_err(|error| Status::invalid_argument(error.to_string()))?; - let repository_id = server - .application - .provision_repository( - owner, - name, - visibility, - parse_uuid(&request.creator_id)?.into(), - ) - .await - .map_err(application_error)?; - Ok(Response::new(ProvisionRepositoryResponse { - repository_id: repository_id.to_string(), - })) -} - -pub(super) async fn register( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - let request = request.into_inner(); - server - .application - .register_repository( - parse_uuid(&request.repository_id)?.into(), - model_repository_owner(request.owner_kind(), &request.owner_id)?, - request - .name - .parse::() - .map_err(|error| Status::invalid_argument(error.to_string()))?, - model_repository_visibility(request.visibility())?, - parse_uuid(&request.creator_id)?.into(), - ) - .await - .map_err(application_error)?; - Ok(Response::new(RegisterRepositoryResponse {})) -} - -pub(super) async fn update_visibility( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - let request = request.into_inner(); - server - .application - .update_repository_visibility( - parse_uuid(&request.repository_id)?.into(), - model_repository_visibility(request.visibility())?, - ) - .await - .map_err(application_error)?; - Ok(Response::new(UpdateRepositoryVisibilityResponse {})) -} - -pub(super) async fn update_metadata( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - let request = request.into_inner(); - server - .application - .update_repository_metadata( - parse_uuid(&request.repository_id)?.into(), - model_repository_owner(request.owner_kind(), &request.owner_id)?, - request - .name - .parse::() - .map_err(|error| Status::invalid_argument(error.to_string()))?, - model_repository_visibility(request.visibility())?, - parse_uuid(&request.actor_id)?.into(), - ) - .await - .map_err(application_error)?; - Ok(Response::new(UpdateRepositoryMetadataResponse {})) -} - -pub(super) async fn archive_repository( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - server - .application - .archive_repository(parse_uuid(&request.into_inner().repository_id)?.into()) - .await - .map_err(application_error)?; - Ok(Response::new(ArchiveRepositoryResponse {})) -} - -pub(super) async fn archive( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - server - .application - .set_repository_archived( - parse_uuid(&request.into_inner().repository_id)?.into(), - true, - ) - .await - .map_err(application_error)?; - Ok(Response::new(ArchiveRepositoryProjectionResponse {})) -} - -pub(super) async fn unarchive( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - server - .application - .set_repository_archived( - parse_uuid(&request.into_inner().repository_id)?.into(), - false, - ) - .await - .map_err(application_error)?; - Ok(Response::new(UnarchiveRepositoryResponse {})) -} - -pub(super) async fn delete( - server: &IdentityServer, - request: Request, -) -> Result, Status> { - server - .application - .delete_repository(parse_uuid(&request.into_inner().repository_id)?.into()) - .await - .map_err(application_error)?; - Ok(Response::new(DeleteRepositoryResponse {})) -} +use syncode_identity_model::{RepositoryName, RepositoryOwner}; +use tonic::{Request, Response, Status}; + +use super::IdentityServer; +use super::conversion::{ + application_error, model_repository_owner, model_repository_visibility, parse_uuid, +}; +use crate::wire::{ + ArchiveRepositoryProjectionRequest, ArchiveRepositoryProjectionResponse, + ArchiveRepositoryRequest, ArchiveRepositoryResponse, DeleteRepositoryRequest, + DeleteRepositoryResponse, GetRepositoryCoordinatesRequest, GetRepositoryCoordinatesResponse, + ProvisionRepositoryRequest, ProvisionRepositoryResponse, RegisterRepositoryRequest, + RegisterRepositoryResponse, ResolveRepositoryRequest, ResolveRepositoryResponse, + UnarchiveRepositoryRequest, UnarchiveRepositoryResponse, UpdateRepositoryMetadataRequest, + UpdateRepositoryMetadataResponse, UpdateRepositoryVisibilityRequest, + UpdateRepositoryVisibilityResponse, +}; + +pub(super) async fn resolve( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + let request = request.into_inner(); + let owner = request + .owner + .parse::() + .map_err(|error| Status::invalid_argument(error.to_string()))?; + let name = request + .name + .parse::() + .map_err(|error| Status::invalid_argument(error.to_string()))?; + let repository_id = server + .application + .resolve_repository(owner, name) + .await + .map_err(application_error)?; + Ok(Response::new(ResolveRepositoryResponse { + repository_id: repository_id.to_string(), + })) +} + +pub(super) async fn coordinates( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + let coordinates = server + .application + .repository_coordinates(parse_uuid(&request.into_inner().repository_id)?.into()) + .await + .map_err(application_error)?; + Ok(Response::new(GetRepositoryCoordinatesResponse { + owner: coordinates.owner.as_str().to_owned(), + name: coordinates.name.as_str().to_owned(), + })) +} + +pub(super) async fn provision( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + let request = request.into_inner(); + let owner = model_repository_owner(request.owner_kind(), &request.owner_id)?; + let visibility = model_repository_visibility(request.visibility())?; + let name = request + .name + .parse::() + .map_err(|error| Status::invalid_argument(error.to_string()))?; + let repository_id = server + .application + .provision_repository( + owner, + name, + visibility, + parse_uuid(&request.creator_id)?.into(), + ) + .await + .map_err(application_error)?; + Ok(Response::new(ProvisionRepositoryResponse { + repository_id: repository_id.to_string(), + })) +} + +pub(super) async fn register( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + let request = request.into_inner(); + server + .application + .register_repository( + parse_uuid(&request.repository_id)?.into(), + model_repository_owner(request.owner_kind(), &request.owner_id)?, + request + .name + .parse::() + .map_err(|error| Status::invalid_argument(error.to_string()))?, + model_repository_visibility(request.visibility())?, + parse_uuid(&request.creator_id)?.into(), + ) + .await + .map_err(application_error)?; + Ok(Response::new(RegisterRepositoryResponse {})) +} + +pub(super) async fn update_visibility( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + let request = request.into_inner(); + server + .application + .update_repository_visibility( + parse_uuid(&request.repository_id)?.into(), + model_repository_visibility(request.visibility())?, + ) + .await + .map_err(application_error)?; + Ok(Response::new(UpdateRepositoryVisibilityResponse {})) +} + +pub(super) async fn update_metadata( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + let request = request.into_inner(); + server + .application + .update_repository_metadata( + parse_uuid(&request.repository_id)?.into(), + model_repository_owner(request.owner_kind(), &request.owner_id)?, + request + .name + .parse::() + .map_err(|error| Status::invalid_argument(error.to_string()))?, + model_repository_visibility(request.visibility())?, + parse_uuid(&request.actor_id)?.into(), + ) + .await + .map_err(application_error)?; + Ok(Response::new(UpdateRepositoryMetadataResponse {})) +} + +pub(super) async fn archive_repository( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + server + .application + .archive_repository(parse_uuid(&request.into_inner().repository_id)?.into()) + .await + .map_err(application_error)?; + Ok(Response::new(ArchiveRepositoryResponse {})) +} + +pub(super) async fn archive( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + server + .application + .set_repository_archived( + parse_uuid(&request.into_inner().repository_id)?.into(), + true, + ) + .await + .map_err(application_error)?; + Ok(Response::new(ArchiveRepositoryProjectionResponse {})) +} + +pub(super) async fn unarchive( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + server + .application + .set_repository_archived( + parse_uuid(&request.into_inner().repository_id)?.into(), + false, + ) + .await + .map_err(application_error)?; + Ok(Response::new(UnarchiveRepositoryResponse {})) +} + +pub(super) async fn delete( + server: &IdentityServer, + request: Request, +) -> Result, Status> { + server + .application + .delete_repository(parse_uuid(&request.into_inner().repository_id)?.into()) + .await + .map_err(application_error)?; + Ok(Response::new(DeleteRepositoryResponse {})) +} diff --git a/crates/application/src/bridge/repository.rs b/crates/application/src/bridge/repository.rs --- a/crates/application/src/bridge/repository.rs +++ b/crates/application/src/bridge/repository.rs @@ -1,115 +1,126 @@ -use async_trait::async_trait; -use syncode_identity_model::{ - RepositoryId, RepositoryName, RepositoryOwner, RepositoryOwnerId, RepositoryVisibility, UserId, -}; - -use super::{IdentityBridgeApplication, IdentityBridgeRepositoryUseCases}; -use crate::ApplicationError; - -#[async_trait] -impl IdentityBridgeRepositoryUseCases for IdentityBridgeApplication { - async fn resolve_repository( - &self, - owner: RepositoryOwner, - name: RepositoryName, - ) -> Result { - self.repository - .resolve_repository(&owner, &name) - .await? - .ok_or(ApplicationError::NotFound("repository")) - } - - async fn register_repository( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result<(), ApplicationError> { - self.repository.load_user_principal(creator_id).await?; - self.repository - .register_repository(repository_id, owner, &name, visibility, creator_id) - .await - } - - async fn provision_repository( - &self, - owner: RepositoryOwnerId, - name: RepositoryName, - visibility: RepositoryVisibility, - creator_id: UserId, - ) -> Result { - self.repository.load_user_principal(creator_id).await?; - self.repository - .provision_repository(owner, &name, visibility, creator_id) - .await - } - - async fn update_repository_visibility( - &self, - repository_id: RepositoryId, - visibility: RepositoryVisibility, - ) -> Result<(), ApplicationError> { - if self - .repository - .update_repository_visibility(repository_id, visibility) - .await? - { - Ok(()) - } else { - Err(ApplicationError::NotFound("repository")) - } - } - - async fn update_repository_metadata( - &self, - repository_id: RepositoryId, - owner: RepositoryOwnerId, - name: RepositoryName, - visibility: RepositoryVisibility, - actor_id: UserId, - ) -> Result<(), ApplicationError> { - self.repository.load_user_principal(actor_id).await?; - if self - .repository - .update_repository_metadata(repository_id, owner, &name, visibility, actor_id) - .await? - { - Ok(()) - } else { - Err(ApplicationError::NotFound("repository")) - } - } - - async fn archive_repository( - &self, - repository_id: RepositoryId, - ) -> Result<(), ApplicationError> { - if self.repository.archive_repository(repository_id).await? { - Ok(()) - } else { - Err(ApplicationError::NotFound("repository")) - } - } - - async fn set_repository_archived( - &self, - repository_id: RepositoryId, - archived: bool, - ) -> Result<(), ApplicationError> { - if self - .repository - .set_repository_archived(repository_id, archived) - .await? - { - Ok(()) - } else { - Err(ApplicationError::NotFound("repository")) - } - } - - async fn delete_repository(&self, repository_id: RepositoryId) -> Result<(), ApplicationError> { - self.archive_repository(repository_id).await - } -} +use async_trait::async_trait; +use syncode_identity_model::{ + RepositoryCoordinates, RepositoryId, RepositoryName, RepositoryOwner, RepositoryOwnerId, + RepositoryVisibility, UserId, +}; + +use super::{IdentityBridgeApplication, IdentityBridgeRepositoryUseCases}; +use crate::ApplicationError; + +#[async_trait] +impl IdentityBridgeRepositoryUseCases for IdentityBridgeApplication { + async fn resolve_repository( + &self, + owner: RepositoryOwner, + name: RepositoryName, + ) -> Result { + self.repository + .resolve_repository(&owner, &name) + .await? + .ok_or(ApplicationError::NotFound("repository")) + } + + async fn repository_coordinates( + &self, + repository_id: RepositoryId, + ) -> Result { + self.repository + .repository_coordinates(repository_id) + .await? + .ok_or(ApplicationError::NotFound("repository")) + } + + async fn register_repository( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result<(), ApplicationError> { + self.repository.load_user_principal(creator_id).await?; + self.repository + .register_repository(repository_id, owner, &name, visibility, creator_id) + .await + } + + async fn provision_repository( + &self, + owner: RepositoryOwnerId, + name: RepositoryName, + visibility: RepositoryVisibility, + creator_id: UserId, + ) -> Result { + self.repository.load_user_principal(creator_id).await?; + self.repository + .provision_repository(owner, &name, visibility, creator_id) + .await + } + + async fn update_repository_visibility( + &self, + repository_id: RepositoryId, + visibility: RepositoryVisibility, + ) -> Result<(), ApplicationError> { + if self + .repository + .update_repository_visibility(repository_id, visibility) + .await? + { + Ok(()) + } else { + Err(ApplicationError::NotFound("repository")) + } + } + + async fn update_repository_metadata( + &self, + repository_id: RepositoryId, + owner: RepositoryOwnerId, + name: RepositoryName, + visibility: RepositoryVisibility, + actor_id: UserId, + ) -> Result<(), ApplicationError> { + self.repository.load_user_principal(actor_id).await?; + if self + .repository + .update_repository_metadata(repository_id, owner, &name, visibility, actor_id) + .await? + { + Ok(()) + } else { + Err(ApplicationError::NotFound("repository")) + } + } + + async fn archive_repository( + &self, + repository_id: RepositoryId, + ) -> Result<(), ApplicationError> { + if self.repository.archive_repository(repository_id).await? { + Ok(()) + } else { + Err(ApplicationError::NotFound("repository")) + } + } + + async fn set_repository_archived( + &self, + repository_id: RepositoryId, + archived: bool, + ) -> Result<(), ApplicationError> { + if self + .repository + .set_repository_archived(repository_id, archived) + .await? + { + Ok(()) + } else { + Err(ApplicationError::NotFound("repository")) + } + } + + async fn delete_repository(&self, repository_id: RepositoryId) -> Result<(), ApplicationError> { + self.archive_repository(repository_id).await + } +} diff --git a/crates/storage/src/bridge/repository.rs b/crates/storage/src/bridge/repository.rs --- a/crates/storage/src/bridge/repository.rs +++ b/crates/storage/src/bridge/repository.rs @@ -1,177 +1,202 @@ -use syncode_identity_application::ApplicationError; -use syncode_identity_model::{ - RepositoryId, RepositoryName, RepositoryOwner, RepositoryOwnerId, RepositoryOwnerKind, - RepositoryVisibility, -}; -use uuid::Uuid; - -use super::repository_grants::{ensure_repository_admin_grants, owner_admin_principals}; -use crate::{Postgres, application_persistence as persistence}; - -impl Postgres { - pub(super) async fn bridge_resolve_repository( - &self, - owner: &RepositoryOwner, - name: &RepositoryName, - ) -> Result, ApplicationError> { - Postgres::resolve_repository(self, owner.as_str(), name.as_str()) - .await - .map(|repository| repository.map(RepositoryId::from)) - .map_err(persistence) - } - - pub(super) async fn bridge_repository_visibility( - &self, - repository_id: Uuid, - ) -> Result, ApplicationError> { - sqlx::query_scalar!( - r#"SELECT visibility::text AS "visibility!" FROM repository - WHERE id = $1 AND status = 'active' AND deleted_at IS NULL"#, - repository_id - ) - .fetch_optional(&self.pool) - .await - .map_err(persistence)? - .map(|visibility| match visibility.as_str() { - "public" => Ok(RepositoryVisibility::Public), - "private" => Ok(RepositoryVisibility::Private), - "limited" => Ok(RepositoryVisibility::Limited), - other => Err(ApplicationError::Persistence(format!( - "unknown repository visibility {other}" - ))), - }) - .transpose() - } - - pub(super) async fn bridge_repository_owner( - &self, - repository_id: Uuid, - ) -> Result, ApplicationError> { - sqlx::query!( - r#"SELECT owner_kind::text AS "owner_kind!", owner_id FROM repository - WHERE id = $1 AND status = 'active' AND deleted_at IS NULL"#, - repository_id - ) - .fetch_optional(&self.pool) - .await - .map_err(persistence)? - .map(|owner| { - let owner_id = match owner.owner_kind.as_str() { - "user" => RepositoryOwnerId::User(owner.owner_id.into()), - "organization" => RepositoryOwnerId::Organization(owner.owner_id.into()), - other => { - return Err(ApplicationError::Persistence(format!( - "unknown repository owner kind {other}" - ))); - } - }; - Ok(owner_id) - }) - .transpose() - } - - pub(super) async fn bridge_provision_repository( - &self, - owner_kind: RepositoryOwnerKind, - owner_id: Uuid, - name: &RepositoryName, - visibility: RepositoryVisibility, - creator_id: Uuid, - ) -> Result { - let mut transaction = self.pool.begin().await.map_err(persistence)?; - let repository_id = Uuid::new_v4(); - let repository_id = sqlx::query_scalar!( - "INSERT INTO repository (id, owner_kind, owner_id, name, visibility) - VALUES ($1, $2::text::owner_kind, $3, $4, $5::text::repository_visibility) - ON CONFLICT (owner_kind, owner_id, name) WHERE deleted_at IS NULL DO UPDATE - SET visibility = EXCLUDED.visibility - RETURNING id", - repository_id, - owner_kind.as_str(), - owner_id, - name.as_str(), - visibility.as_str() - ) - .fetch_one(&mut *transaction) - .await - .map_err(persistence)?; - let principals = owner_admin_principals(&mut transaction, owner_kind, owner_id).await?; - ensure_repository_admin_grants(&mut transaction, repository_id, &principals, creator_id) - .await?; - transaction.commit().await.map_err(persistence)?; - Ok(repository_id) - } - - pub(super) async fn bridge_update_repository_visibility( - &self, - repository_id: Uuid, - visibility: RepositoryVisibility, - ) -> Result { - sqlx::query_scalar!( - "UPDATE repository SET visibility = $2::text::repository_visibility - WHERE id = $1 AND status = 'active' AND deleted_at IS NULL - RETURNING id", - repository_id, - visibility.as_str() - ) - .fetch_optional(&self.pool) - .await - .map(|updated| updated.is_some()) - .map_err(persistence) - } - - pub(super) async fn bridge_archive_repository( - &self, - repository_id: Uuid, - ) -> Result { - let mut transaction = self.pool.begin().await.map_err(persistence)?; - let exists = sqlx::query_scalar!( - "SELECT id FROM repository WHERE id = $1 FOR UPDATE", - repository_id - ) - .fetch_optional(&mut *transaction) - .await - .map_err(persistence)? - .is_some(); - if !exists { - return Ok(false); - } - sqlx::query!( - "UPDATE repository SET status = 'deleted', deleted_at = COALESCE(deleted_at, now()) - WHERE id = $1", - repository_id - ) - .execute(&mut *transaction) - .await - .map_err(persistence)?; - sqlx::query!( - r#"UPDATE "grant" SET revoked_at = now() - WHERE resource_kind = 'repository' AND resource_id = $1 AND revoked_at IS NULL"#, - repository_id - ) - .execute(&mut *transaction) - .await - .map_err(persistence)?; - transaction.commit().await.map_err(persistence)?; - Ok(true) - } - - pub(super) async fn bridge_set_repository_archived( - &self, - repository_id: Uuid, - archived: bool, - ) -> Result { - let status = if archived { "archived" } else { "active" }; - sqlx::query_scalar!( - "UPDATE repository SET status = $2::text::repository_status - WHERE id = $1 AND status IN ('active', 'archived') AND deleted_at IS NULL - RETURNING id", - repository_id, - status - ) - .fetch_optional(&self.pool) - .await - .map(|updated| updated.is_some()) - .map_err(persistence) - } -} +use syncode_identity_application::ApplicationError; +use syncode_identity_model::{ + RepositoryCoordinates, RepositoryId, RepositoryName, RepositoryOwner, RepositoryOwnerId, + RepositoryOwnerKind, RepositoryVisibility, +}; +use uuid::Uuid; + +use super::repository_grants::{ensure_repository_admin_grants, owner_admin_principals}; +use crate::{Postgres, application_persistence as persistence}; + +impl Postgres { + pub(super) async fn bridge_resolve_repository( + &self, + owner: &RepositoryOwner, + name: &RepositoryName, + ) -> Result, ApplicationError> { + Postgres::resolve_repository(self, owner.as_str(), name.as_str()) + .await + .map(|repository| repository.map(RepositoryId::from)) + .map_err(persistence) + } + + pub(super) async fn bridge_repository_coordinates( + &self, + repository_id: Uuid, + ) -> Result, ApplicationError> { + let Some(repository) = self + .find_repository_by_id(repository_id) + .await + .map_err(persistence)? + else { + return Ok(None); + }; + Ok(Some(RepositoryCoordinates { + owner: repository.owner_name.parse().map_err( + |error: syncode_identity_model::InvalidForgeValue| { + ApplicationError::Persistence(error.to_string()) + }, + )?, + name: repository.name.parse().map_err( + |error: syncode_identity_model::InvalidForgeValue| { + ApplicationError::Persistence(error.to_string()) + }, + )?, + })) + } + + pub(super) async fn bridge_repository_visibility( + &self, + repository_id: Uuid, + ) -> Result, ApplicationError> { + sqlx::query_scalar!( + r#"SELECT visibility::text AS "visibility!" FROM repository + WHERE id = $1 AND status = 'active' AND deleted_at IS NULL"#, + repository_id + ) + .fetch_optional(&self.pool) + .await + .map_err(persistence)? + .map(|visibility| match visibility.as_str() { + "public" => Ok(RepositoryVisibility::Public), + "private" => Ok(RepositoryVisibility::Private), + "limited" => Ok(RepositoryVisibility::Limited), + other => Err(ApplicationError::Persistence(format!( + "unknown repository visibility {other}" + ))), + }) + .transpose() + } + + pub(super) async fn bridge_repository_owner( + &self, + repository_id: Uuid, + ) -> Result, ApplicationError> { + sqlx::query!( + r#"SELECT owner_kind::text AS "owner_kind!", owner_id FROM repository + WHERE id = $1 AND status = 'active' AND deleted_at IS NULL"#, + repository_id + ) + .fetch_optional(&self.pool) + .await + .map_err(persistence)? + .map(|owner| { + let owner_id = match owner.owner_kind.as_str() { + "user" => RepositoryOwnerId::User(owner.owner_id.into()), + "organization" => RepositoryOwnerId::Organization(owner.owner_id.into()), + other => { + return Err(ApplicationError::Persistence(format!( + "unknown repository owner kind {other}" + ))); + } + }; + Ok(owner_id) + }) + .transpose() + } + + pub(super) async fn bridge_provision_repository( + &self, + owner_kind: RepositoryOwnerKind, + owner_id: Uuid, + name: &RepositoryName, + visibility: RepositoryVisibility, + creator_id: Uuid, + ) -> Result { + let mut transaction = self.pool.begin().await.map_err(persistence)?; + let repository_id = Uuid::new_v4(); + let repository_id = sqlx::query_scalar!( + "INSERT INTO repository (id, owner_kind, owner_id, name, visibility) + VALUES ($1, $2::text::owner_kind, $3, $4, $5::text::repository_visibility) + ON CONFLICT (owner_kind, owner_id, name) WHERE deleted_at IS NULL DO UPDATE + SET visibility = EXCLUDED.visibility + RETURNING id", + repository_id, + owner_kind.as_str(), + owner_id, + name.as_str(), + visibility.as_str() + ) + .fetch_one(&mut *transaction) + .await + .map_err(persistence)?; + let principals = owner_admin_principals(&mut transaction, owner_kind, owner_id).await?; + ensure_repository_admin_grants(&mut transaction, repository_id, &principals, creator_id) + .await?; + transaction.commit().await.map_err(persistence)?; + Ok(repository_id) + } + + pub(super) async fn bridge_update_repository_visibility( + &self, + repository_id: Uuid, + visibility: RepositoryVisibility, + ) -> Result { + sqlx::query_scalar!( + "UPDATE repository SET visibility = $2::text::repository_visibility + WHERE id = $1 AND status = 'active' AND deleted_at IS NULL + RETURNING id", + repository_id, + visibility.as_str() + ) + .fetch_optional(&self.pool) + .await + .map(|updated| updated.is_some()) + .map_err(persistence) + } + + pub(super) async fn bridge_archive_repository( + &self, + repository_id: Uuid, + ) -> Result { + let mut transaction = self.pool.begin().await.map_err(persistence)?; + let exists = sqlx::query_scalar!( + "SELECT id FROM repository WHERE id = $1 FOR UPDATE", + repository_id + ) + .fetch_optional(&mut *transaction) + .await + .map_err(persistence)? + .is_some(); + if !exists { + return Ok(false); + } + sqlx::query!( + "UPDATE repository SET status = 'deleted', deleted_at = COALESCE(deleted_at, now()) + WHERE id = $1", + repository_id + ) + .execute(&mut *transaction) + .await + .map_err(persistence)?; + sqlx::query!( + r#"UPDATE "grant" SET revoked_at = now() + WHERE resource_kind = 'repository' AND resource_id = $1 AND revoked_at IS NULL"#, + repository_id + ) + .execute(&mut *transaction) + .await + .map_err(persistence)?; + transaction.commit().await.map_err(persistence)?; + Ok(true) + } + + pub(super) async fn bridge_set_repository_archived( + &self, + repository_id: Uuid, + archived: bool, + ) -> Result { + let status = if archived { "archived" } else { "active" }; + sqlx::query_scalar!( + "UPDATE repository SET status = $2::text::repository_status + WHERE id = $1 AND status IN ('active', 'archived') AND deleted_at IS NULL + RETURNING id", + repository_id, + status + ) + .fetch_optional(&self.pool) + .await + .map(|updated| updated.is_some()) + .map_err(persistence) + } +}