feat: issue workflow repository tokens #34

Manually merged
day01 merged 1 commits from feat/0.6-workflow-repository-token into develop 2026-08-30 16:06:22 +00:00
15 changed files with 327 additions and 71 deletions
Showing only changes of commit 44a7f61808 - Show all commits
+51 -4
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -22
View File
@@ -1,249 +1,235 @@
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
}
}
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,
};

use crate::wire::identity_server::Identity;
use crate::wire::{
ArchiveRepositoryProjectionRequest, ArchiveRepositoryProjectionResponse,
ArchiveRepositoryRequest, ArchiveRepositoryResponse, CheckBranchProtectionBypassRequest,
CheckBranchProtectionBypassResponse, CheckCapabilitiesRequest, CheckCapabilitiesResponse,
CheckCapabilityRequest, CheckCapabilityResponse, DeleteRepositoryRequest,
DeleteRepositoryResponse, GetRepositoryCoordinatesRequest, GetRepositoryCoordinatesResponse,
IssueWorkflowRepositoryTokenRequest, IssueWorkflowRepositoryTokenResponse,
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 issue_workflow_repository_token(
&self,
request: Request<IssueWorkflowRepositoryTokenRequest>,
) -> Result<Response<IssueWorkflowRepositoryTokenResponse>, Status> {
repository::issue_workflow_token(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> {
access::validate_session(self, request).await
}

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
}
}
+14 -14
View File
@@ -1,242 +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,
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 }
}
}
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 application;
mod repository;
mod types;

pub use application::*;
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 fn issue_workflow_repository_token(
&self,
user_id: UserId,
repository_id: RepositoryId,
token: &str,
expires_at: DateTime<Utc>,
) -> Result<(), 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>;
async fn issue_workflow_repository_token(
&self,
user_id: UserId,
repository_id: RepositoryId,
) -> Result<crate::IssuedRepositoryToken, ApplicationError>;
}

pub trait IdentityBridgeUseCases:
IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases
{
}

impl<T> IdentityBridgeUseCases for T where
T: IdentityBridgeAccessUseCases + IdentityBridgeRepositoryUseCases + ?Sized
{
}
+1
View File
@@ -1,23 +1,24 @@
use std::collections::BTreeSet;
use std::sync::Arc;

use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use syncode_identity_model::{
AgentDefinitionSlug, Capability, InstanceHost, LocalAgentId, LocalAgentName,
ParsedSshPublicKey, RepositoryId, SshKeyId, UserId,
};

use crate::ApplicationError;

mod application;
mod helpers;
mod ports;
mod types;
mod use_cases;

pub use application::*;
pub use ports::*;
pub use types::*;

use helpers::*;
use std::collections::BTreeSet;
use std::sync::Arc;

use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use syncode_identity_model::{
AgentDefinitionSlug, Capability, InstanceHost, LocalAgentId, LocalAgentName,
ParsedSshPublicKey, RepositoryId, SshKeyId, UserId,
};

use crate::ApplicationError;

mod application;
mod helpers;
mod ports;
mod types;
mod use_cases;

pub use application::*;
pub use ports::*;
pub use types::*;

pub(crate) use helpers::issued_repository_token;
use helpers::*;
+24 -24
View File
@@ -1,250 +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, 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
}
}
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;

#[async_trait]
impl IdentityBridgeRepository for Postgres {
async fn validate_web_session(
&self,
token: &str,
) -> Result<Option<(UserId, chrono::DateTime<chrono::Utc>)>, ApplicationError> {
self.bridge_validate_web_session(token).await
}
async fn validate_access_token(
&self,
token: &str,
) -> Result<Option<AccessTokenIdentity>, ApplicationError> {
self.bridge_validate_access_token(token).await
}
async fn load_access_token(
&self,
token_id: AccessTokenId,
) -> Result<Option<AccessTokenIdentity>, ApplicationError> {
self.bridge_load_access_token(token_id.get()).await
}
async fn load_user_principal(
&self,
user_id: UserId,
) -> Result<UserPrincipal, ApplicationError> {
self.bridge_load_user_principal(user_id.get()).await
}

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> {
self.bridge_resolve_ssh_key(fingerprint).await
}

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
}

