feat: resolve repository coordinates #33

Manually merged
day01 merged 1 commits from feat/0.6-repository-coordinates into develop 2026-08-30 13:58:27 +00:00
8 changed files with 111 additions and 25 deletions
Showing only changes of commit 96af7ce785 - Show all commits
+12 -4
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -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<dyn IdentityBridgeUseCases>,
}

#[tonic::async_trait]
impl Identity for IdentityServer {
async fn resolve_repository(
&self,
request: Request<ResolveRepositoryRequest>,
) -> Result<Response<ResolveRepositoryResponse>, Status> {
repository::resolve(self, request).await
}

async fn register_repository(
&self,
request: Request<RegisterRepositoryRequest>,
) -> Result<Response<RegisterRepositoryResponse>, Status> {
repository::register(self, request).await
}

async fn validate_session(
&self,
request: Request<ValidateSessionRequest>,
) -> Result<Response<ValidateSessionResponse>, 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<CheckCapabilityRequest>,
) -> Result<Response<CheckCapabilityResponse>, 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<CheckCapabilitiesRequest>,
) -> Result<Response<CheckCapabilitiesResponse>, 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<ListPermittedResourcesRequest>,
) -> Result<Response<ListPermittedResourcesResponse>, 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<ResolveSshKeyRequest>,
) -> Result<Response<ResolveSshKeyResponse>, 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<CheckBranchProtectionBypassRequest>,
) -> Result<Response<CheckBranchProtectionBypassResponse>, Status> {
access::check_branch_protection_bypass(self, request).await
}

async fn list_branch_protection_rules(
&self,
request: Request<ListBranchProtectionRulesRequest>,
) -> Result<Response<ListBranchProtectionRulesResponse>, Status> {
access::list_branch_protection_rules(self, request).await
}

async fn list_user_signing_keys(
&self,
request: Request<ListUserSigningKeysRequest>,
) -> Result<Response<ListUserSigningKeysResponse>, Status> {
access::list_user_signing_keys(self, request).await
}

async fn provision_repository(
&self,
request: Request<ProvisionRepositoryRequest>,
) -> Result<Response<ProvisionRepositoryResponse>, Status> {
repository::provision(self, request).await
}

async fn update_repository_visibility(
&self,
request: Request<UpdateRepositoryVisibilityRequest>,
) -> Result<Response<UpdateRepositoryVisibilityResponse>, Status> {
repository::update_visibility(self, request).await
}

async fn update_repository_metadata(
&self,
request: Request<UpdateRepositoryMetadataRequest>,
) -> Result<Response<UpdateRepositoryMetadataResponse>, Status> {
repository::update_metadata(self, request).await
}

async fn archive_repository(
&self,
request: Request<ArchiveRepositoryRequest>,
) -> Result<Response<ArchiveRepositoryResponse>, Status> {
repository::archive_repository(self, request).await
}

async fn archive_repository_projection(
&self,
request: Request<ArchiveRepositoryProjectionRequest>,
) -> Result<Response<ArchiveRepositoryProjectionResponse>, Status> {
repository::archive(self, request).await
}

async fn unarchive_repository(
&self,
request: Request<UnarchiveRepositoryRequest>,
) -> Result<Response<UnarchiveRepositoryResponse>, Status> {
repository::unarchive(self, request).await
}

async fn delete_repository(
&self,
request: Request<DeleteRepositoryRequest>,
) -> Result<Response<DeleteRepositoryResponse>, 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<dyn IdentityBridgeUseCases>,
}

