use crate::access_control::{AccessLevel, Role, RoleAccessMapping, RoleStatus};
use crate::error::SkiljRejection;
use crate::event_store::{BoundedContext, BoundedContextStatus};
pub const ADMIN_BOUNDED_CONTEXT_NAME: &str = "admin";
pub fn stamp_admin_bounded_context(
existing: Option<&BoundedContext>,
now: chrono::DateTime<chrono::Utc>,
) -> Option<BoundedContext> {
if existing.is_some() {
return None;
}
Some(BoundedContext {
name: ADMIN_BOUNDED_CONTEXT_NAME.to_string(),
status: BoundedContextStatus::Active,
created_at: now,
created_by: ContextCreator::SystemCreator,
template: None,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BootstrapSecret {
pub secret: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContextCreator {
SuperadminCreator {
role: Role,
},
SystemCreator,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("a superadmin Role already exists; the bootstrap secret can never be used again")]
SuperadminAlreadyExists,
#[error("the presented bootstrap secret doesn't match")]
BootstrapSecretMismatch,
#[error("a bounded context with this name already exists")]
BoundedContextNameTaken,
#[error(
"bounded context names must start with a lowercase letter, contain only lowercase \
letters, digits and underscores, and be at most 40 characters long"
)]
InvalidBoundedContextName,
#[error("the admin bounded context can never be archived")]
CannotArchiveAdminContext,
#[error("a bounded context must be archived before it can be deleted")]
BoundedContextNotArchived,
#[error("the admin bounded context can never be deleted")]
CannotDeleteAdminContext,
#[error("this external_subject is already bound to another active Role")]
ExternalSubjectAlreadyClaimed,
#[error("a BoundedContext used as a template must have no template of its own")]
TemplateItselfTemplated,
#[error("this bounded context has no template to resync from")]
BoundedContextHasNoTemplate,
}
impl SkiljRejection for Error {
fn code(&self) -> &str {
match self {
Error::SuperadminAlreadyExists => "superadmin_already_exists",
Error::BootstrapSecretMismatch => "bootstrap_secret_mismatch",
Error::BoundedContextNameTaken => "bounded_context_name_taken",
Error::InvalidBoundedContextName => "invalid_bounded_context_name",
Error::CannotArchiveAdminContext => "cannot_archive_admin_context",
Error::BoundedContextNotArchived => "bounded_context_not_archived",
Error::CannotDeleteAdminContext => "cannot_delete_admin_context",
Error::ExternalSubjectAlreadyClaimed => "external_subject_already_claimed",
Error::TemplateItselfTemplated => "template_itself_templated",
Error::BoundedContextHasNoTemplate => "bounded_context_has_no_template",
}
}
fn message(&self) -> String {
self.to_string()
}
}
#[derive(Clone, Default)]
pub struct BootstrapGate(std::sync::Arc<tokio::sync::Mutex<Option<BootstrapSecret>>>);
impl BootstrapGate {
pub fn new(secret: Option<BootstrapSecret>) -> Self {
Self(std::sync::Arc::new(tokio::sync::Mutex::new(secret)))
}
pub async fn lock(&self) -> tokio::sync::MutexGuard<'_, Option<BootstrapSecret>> {
self.0.lock().await
}
pub fn current(&self) -> Option<String> {
self.0
.try_lock()
.ok()
.and_then(|secret| secret.as_ref().map(|s| s.secret.clone()))
}
}
pub fn generate_bootstrap_secret(existing_roles: &[Role]) -> Option<BootstrapSecret> {
if existing_roles
.iter()
.any(|r| r.superadmin && r.status == RoleStatus::Active)
{
return None;
}
Some(BootstrapSecret {
secret: crate::shared::generate_token_secret(),
})
}
pub fn create_superadmin(
bootstrap: &BootstrapSecret,
presented_secret: &str,
name: String,
external_subject: String,
existing_roles: &[Role],
id: String,
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<Role> {
if existing_roles
.iter()
.any(|r| r.superadmin && r.status == RoleStatus::Active)
{
return Err(Error::SuperadminAlreadyExists.into());
}
if !crate::shared::secret_matches(presented_secret, &bootstrap.secret) {
return Err(Error::BootstrapSecretMismatch.into());
}
if existing_roles
.iter()
.any(|r| r.external_subject == external_subject && r.status == RoleStatus::Active)
{
return Err(Error::ExternalSubjectAlreadyClaimed.into());
}
Ok(Role {
id,
external_subject,
name,
superadmin: true,
status: RoleStatus::Active,
created_at: now,
revoked_at: None,
})
}
fn valid_bounded_context_name(name: &str) -> bool {
if name.is_empty() || name.len() > 40 {
return false;
}
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_lowercase() {
return false;
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
pub fn add_bounded_context(
caller: &Role,
name: String,
existing_contexts: &[BoundedContext],
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<BoundedContext> {
crate::access_control::require_active_superadmin(caller)?;
if existing_contexts.iter().any(|bc| bc.name == name) {
return Err(Error::BoundedContextNameTaken.into());
}
if !valid_bounded_context_name(&name) {
return Err(Error::InvalidBoundedContextName.into());
}
Ok(BoundedContext {
name,
status: BoundedContextStatus::Active,
created_at: now,
created_by: ContextCreator::SuperadminCreator {
role: caller.clone(),
},
template: None,
})
}
pub fn create_bounded_context_from_template(
caller: &Role,
template: &BoundedContext,
name: String,
existing_contexts: &[BoundedContext],
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<BoundedContext> {
crate::access_control::require_active_superadmin(caller)?;
if existing_contexts.iter().any(|bc| bc.name == name) {
return Err(Error::BoundedContextNameTaken.into());
}
if !valid_bounded_context_name(&name) {
return Err(Error::InvalidBoundedContextName.into());
}
if template.status != BoundedContextStatus::Active {
return Err(crate::event_store::Error::BoundedContextArchived.into());
}
if template.template.is_some() {
return Err(Error::TemplateItselfTemplated.into());
}
Ok(BoundedContext {
name,
status: BoundedContextStatus::Active,
created_at: now,
created_by: ContextCreator::SuperadminCreator {
role: caller.clone(),
},
template: Some(Box::new(template.clone())),
})
}
pub fn resync_bounded_context_from_template<'a>(
caller: &Role,
bounded_context: &'a BoundedContext,
) -> crate::error::Result<&'a BoundedContext> {
crate::access_control::require_active_superadmin(caller)?;
if bounded_context.status != BoundedContextStatus::Active {
return Err(crate::event_store::Error::BoundedContextArchived.into());
}
let Some(template) = bounded_context.template.as_deref() else {
return Err(Error::BoundedContextHasNoTemplate.into());
};
if template.status != BoundedContextStatus::Active {
return Err(crate::event_store::Error::BoundedContextArchived.into());
}
Ok(template)
}
pub fn list_bounded_contexts(
caller: &Role,
bounded_contexts: &[BoundedContext],
) -> crate::error::Result<Vec<BoundedContext>> {
crate::access_control::require_active_superadmin(caller)?;
Ok(bounded_contexts.to_vec())
}
pub fn archive_bounded_context(
access_mapping: &RoleAccessMapping,
bounded_context: &BoundedContext,
) -> crate::error::Result<BoundedContext> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !access_mapping.bounded_context.same_as(bounded_context) {
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if bounded_context.status != BoundedContextStatus::Active {
return Err(crate::event_store::Error::BoundedContextArchived.into());
}
if bounded_context.name == ADMIN_BOUNDED_CONTEXT_NAME {
return Err(Error::CannotArchiveAdminContext.into());
}
Ok(BoundedContext {
status: BoundedContextStatus::Archived,
..bounded_context.clone()
})
}
pub fn delete_bounded_context(
caller: &Role,
bounded_context: &BoundedContext,
) -> crate::error::Result<()> {
crate::access_control::require_active_superadmin(caller)?;
if bounded_context.status != BoundedContextStatus::Archived {
return Err(Error::BoundedContextNotArchived.into());
}
if bounded_context.name == ADMIN_BOUNDED_CONTEXT_NAME {
return Err(Error::CannotDeleteAdminContext.into());
}
Ok(())
}