async fn issue_workflow_repository_token(
&self,
user_id: UserId,
repository_id: RepositoryId,
token: &str,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<(), ApplicationError> {
Postgres::issue_workflow_repository_token(
self,
user_id.get(),
repository_id.get(),
token,
expires_at,
)
.await
.map(|_| ())
.map_err(persistence)
}
}
+1
View File
@@ -1,132 +1,133 @@
mod access_tokens;
mod account;
mod account_deletion;
mod application;
mod branch_protection;
mod bridge;
mod cli_login;
mod contributions;
mod email_verification;
mod emails;
mod events;
mod grants;
mod heatmap_query;
mod linked_identity_tokens;
mod local_agent_application;
mod local_agent_sessions;
mod local_agents;
mod oauth;
mod oauth_application;
mod oauth_user_projection;
mod personal_tokens;
mod platform_agents;
mod principals;
mod repository_identity;
mod seed;
mod sessions;
mod settings;
mod ssh_keys;
mod teams;

pub use access_tokens::AccessTokenRecord;
pub use account::{LinkedIdentityRecord, UnlinkError, UserRecord};
pub use branch_protection::{BranchProtectionBypassRecord, BranchProtectionRuleRecord};
pub use contributions::{LinkedIdentityTokenRecord, LinkedUsername};
pub use email_verification::PendingEmailVerification;
pub use emails::{RemoveEmailError, SetPrimaryEmailError, UserEmailRecord};
pub use events::PendingEvent;
pub use grants::GrantRecord;
pub use heatmap_query::{HeatmapPrivacySetting, NativeContributionDay, ProviderContributionDay};
pub use local_agent_sessions::{
EnrollLocalAgentSession, EnrolledLocalAgentSession, HeartbeatLocalAgentSession,
LocalAgentHeartbeat,
};
pub use local_agents::{CreateLocalAgentError, LocalAgentRecord};
pub use oauth::{AuthProvider, LinkError, OAuthClientConfig};
pub use platform_agents::{AgentDefinitionRecord, PlatformAgentGrantRecord, PlatformAgentRecord};
pub use repository_identity::RepositoryIdentityRecord;
pub use seed::MIGRATION_SYSTEM_USERNAME;
pub use settings::{
AddKeyError, CreateOrganizationError, PersonalTokenRecord, SigningKeyRecord, UserSshKeyRecord,
};
pub use ssh_keys::{AddSshKeyError, SshKeyOwner};
pub use syncode_identity_application::SESSION_COOKIE_NAME;
pub use teams::{OrganizationRecord, TeamGrantRecord, TeamRecord};

use sqlx::migrate::MigrateError;
use sqlx::postgres::{PgPool, PgPoolOptions};
use syncode_identity_model::{Capability, ForgeProjection, ForgeProjectionStatus};
use thiserror::Error;

pub(crate) fn application_persistence(
error: impl std::fmt::Display,
) -> syncode_identity_application::ApplicationError {
syncode_identity_application::ApplicationError::Persistence(error.to_string())
}