#[tonic::async_trait]
impl Identity for IdentityServer {
async fn resolve_repository(
&self,
request: Request<ResolveRepositoryRequest>,
) -> Result<Response<ResolveRepositoryResponse>, Status> {
repository::resolve(self, request).await
}
async fn get_repository_coordinates(
&self,
request: Request<GetRepositoryCoordinatesRequest>,
) -> Result<Response<GetRepositoryCoordinatesResponse>, Status> {
repository::coordinates(self, request).await
}
async fn register_repository(
&self,
request: Request<RegisterRepositoryRequest>,
) -> Result<Response<RegisterRepositoryResponse>, Status> {
repository::register(self, request).await
}
async fn validate_session(
&self,
request: Request<ValidateSessionRequest>,
) -> Result<Response<ValidateSessionResponse>, 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<CheckCapabilityRequest>,
) -> Result<Response<CheckCapabilityResponse>, 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<CheckCapabilitiesRequest>,
) -> Result<Response<CheckCapabilitiesResponse>, 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<ListPermittedResourcesRequest>,
) -> Result<Response<ListPermittedResourcesResponse>, 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<ResolveSshKeyRequest>,
) -> Result<Response<ResolveSshKeyResponse>, 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<CheckBranchProtectionBypassRequest>,
) -> Result<Response<CheckBranchProtectionBypassResponse>, Status> {
access::check_branch_protection_bypass(self, request).await
}

async fn list_branch_protection_rules(
&self,
request: Request<ListBranchProtectionRulesRequest>,
) -> Result<Response<ListBranchProtectionRulesResponse>, Status> {
access::list_branch_protection_rules(self, request).await
}

async fn list_user_signing_keys(
&self,
request: Request<ListUserSigningKeysRequest>,
) -> Result<Response<ListUserSigningKeysResponse>, Status> {
access::list_user_signing_keys(self, request).await
}

async fn provision_repository(
&self,
request: Request<ProvisionRepositoryRequest>,
) -> Result<Response<ProvisionRepositoryResponse>, Status> {
repository::provision(self, request).await
}

async fn update_repository_visibility(
&self,
request: Request<UpdateRepositoryVisibilityRequest>,
) -> Result<Response<UpdateRepositoryVisibilityResponse>, Status> {
repository::update_visibility(self, request).await
}

async fn update_repository_metadata(
&self,
request: Request<UpdateRepositoryMetadataRequest>,
) -> Result<Response<UpdateRepositoryMetadataResponse>, Status> {
repository::update_metadata(self, request).await
}

async fn archive_repository(
&self,
request: Request<ArchiveRepositoryRequest>,
) -> Result<Response<ArchiveRepositoryResponse>, Status> {
repository::archive_repository(self, request).await
}

async fn archive_repository_projection(
&self,
request: Request<ArchiveRepositoryProjectionRequest>,
) -> Result<Response<ArchiveRepositoryProjectionResponse>, Status> {
repository::archive(self, request).await
}

async fn unarchive_repository(
&self,
request: Request<UnarchiveRepositoryRequest>,
) -> Result<Response<UnarchiveRepositoryResponse>, Status> {
repository::unarchive(self, request).await
}

