feat: issue workflow repository tokens #34
+51
-4
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,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::*;
|
||||
@@ -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,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
|
||||
}
|
||||
}
|
||||
@@ -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 }))
|
||||
}
|
||||
@@ -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 {}))
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user