#[derive(Debug, Error)]
pub enum StoreError {
#[error("the store cannot be reached: {0}")]
Unreachable(#[from] sqlx::Error),

#[error("the schema migrations did not apply: {0}")]
Migration(#[from] MigrateError),

#[error("a stored grant or resource kind is not one this build recognizes: {0}")]
UnknownKind(String),

#[error("a provider's oauth_client_config does not match the expected shape: {0}")]
MalformedConfig(#[source] serde_json::Error),

#[error("a platform agent policy cannot be encoded: {0}")]
MalformedPolicy(#[source] serde_json::Error),

#[error("imported identity data conflicts with existing state: {0}")]
ImportConflict(String),
}

pub(crate) fn decode_capabilities(
values: Vec<String>,
) -> Result<std::collections::BTreeSet<Capability>, StoreError> {
values
.into_iter()
.map(|value| {
value
.parse()
.map_err(|_| StoreError::UnknownKind(format!("capability:{value}")))
})
.collect()
}

pub(crate) fn decode_forge_projection(
status: String,
error: Option<String>,
) -> Result<ForgeProjection, StoreError> {
Ok(ForgeProjection {
status: status
.parse::<ForgeProjectionStatus>()
.map_err(|_| StoreError::UnknownKind(format!("forge_projection_status:{status}")))?,
error,
})
}

#[derive(Clone)]
pub struct Postgres {
pool: PgPool,
}

impl Postgres {
pub async fn connect(url: &str, connections: u32) -> Result<Self, StoreError> {
let pool = PgPoolOptions::new()
.max_connections(connections)
.connect(url)
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(Self { pool })
}

#[must_use]
pub const fn pool(&self) -> &PgPool {
&self.pool
}
}
mod access_tokens;
mod account;
mod account_deletion;
mod application;
mod branch_protection;
mod bridge;
mod cli_login;
mod contributions;
mod email_verification;
mod emails;
mod events;
mod grants;
mod heatmap_query;
mod linked_identity_tokens;
mod local_agent_application;
mod local_agent_sessions;
mod local_agents;
mod oauth;
mod oauth_application;
mod oauth_user_projection;
mod personal_tokens;
mod platform_agents;
mod principals;
mod repository_identity;
mod seed;
mod sessions;
mod settings;
mod ssh_keys;
mod teams;
mod workflow_repository_tokens;

pub use access_tokens::AccessTokenRecord;
pub use account::{LinkedIdentityRecord, UnlinkError, UserRecord};
pub use branch_protection::{BranchProtectionBypassRecord, BranchProtectionRuleRecord};
pub use contributions::{LinkedIdentityTokenRecord, LinkedUsername};
pub use email_verification::PendingEmailVerification;
pub use emails::{RemoveEmailError, SetPrimaryEmailError, UserEmailRecord};
pub use events::PendingEvent;
pub use grants::GrantRecord;
pub use heatmap_query::{HeatmapPrivacySetting, NativeContributionDay, ProviderContributionDay};
pub use local_agent_sessions::{
EnrollLocalAgentSession, EnrolledLocalAgentSession, HeartbeatLocalAgentSession,
LocalAgentHeartbeat,
};
pub use local_agents::{CreateLocalAgentError, LocalAgentRecord};
pub use oauth::{AuthProvider, LinkError, OAuthClientConfig};
pub use platform_agents::{AgentDefinitionRecord, PlatformAgentGrantRecord, PlatformAgentRecord};
pub use repository_identity::RepositoryIdentityRecord;
pub use seed::MIGRATION_SYSTEM_USERNAME;
pub use settings::{
AddKeyError, CreateOrganizationError, PersonalTokenRecord, SigningKeyRecord, UserSshKeyRecord,
};
pub use ssh_keys::{AddSshKeyError, SshKeyOwner};
pub use syncode_identity_application::SESSION_COOKIE_NAME;
pub use teams::{OrganizationRecord, TeamGrantRecord, TeamRecord};

use sqlx::migrate::MigrateError;
use sqlx::postgres::{PgPool, PgPoolOptions};
use syncode_identity_model::{Capability, ForgeProjection, ForgeProjectionStatus};
use thiserror::Error;

pub(crate) fn application_persistence(
error: impl std::fmt::Display,
) -> syncode_identity_application::ApplicationError {
syncode_identity_application::ApplicationError::Persistence(error.to_string())
}

#[derive(Debug, Error)]
pub enum StoreError {
#[error("the store cannot be reached: {0}")]
Unreachable(#[from] sqlx::Error),

#[error("the schema migrations did not apply: {0}")]
Migration(#[from] MigrateError),

#[error("a stored grant or resource kind is not one this build recognizes: {0}")]
UnknownKind(String),

#[error("a provider's oauth_client_config does not match the expected shape: {0}")]
MalformedConfig(#[source] serde_json::Error),

#[error("a platform agent policy cannot be encoded: {0}")]
MalformedPolicy(#[source] serde_json::Error),

#[error("imported identity data conflicts with existing state: {0}")]
ImportConflict(String),
}

pub(crate) fn decode_capabilities(
values: Vec<String>,
) -> Result<std::collections::BTreeSet<Capability>, StoreError> {
values
.into_iter()
.map(|value| {
value
.parse()
.map_err(|_| StoreError::UnknownKind(format!("capability:{value}")))
})
.collect()
}

pub(crate) fn decode_forge_projection(
status: String,
error: Option<String>,
) -> Result<ForgeProjection, StoreError> {
Ok(ForgeProjection {
status: status
.parse::<ForgeProjectionStatus>()
.map_err(|_| StoreError::UnknownKind(format!("forge_projection_status:{status}")))?,
error,
})
}

#[derive(Clone)]
pub struct Postgres {
pool: PgPool,
}

impl Postgres {
pub async fn connect(url: &str, connections: u32) -> Result<Self, StoreError> {
let pool = PgPoolOptions::new()
.max_connections(connections)
.connect(url)
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(Self { pool })
}

#[must_use]
pub const fn pool(&self) -> &PgPool {
&self.pool
}
}
+30 -1
View File
@@ -1,80 +1,109 @@
use tonic::{Request, Response, Status};

use super::IdentityServer;
use super::conversion::{application_error, application_principal, parse_uuid};
use crate::wire::{
BranchProtectionRule, CheckBranchProtectionBypassRequest, CheckBranchProtectionBypassResponse,
ListBranchProtectionRulesRequest, ListBranchProtectionRulesResponse,
ListUserSigningKeysRequest, ListUserSigningKeysResponse, PrincipalKind, SigningKey,
};

pub(super) async fn check_branch_protection_bypass(
server: &IdentityServer,
request: Request<CheckBranchProtectionBypassRequest>,
) -> Result<Response<CheckBranchProtectionBypassResponse>, Status> {
let request = request.into_inner();
let principal = if request.principal_kind() == PrincipalKind::Unspecified {
None
} else {
Some(application_principal(
request.principal_kind(),
&request.principal_id,
)?)
};
let decision = server
.application
.check_branch_protection_bypass(
parse_uuid(&request.repository_id)?.into(),
&request.pattern,
principal,
)
.await
.map_err(application_error)?;
Ok(Response::new(CheckBranchProtectionBypassResponse {
allowed: decision.bypass_allowed,
protected: decision.protected,
matched_pattern: decision.matched_pattern,
require_review_count: decision.require_review_count,
required_status_checks: decision.required_status_checks,
}))
}

pub(super) async fn list_branch_protection_rules(
server: &IdentityServer,
request: Request<ListBranchProtectionRulesRequest>,
) -> Result<Response<ListBranchProtectionRulesResponse>, Status> {
let repository_id = parse_uuid(&request.into_inner().repository_id)?.into();
let rules = server
.application
.list_branch_protection_rules(repository_id)
.await
.map_err(application_error)?
.into_iter()
.map(|rule| BranchProtectionRule {
pattern: rule.pattern,
require_review_count: rule.require_review_count,
required_status_checks: rule.required_status_checks,
})
.collect();
Ok(Response::new(ListBranchProtectionRulesResponse { rules }))
}

pub(super) async fn list_user_signing_keys(
server: &IdentityServer,
request: Request<ListUserSigningKeysRequest>,
) -> Result<Response<ListUserSigningKeysResponse>, Status> {
let user_id = parse_uuid(&request.into_inner().user_id)?.into();
let keys = server
.application
.list_user_signing_keys(user_id)
.await
.map_err(application_error)?
.into_iter()
.map(|key| SigningKey {
key_type: key.key_type,
key_id: key.key_id,
public_key: key.public_key,
})
.collect();
Ok(Response::new(ListUserSigningKeysResponse { keys }))
}
use tonic::{Request, Response, Status};

use super::IdentityServer;
use super::conversion::{
application_error, application_principal, parse_uuid, wire_principal_kind, wire_resource,
};
use crate::wire::{
BranchProtectionRule, CheckBranchProtectionBypassRequest, CheckBranchProtectionBypassResponse,
ListBranchProtectionRulesRequest, ListBranchProtectionRulesResponse,
ListUserSigningKeysRequest, ListUserSigningKeysResponse, PrincipalKind, SigningKey,
ValidateSessionRequest, ValidateSessionResponse,
};

pub(super) async fn validate_session(
server: &IdentityServer,
request: Request<ValidateSessionRequest>,
) -> Result<Response<ValidateSessionResponse>, Status> {
let credential = server
.application
.validate_credential(&request.into_inner().session_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,
}))
}

pub(super) async fn check_branch_protection_bypass(
server: &IdentityServer,
request: Request<CheckBranchProtectionBypassRequest>,
) -> Result<Response<CheckBranchProtectionBypassResponse>, Status> {
let request = request.into_inner();
let principal = if request.principal_kind() == PrincipalKind::Unspecified {
None
} else {
Some(application_principal(
request.principal_kind(),
&request.principal_id,
)?)
};
let decision = server
.application
.check_branch_protection_bypass(
parse_uuid(&request.repository_id)?.into(),
&request.pattern,
principal,
)
.await
.map_err(application_error)?;
Ok(Response::new(CheckBranchProtectionBypassResponse {
allowed: decision.bypass_allowed,
protected: decision.protected,
matched_pattern: decision.matched_pattern,
require_review_count: decision.require_review_count,
required_status_checks: decision.required_status_checks,
}))
}

pub(super) async fn list_branch_protection_rules(
server: &IdentityServer,
request: Request<ListBranchProtectionRulesRequest>,
) -> Result<Response<ListBranchProtectionRulesResponse>, Status> {
let repository_id = parse_uuid(&request.into_inner().repository_id)?.into();
let rules = server
.application
.list_branch_protection_rules(repository_id)
.await
.map_err(application_error)?
.into_iter()
.map(|rule| BranchProtectionRule {
pattern: rule.pattern,
require_review_count: rule.require_review_count,
required_status_checks: rule.required_status_checks,
})
.collect();
Ok(Response::new(ListBranchProtectionRulesResponse { rules }))
}

pub(super) async fn list_user_signing_keys(
server: &IdentityServer,
request: Request<ListUserSigningKeysRequest>,
) -> Result<Response<ListUserSigningKeysResponse>, Status> {
let user_id = parse_uuid(&request.into_inner().user_id)?.into();
let keys = server
.application
.list_user_signing_keys(user_id)
.await
.map_err(application_error)?
.into_iter()
.map(|key| SigningKey {
key_type: key.key_type,
key_id: key.key_id,
public_key: key.public_key,
})
.collect();
Ok(Response::new(ListUserSigningKeysResponse { keys }))
}
+20
View File
@@ -1,195 +1,215 @@
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 {}))
}
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,
IssueWorkflowRepositoryTokenRequest, IssueWorkflowRepositoryTokenResponse,
ProvisionRepositoryRequest, ProvisionRepositoryResponse, RegisterRepositoryRequest,
RegisterRepositoryResponse, ResolveRepositoryRequest, ResolveRepositoryResponse,
UnarchiveRepositoryRequest, UnarchiveRepositoryResponse, UpdateRepositoryMetadataRequest,
UpdateRepositoryMetadataResponse, UpdateRepositoryVisibilityRequest,
UpdateRepositoryVisibilityResponse,
};

pub(super) async fn issue_workflow_token(
server: &IdentityServer,
request: Request<IssueWorkflowRepositoryTokenRequest>,
) -> Result<Response<IssueWorkflowRepositoryTokenResponse>, Status> {
let request = request.into_inner();
let issued = server
.application
.issue_workflow_repository_token(
parse_uuid(&request.user_id)?.into(),
parse_uuid(&request.repository_id)?.into(),
)
.await
.map_err(application_error)?;
Ok(Response::new(IssueWorkflowRepositoryTokenResponse {
token: issued.token,
expires_at_unix: issued.expires_at.timestamp(),
}))
}

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 {}))
}
+33 -3
View File
@@ -1,126 +1,156 @@
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
}
}
use async_trait::async_trait;
use syncode_identity_model::{
Capability, RepositoryCoordinates, RepositoryId, RepositoryName, RepositoryOwner,
RepositoryOwnerId, RepositoryVisibility, Resource, UserId,
};