async fn delete_repository(
&self,
request: Request<DeleteRepositoryRequest>,
) -> Result<Response<DeleteRepositoryResponse>, Status> {
repository::delete(self, request).await
}
}
+11 -3
View File
@@ -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<Option<(UserId, DateTime<Utc>)>, ApplicationError>;
async fn validate_access_token(
&self,
token: &str,
) -> Result<Option<AccessTokenIdentity>, ApplicationError>;
async fn load_access_token(
&self,
token_id: AccessTokenId,
) -> Result<Option<AccessTokenIdentity>, ApplicationError>;
async fn load_user_principal(&self, user_id: UserId)
-> Result<UserPrincipal, ApplicationError>;
async fn load_active_grants(&self, resource: Resource) -> Result<Vec<Grant>, 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<syncode_identity_model::Capability>,
)>,
ApplicationError,
>;
async fn load_active_repository_visibility(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryVisibility>, ApplicationError>;
async fn load_active_repository_owner(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryOwnerId>, ApplicationError>;
async fn platform_agent_is_active(
&self,
agent_id: PlatformAgentId,
) -> Result<bool, ApplicationError>;
async fn local_agent(
&self,
agent_id: LocalAgentId,
) -> Result<Option<LocalAgentIdentity>, ApplicationError>;
async fn resolve_ssh_key(
&self,
fingerprint: &str,
) -> Result<Option<SshKeyIdentity>, ApplicationError>;
async fn branch_protection_bypass_allowed(
&self,
repository_id: RepositoryId,
pattern: &str,
principal: GrantPrincipal,
) -> Result<bool, ApplicationError>;
async fn active_branch_protection_rules(
&self,
repository_id: RepositoryId,
) -> Result<Vec<ActiveBranchProtectionRule>, ApplicationError>;
async fn user_signing_keys(
&self,
user_id: UserId,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError>;
async fn resolve_repository(
&self,
owner: &RepositoryOwner,
name: &RepositoryName,
) -> Result<Option<RepositoryId>, ApplicationError>;
async fn provision_repository(
&self,
owner: RepositoryOwnerId,
name: &RepositoryName,
visibility: RepositoryVisibility,
creator_id: UserId,
) -> Result<RepositoryId, ApplicationError>;
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<bool, ApplicationError>;
async fn update_repository_metadata(
&self,
repository_id: RepositoryId,
owner: RepositoryOwnerId,
name: &RepositoryName,
visibility: RepositoryVisibility,
actor_id: UserId,
) -> Result<bool, ApplicationError>;
async fn archive_repository(
&self,
repository_id: RepositoryId,
) -> Result<bool, ApplicationError>;
async fn set_repository_archived(
&self,
repository_id: RepositoryId,
archived: bool,
) -> Result<bool, ApplicationError>;
}

#[async_trait]
pub trait IdentityBridgeAccessUseCases: Send + Sync {
async fn validate_credential(
&self,
token: &str,
) -> Result<ValidatedCredential, ApplicationError>;
async fn check_capability(
&self,
principal: BridgePrincipalId,
resource: Resource,
capability: Capability,
) -> Result<bool, ApplicationError>;
/// 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<Vec<bool>, ApplicationError>;
async fn list_permitted_resources(
&self,
principal: BridgePrincipalId,
resource_kind: syncode_identity_model::ResourceKind,
capability: Capability,
) -> Result<PermittedResources, ApplicationError>;
async fn resolve_ssh_key(&self, fingerprint: &str) -> Result<SshKeyIdentity, ApplicationError>;
async fn check_branch_protection_bypass(
&self,
repository_id: RepositoryId,
pattern: &str,
principal: Option<BridgePrincipalId>,
) -> Result<BranchProtectionDecision, ApplicationError>;
async fn list_branch_protection_rules(
&self,
repository_id: RepositoryId,
) -> Result<Vec<ActiveBranchProtectionRule>, ApplicationError>;
async fn list_user_signing_keys(
&self,
user_id: UserId,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError>;
}

#[async_trait]
pub trait IdentityBridgeRepositoryUseCases: Send + Sync {
async fn resolve_repository(
&self,
owner: RepositoryOwner,
name: RepositoryName,
) -> Result<RepositoryId, ApplicationError>;
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<RepositoryId, ApplicationError>;
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<T> IdentityBridgeUseCases for T where
T: IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases + ?Sized
{
}

#[derive(Clone)]
pub struct IdentityBridgeApplication {
repository: Arc<dyn IdentityBridgeRepository>,
}

impl IdentityBridgeApplication {
#[must_use]
pub fn new(repository: Arc<dyn IdentityBridgeRepository>) -> 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<Option<(UserId, DateTime<Utc>)>, ApplicationError>;
async fn validate_access_token(
&self,
token: &str,
) -> Result<Option<AccessTokenIdentity>, ApplicationError>;
async fn load_access_token(
&self,
token_id: AccessTokenId,
) -> Result<Option<AccessTokenIdentity>, ApplicationError>;
async fn load_user_principal(&self, user_id: UserId)
-> Result<UserPrincipal, ApplicationError>;
async fn load_active_grants(&self, resource: Resource) -> Result<Vec<Grant>, 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<syncode_identity_model::Capability>,
)>,
ApplicationError,
>;
async fn load_active_repository_visibility(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryVisibility>, ApplicationError>;
async fn load_active_repository_owner(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryOwnerId>, ApplicationError>;
async fn platform_agent_is_active(
&self,
agent_id: PlatformAgentId,
) -> Result<bool, ApplicationError>;
async fn local_agent(
&self,
agent_id: LocalAgentId,
) -> Result<Option<LocalAgentIdentity>, ApplicationError>;
async fn resolve_ssh_key(
&self,
fingerprint: &str,
) -> Result<Option<SshKeyIdentity>, ApplicationError>;
async fn branch_protection_bypass_allowed(
&self,
repository_id: RepositoryId,
pattern: &str,
principal: GrantPrincipal,
) -> Result<bool, ApplicationError>;
async fn active_branch_protection_rules(
&self,
repository_id: RepositoryId,
) -> Result<Vec<ActiveBranchProtectionRule>, ApplicationError>;
async fn user_signing_keys(
&self,
user_id: UserId,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError>;
async fn resolve_repository(
&self,
owner: &RepositoryOwner,
name: &RepositoryName,
) -> Result<Option<RepositoryId>, ApplicationError>;
async fn repository_coordinates(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryCoordinates>, ApplicationError>;
async fn provision_repository(
&self,
owner: RepositoryOwnerId,
name: &RepositoryName,
visibility: RepositoryVisibility,
creator_id: UserId,
) -> Result<RepositoryId, ApplicationError>;
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<bool, ApplicationError>;
async fn update_repository_metadata(
&self,
repository_id: RepositoryId,
owner: RepositoryOwnerId,
name: &RepositoryName,
visibility: RepositoryVisibility,
actor_id: UserId,
) -> Result<bool, ApplicationError>;
async fn archive_repository(
&self,
repository_id: RepositoryId,
) -> Result<bool, ApplicationError>;
async fn set_repository_archived(
&self,
repository_id: RepositoryId,
archived: bool,
) -> Result<bool, ApplicationError>;
}

#[async_trait]
pub trait IdentityBridgeAccessUseCases: Send + Sync {
async fn validate_credential(
&self,
token: &str,
) -> Result<ValidatedCredential, ApplicationError>;
async fn check_capability(
&self,
principal: BridgePrincipalId,
resource: Resource,
capability: Capability,
) -> Result<bool, ApplicationError>;
/// 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<Vec<bool>, ApplicationError>;
async fn list_permitted_resources(
&self,
principal: BridgePrincipalId,
resource_kind: syncode_identity_model::ResourceKind,
capability: Capability,
) -> Result<PermittedResources, ApplicationError>;
async fn resolve_ssh_key(&self, fingerprint: &str) -> Result<SshKeyIdentity, ApplicationError>;
async fn check_branch_protection_bypass(
&self,
repository_id: RepositoryId,
pattern: &str,
principal: Option<BridgePrincipalId>,
) -> Result<BranchProtectionDecision, ApplicationError>;
async fn list_branch_protection_rules(
&self,
repository_id: RepositoryId,
) -> Result<Vec<ActiveBranchProtectionRule>, ApplicationError>;
async fn list_user_signing_keys(
&self,
user_id: UserId,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError>;
}

#[async_trait]
pub trait IdentityBridgeRepositoryUseCases: Send + Sync {
async fn resolve_repository(
&self,
owner: RepositoryOwner,
name: RepositoryName,
) -> Result<RepositoryId, ApplicationError>;
async fn repository_coordinates(
&self,
repository_id: RepositoryId,
) -> Result<RepositoryCoordinates, ApplicationError>;
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<RepositoryId, ApplicationError>;
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<T> IdentityBridgeUseCases for T where
T: IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases + ?Sized
{
}

#[derive(Clone)]
pub struct IdentityBridgeApplication {
repository: Arc<dyn IdentityBridgeRepository>,
}

impl IdentityBridgeApplication {
#[must_use]
pub fn new(repository: Arc<dyn IdentityBridgeRepository>) -> Self {
Self { repository }
}
}
+10 -7
View File
@@ -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<Option<(UserId, chrono::DateTime<chrono::Utc>)>, 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<Option<AccessTokenIdentity>, 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<Option<AccessTokenIdentity>, 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<UserPrincipal, ApplicationError> {
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<syncode_identity_model::Capability>,
)>,
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<Vec<Grant>, ApplicationError> {
Postgres::load_active_grants(self, resource)
.await
.map_err(persistence)
}

async fn load_active_repository_visibility(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryVisibility>, ApplicationError> {
self.bridge_repository_visibility(repository_id.get()).await
}

async fn load_active_repository_owner(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryOwnerId>, ApplicationError> {
self.bridge_repository_owner(repository_id.get()).await
}

async fn platform_agent_is_active(
&self,
agent_id: PlatformAgentId,
) -> Result<bool, ApplicationError> {
Postgres::platform_agent_is_active(self, agent_id.get())
.await
.map_err(persistence)
}

async fn local_agent(
&self,
agent_id: LocalAgentId,
) -> Result<Option<LocalAgentIdentity>, ApplicationError> {
self.bridge_local_agent(agent_id.get()).await
}

async fn resolve_ssh_key(
&self,
fingerprint: &str,
) -> Result<Option<SshKeyIdentity>, 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<bool, ApplicationError> {
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<Vec<ActiveBranchProtectionRule>, ApplicationError> {
self.bridge_active_branch_protection_rules(repository_id.get())
.await
}

async fn user_signing_keys(
&self,
user_id: UserId,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError> {
self.bridge_user_signing_keys(user_id.get()).await
}

async fn resolve_repository(
&self,
owner: &RepositoryOwner,
name: &RepositoryName,
) -> Result<Option<RepositoryId>, ApplicationError> {
self.bridge_resolve_repository(owner, name).await
}

async fn provision_repository(
&self,
owner: RepositoryOwnerId,
name: &RepositoryName,
visibility: RepositoryVisibility,
creator_id: UserId,
) -> Result<RepositoryId, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
self.bridge_archive_repository(repository_id.get()).await
}

async fn set_repository_archived(
&self,
repository_id: RepositoryId,
archived: bool,
) -> Result<bool, ApplicationError> {
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<Option<(UserId, chrono::DateTime<chrono::Utc>)>, 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<Option<AccessTokenIdentity>, 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<Option<AccessTokenIdentity>, 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<UserPrincipal, ApplicationError> {
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<syncode_identity_model::Capability>,
)>,
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<Vec<Grant>, ApplicationError> {
Postgres::load_active_grants(self, resource)
.await
.map_err(persistence)
}

async fn load_active_repository_visibility(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryVisibility>, ApplicationError> {
self.bridge_repository_visibility(repository_id.get()).await
}

async fn load_active_repository_owner(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryOwnerId>, ApplicationError> {
self.bridge_repository_owner(repository_id.get()).await
}

async fn platform_agent_is_active(
&self,
agent_id: PlatformAgentId,
) -> Result<bool, ApplicationError> {
Postgres::platform_agent_is_active(self, agent_id.get())
.await
.map_err(persistence)
}

async fn local_agent(
&self,
agent_id: LocalAgentId,
) -> Result<Option<LocalAgentIdentity>, ApplicationError> {
self.bridge_local_agent(agent_id.get()).await
}

async fn resolve_ssh_key(
&self,
fingerprint: &str,
) -> Result<Option<SshKeyIdentity>, 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<bool, ApplicationError> {
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<Vec<ActiveBranchProtectionRule>, ApplicationError> {
self.bridge_active_branch_protection_rules(repository_id.get())
.await
}
async fn user_signing_keys(
&self,
user_id: UserId,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError> {
self.bridge_user_signing_keys(user_id.get()).await
}
async fn resolve_repository(
&self,
owner: &RepositoryOwner,
name: &RepositoryName,
) -> Result<Option<RepositoryId>, ApplicationError> {
self.bridge_resolve_repository(owner, name).await
}
async fn repository_coordinates(
&self,
repository_id: RepositoryId,
) -> Result<Option<RepositoryCoordinates>, ApplicationError> {
self.bridge_repository_coordinates(repository_id.get())
.await
}
async fn provision_repository(
&self,
owner: RepositoryOwnerId,
name: &RepositoryName,
visibility: RepositoryVisibility,
creator_id: UserId,
) -> Result<RepositoryId, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
self.bridge_archive_repository(repository_id.get()).await
}

async fn set_repository_archived(
&self,
repository_id: RepositoryId,
archived: bool,
) -> Result<bool, ApplicationError> {
self.bridge_set_repository_archived(repository_id.get(), archived)
.await
}
}
+21 -5
View File
@@ -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<ResolveRepositoryRequest>,
) -> Result<Response<ResolveRepositoryResponse>, Status> {
let request = request.into_inner();
let owner = request
.owner
.parse::<RepositoryOwner>()
.map_err(|error| Status::invalid_argument(error.to_string()))?;
let name = request
.name
.parse::<RepositoryName>()
.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<ProvisionRepositoryRequest>,
) -> Result<Response<ProvisionRepositoryResponse>, 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::<RepositoryName>()
.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<RegisterRepositoryRequest>,
) -> Result<Response<RegisterRepositoryResponse>, 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::<RepositoryName>()
.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<UpdateRepositoryVisibilityRequest>,
) -> Result<Response<UpdateRepositoryVisibilityResponse>, 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<UpdateRepositoryMetadataRequest>,
) -> Result<Response<UpdateRepositoryMetadataResponse>, 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::<RepositoryName>()
.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<ArchiveRepositoryRequest>,
) -> Result<Response<ArchiveRepositoryResponse>, 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<ArchiveRepositoryProjectionRequest>,
) -> Result<Response<ArchiveRepositoryProjectionResponse>, 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<UnarchiveRepositoryRequest>,
) -> Result<Response<UnarchiveRepositoryResponse>, 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<DeleteRepositoryRequest>,
) -> Result<Response<DeleteRepositoryResponse>, 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<ResolveRepositoryRequest>,
) -> Result<Response<ResolveRepositoryResponse>, Status> {
let request = request.into_inner();
let owner = request
.owner
.parse::<RepositoryOwner>()
.map_err(|error| Status::invalid_argument(error.to_string()))?;
let name = request
.name
.parse::<RepositoryName>()
.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<GetRepositoryCoordinatesRequest>,
) -> Result<Response<GetRepositoryCoordinatesResponse>, 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<ProvisionRepositoryRequest>,
) -> Result<Response<ProvisionRepositoryResponse>, 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::<RepositoryName>()
.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<RegisterRepositoryRequest>,
) -> Result<Response<RegisterRepositoryResponse>, 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::<RepositoryName>()
.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<UpdateRepositoryVisibilityRequest>,
) -> Result<Response<UpdateRepositoryVisibilityResponse>, 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<UpdateRepositoryMetadataRequest>,
) -> Result<Response<UpdateRepositoryMetadataResponse>, 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::<RepositoryName>()
.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<ArchiveRepositoryRequest>,
) -> Result<Response<ArchiveRepositoryResponse>, 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<ArchiveRepositoryProjectionRequest>,
) -> Result<Response<ArchiveRepositoryProjectionResponse>, 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<UnarchiveRepositoryRequest>,
) -> Result<Response<UnarchiveRepositoryResponse>, 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<DeleteRepositoryRequest>,
) -> Result<Response<DeleteRepositoryResponse>, Status> {
server
.application
.delete_repository(parse_uuid(&request.into_inner().repository_id)?.into())
.await
.map_err(application_error)?;
Ok(Response::new(DeleteRepositoryResponse {}))
}
+12 -1
View File
@@ -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<RepositoryId, ApplicationError> {
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<RepositoryId, ApplicationError> {
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<RepositoryId, ApplicationError> {
self.repository
.resolve_repository(&owner, &name)
.await?
.ok_or(ApplicationError::NotFound("repository"))
}

async fn repository_coordinates(
&self,
repository_id: RepositoryId,
) -> Result<RepositoryCoordinates, ApplicationError> {
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<RepositoryId, ApplicationError> {
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
}
}
+27 -2
View File
@@ -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<Option<RepositoryId>, 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<Option<RepositoryVisibility>, 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<Option<RepositoryOwnerId>, 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<Uuid, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
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<Option<RepositoryId>, 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<Option<RepositoryCoordinates>, 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<Option<RepositoryVisibility>, 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<Option<RepositoryOwnerId>, 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<Uuid, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
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<bool, ApplicationError> {
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)
}
}