use super::{
BridgePrincipalId, IdentityBridgeAccessUseCases, 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
}

async fn issue_workflow_repository_token(
&self,
user_id: UserId,
repository_id: RepositoryId,
) -> Result<crate::IssuedRepositoryToken, ApplicationError> {
let allowed = self
.check_capability(
BridgePrincipalId::User(user_id),
Resource::repository(repository_id),
Capability::RepositoryRead,
)
.await?;
if !allowed {
return Err(ApplicationError::Forbidden);
}
let issued = crate::local_agent::issued_repository_token();
self.repository
.issue_workflow_repository_token(
user_id,
repository_id,
&issued.token,
issued.expires_at,
)
.await?;
Ok(issued)
}
}
@@ -1,16 +1,16 @@
use super::*;

pub(super) fn generate_credential(prefix: &str) -> String {
format!(
"{prefix}{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
)
}

pub(super) fn issued_repository_token() -> IssuedRepositoryToken {
IssuedRepositoryToken {
token: generate_credential("syn_rat_"),
expires_at: Utc::now() + Duration::minutes(REPOSITORY_TOKEN_LIFETIME_MINUTES),
}
}
use super::*;

pub(super) fn generate_credential(prefix: &str) -> String {
format!(
"{prefix}{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
)
}

pub(crate) fn issued_repository_token() -> IssuedRepositoryToken {
IssuedRepositoryToken {
token: generate_credential("syn_rat_"),
expires_at: Utc::now() + Duration::minutes(REPOSITORY_TOKEN_LIFETIME_MINUTES),
}
}
+60 -2
View File
@@ -1,49 +1,107 @@
use syncode_identity_application::{
ActiveBranchProtectionRule, ApplicationError, LocalAgentIdentity, SigningKeyIdentity,
};
use uuid::Uuid;

use super::token::{active_branch_protection_rule, signing_key};
use crate::{Postgres, application_persistence as persistence};

impl Postgres {
pub(super) async fn bridge_local_agent(
&self,
agent_id: Uuid,
) -> Result<Option<LocalAgentIdentity>, ApplicationError> {
self.load_local_agent(agent_id)
.await
.map(|agent| {
agent.map(|agent| LocalAgentIdentity {
owner_user_id: agent.owner_user_id.into(),
restriction: agent.restriction,
})
})
.map_err(persistence)
}

pub(super) async fn bridge_active_branch_protection_rules(
&self,
repository_id: Uuid,
) -> Result<Vec<ActiveBranchProtectionRule>, ApplicationError> {
self.find_branch_protection_rules(repository_id)
.await
.map(|rules| {
rules
.into_iter()
.map(active_branch_protection_rule)
.collect()
})
.map_err(persistence)
}

pub(super) async fn bridge_user_signing_keys(
&self,
user_id: Uuid,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError> {
self.find_user_signing_keys(user_id)
.await
.map(|keys| keys.into_iter().map(signing_key).collect())
.map_err(persistence)
}
}
use syncode_identity_application::{
AccessTokenIdentity, ActiveBranchProtectionRule, ApplicationError, LocalAgentIdentity,
SigningKeyIdentity, SshKeyIdentity,
};
use syncode_identity_model::UserPrincipal;
use uuid::Uuid;

use super::token::{access_token, active_branch_protection_rule, signing_key, ssh_key};
use crate::{Postgres, application_persistence as persistence};

impl Postgres {
pub(super) async fn bridge_validate_web_session(
&self,
token: &str,
) -> Result<
Option<(
syncode_identity_model::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)
}

pub(super) async fn bridge_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)
}

pub(super) async fn bridge_load_access_token(
&self,
token_id: Uuid,
) -> Result<Option<AccessTokenIdentity>, ApplicationError> {
Postgres::load_access_token(self, token_id)
.await
.map(|token| token.map(access_token))
.map_err(persistence)
}

pub(super) async fn bridge_load_user_principal(
&self,
user_id: Uuid,
) -> Result<UserPrincipal, ApplicationError> {
Postgres::load_user_principal(self, user_id)
.await
.map_err(persistence)
}

pub(super) async fn bridge_resolve_ssh_key(
&self,
fingerprint: &str,
) -> Result<Option<SshKeyIdentity>, ApplicationError> {
Postgres::resolve_ssh_key(self, fingerprint)
.await
.map_err(persistence)?
.map(ssh_key)
.transpose()
}

pub(super) async fn bridge_local_agent(
&self,
agent_id: Uuid,
) -> Result<Option<LocalAgentIdentity>, ApplicationError> {
self.load_local_agent(agent_id)
.await
.map(|agent| {
agent.map(|agent| LocalAgentIdentity {
owner_user_id: agent.owner_user_id.into(),
restriction: agent.restriction,
})
})
.map_err(persistence)
}

pub(super) async fn bridge_active_branch_protection_rules(
&self,
repository_id: Uuid,
) -> Result<Vec<ActiveBranchProtectionRule>, ApplicationError> {
self.find_branch_protection_rules(repository_id)
.await
.map(|rules| {
rules
.into_iter()
.map(active_branch_protection_rule)
.collect()
})
.map_err(persistence)
}

pub(super) async fn bridge_user_signing_keys(
&self,
user_id: Uuid,
) -> Result<Vec<SigningKeyIdentity>, ApplicationError> {
self.find_user_signing_keys(user_id)
.await
.map(|keys| keys.into_iter().map(signing_key).collect())
.map_err(persistence)
}
}
@@ -1,0 +1,15 @@
use std::sync::Arc;

use super::IdentityBridgeRepository;

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

impl IdentityBridgeApplication {
#[must_use]
pub fn new(repository: Arc<dyn IdentityBridgeRepository>) -> Self {
Self { repository }
}
}
@@ -1,0 +1,22 @@
ALTER TABLE access_token
DROP CONSTRAINT access_token_git_http_scope_check,
ADD CONSTRAINT access_token_git_http_scope_check CHECK (
audience != 'git_http'
OR (
resource_kind = 'repository'
AND resource_id IS NOT NULL
AND (
(
local_agent_id IS NOT NULL
AND user_id IS NULL
AND name = 'local-agent-repository'
)
OR (
user_id IS NOT NULL
AND local_agent_id IS NULL
AND name IN ('syn-repository-clone', 'syncode-workflow-repository')
AND capabilities = ARRAY['repo:read']
)
)
)
);
@@ -1,0 +1,36 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;

use crate::sessions::hash_token;
use crate::{Postgres, StoreError};

impl Postgres {
pub async fn issue_workflow_repository_token(
&self,
user_id: Uuid,
repository_id: Uuid,
token: &str,
expires_at: DateTime<Utc>,
) -> Result<Uuid, StoreError> {
let id = Uuid::new_v4();
sqlx::query_scalar::<_, Uuid>(
"INSERT INTO access_token (
id, user_id, local_agent_id, name, token_hash, capabilities,
resource_kind, resource_id, audience, expires_at, created_at
)
SELECT $1, account.id, NULL, 'syncode-workflow-repository', $4,
ARRAY['repo:read'], 'repository', $3, 'git_http', $5, now()
FROM \"user\" account
WHERE account.id = $2 AND account.deleted_at IS NULL
RETURNING access_token.id",
)
.bind(id)
.bind(user_id)
.bind(repository_id)
.bind(hash_token(token))
.bind(expires_at)
.fetch_one(&self.pool)
.await
.map_err(Into::into)
}
}