feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+542 -34
View File
@@ -12,7 +12,8 @@ use tracing::{info, instrument};
use crate::{
error::ApiError,
service::{
AdminService, AuthProfilePayload, RotateSecretPayload, SecretPayload, new_prefixed_id,
AdminAuditContext, AdminService, AuthProfilePayload, CredentialAuditRecord,
RotateSecretPayload, SecretPayload, new_prefixed_id,
},
};
@@ -57,15 +58,46 @@ impl AdminService {
})
}
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), secret_name = %payload.name))]
#[instrument(skip(self, created_by, payload, audit_context), fields(workspace_id = %workspace_id.as_str(), secret_name = %payload.name))]
pub async fn create_secret(
&self,
workspace_id: &WorkspaceId,
created_by: Option<&UserId>,
payload: SecretPayload,
audit_context: Option<&AdminAuditContext>,
) -> Result<Secret, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
validate_secret_payload(&payload)?;
if let Err(error) = self.ensure_workspace_exists(workspace_id).await {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.create_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: "pending",
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
if let Err(error) = validate_secret_payload(&payload) {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.create_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: "pending",
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
let now = OffsetDateTime::now_utc();
let secret = Secret {
@@ -82,89 +114,322 @@ impl AdminService {
let ciphertext = self
.secret_crypto
.encrypt(&payload.value)
.map_err(|error| ApiError::internal(error.to_string()))?;
.map_err(|error| ApiError::internal(error.to_string()));
let ciphertext = match ciphertext {
Ok(ciphertext) => ciphertext,
Err(error) => {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.create_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret.id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
self.registry
if let Err(error) = self
.registry
.create_secret(CreateSecretRequest {
secret: &secret,
ciphertext: &ciphertext,
key_version: self.secret_crypto.key_version(),
master_key_epoch: self.secret_crypto.master_key_epoch(),
created_by,
})
.await?;
.await
{
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.create_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret.id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
info!(
name: "admin.secret.created",
secret_id = %secret.id.as_str(),
"secret created"
);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.created",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret.id.as_str(),
credential_type: "secret",
outcome: "success",
reason: "credential_created",
},
)
.await;
Ok(secret)
}
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
#[instrument(skip(self, created_by, payload, audit_context), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
pub async fn rotate_secret(
&self,
workspace_id: &WorkspaceId,
secret_id: &SecretId,
created_by: Option<&UserId>,
payload: RotateSecretPayload,
audit_context: Option<&AdminAuditContext>,
) -> Result<Secret, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
if payload.value.is_null() {
return Err(ApiError::validation("secret value must not be null"));
if let Err(error) = self.ensure_workspace_exists(workspace_id).await {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.rotate_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
let existing = match self.get_secret(workspace_id, secret_id).await {
Ok(secret) => secret,
Err(error) => {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.rotate_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
if let Err(error) = validate_secret_value(existing.kind, &payload.value) {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.rotate_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
let now = OffsetDateTime::now_utc();
let ciphertext = self
.secret_crypto
.encrypt(&payload.value)
.map_err(|error| ApiError::internal(error.to_string()))?;
self.registry
.map_err(|error| ApiError::internal(error.to_string()));
let ciphertext = match ciphertext {
Ok(ciphertext) => ciphertext,
Err(error) => {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.rotate_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
let version = match self
.registry
.rotate_secret(RotateSecretRequest {
workspace_id,
secret_id,
ciphertext: &ciphertext,
key_version: self.secret_crypto.key_version(),
master_key_epoch: self.secret_crypto.master_key_epoch(),
created_at: &now,
updated_at: &now,
created_by,
})
.await?;
.await
{
Ok(version) => version,
Err(error) => {
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.rotate_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
info!(
name: "admin.secret.rotated",
secret_id = %secret_id.as_str(),
"secret rotated"
);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.rotated",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "success",
reason: "credential_rotated",
},
)
.await;
self.get_secret(workspace_id, secret_id).await
Ok(Secret {
current_version: version.secret_version.version,
updated_at: now,
..existing
})
}
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
#[instrument(skip(self, audit_context), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
pub async fn delete_secret(
&self,
workspace_id: &WorkspaceId,
secret_id: &SecretId,
audit_context: Option<&AdminAuditContext>,
) -> Result<(), ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
if let Some(profile) = self
if let Err(error) = self.ensure_workspace_exists(workspace_id).await {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.delete_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
let referencing_profiles = match self
.registry
.list_auth_profiles_referencing_secret(workspace_id, secret_id)
.await?
.into_iter()
.next()
.await
{
Ok(profiles) => profiles,
Err(error) => {
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.delete_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
};
if let Some(profile) = referencing_profiles.into_iter().next() {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.delete_denied",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "denied",
reason: "secret_referenced_by_auth_profile",
},
)
.await;
return Err(RegistryError::SecretReferencedByAuthProfile {
secret_id: secret_id.as_str().to_owned(),
auth_profile_id: profile.id.as_str().to_owned(),
}
.into());
}
self.registry.delete_secret(workspace_id, secret_id).await?;
if let Err(error) = self.registry.delete_secret(workspace_id, secret_id).await {
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.delete_failed",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
info!(
name: "admin.secret.deleted",
secret_id = %secret_id.as_str(),
"secret deleted"
);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.secret.deleted",
target_kind: crank_core::AuditTargetKind::Secret,
workspace_id,
target_id: secret_id.as_str(),
credential_type: "secret",
outcome: "success",
reason: "credential_deleted",
},
)
.await;
Ok(())
}
@@ -185,39 +450,118 @@ impl AdminService {
})
}
#[instrument(skip(self, payload), fields(auth_profile_name = %payload.name, auth_kind = ?payload.kind))]
#[instrument(skip(self, payload, audit_context), fields(auth_profile_name = %payload.name, auth_kind = ?payload.kind))]
pub async fn create_auth_profile(
&self,
workspace_id: &WorkspaceId,
payload: AuthProfilePayload,
audit_context: Option<&AdminAuditContext>,
) -> Result<AuthProfile, ApiError> {
validate_auth_profile_kind(payload.kind, &payload.config)?;
self.ensure_workspace_exists(workspace_id).await?;
self.validate_auth_profile_secret_ids(workspace_id, &payload.config)
.await?;
if let Err(error) = validate_auth_profile_payload(&payload) {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.auth_profile.create_failed",
target_kind: crank_core::AuditTargetKind::AuthProfile,
workspace_id,
target_id: "pending",
credential_type: "auth_profile",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
if let Err(error) = self.ensure_workspace_exists(workspace_id).await {
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.auth_profile.create_failed",
target_kind: crank_core::AuditTargetKind::AuthProfile,
workspace_id,
target_id: "pending",
credential_type: "auth_profile",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
if let Err(error) = self
.validate_auth_profile_secret_ids(workspace_id, &payload.config)
.await
{
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.auth_profile.create_failed",
target_kind: crank_core::AuditTargetKind::AuthProfile,
workspace_id,
target_id: "pending",
credential_type: "auth_profile",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
let now = OffsetDateTime::now_utc();
let profile = AuthProfile {
id: AuthProfileId::new(new_prefixed_id("auth")),
workspace_id: workspace_id.clone(),
name: payload.name,
name: payload.name.trim().to_owned(),
kind: payload.kind,
config: payload.config,
created_at: now,
updated_at: now,
};
self.registry
if let Err(error) = self
.registry
.save_auth_profile(SaveAuthProfileRequest {
workspace_id,
profile: &profile,
})
.await?;
.await
{
let error = ApiError::from(error);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.auth_profile.create_failed",
target_kind: crank_core::AuditTargetKind::AuthProfile,
workspace_id,
target_id: profile.id.as_str(),
credential_type: "auth_profile",
outcome: "failure",
reason: error.code(),
},
)
.await;
return Err(error);
}
info!(
name: "admin.auth_profile.created",
auth_profile_id = %profile.id.as_str(),
"auth profile created"
);
self.record_credential_audit(
audit_context,
CredentialAuditRecord {
action: "credential.auth_profile.created",
target_kind: crank_core::AuditTargetKind::AuthProfile,
workspace_id,
target_id: profile.id.as_str(),
credential_type: "auth_profile",
outcome: "success",
reason: "credential_created",
},
)
.await;
Ok(profile)
}
@@ -228,13 +572,45 @@ impl AdminService {
config: &AuthConfig,
) -> Result<(), ApiError> {
for secret_id in config.secret_ids() {
self.get_secret(workspace_id, secret_id).await?;
let secret = self.get_secret(workspace_id, secret_id).await?;
if secret.status != SecretStatus::Active {
return Err(RegistryError::SecretInactive {
secret_id: secret_id.as_str().to_owned(),
}
.into());
}
}
Ok(())
}
}
fn validate_auth_profile_payload(payload: &AuthProfilePayload) -> Result<(), ApiError> {
const MAX_PROFILE_NAME_CHARS: usize = 128;
let profile_name = payload.name.trim();
if profile_name.is_empty() || profile_name.chars().count() > MAX_PROFILE_NAME_CHARS {
return Err(ApiError::validation(
"auth profile name must contain 1 to 128 characters",
));
}
if profile_name.chars().any(char::is_control) {
return Err(ApiError::validation(
"auth profile name contains unsupported characters",
));
}
validate_auth_profile_kind(payload.kind, &payload.config)?;
match &payload.config {
AuthConfig::Bearer(config) => validate_auth_header_name(&config.header_name)?,
AuthConfig::Basic(_) => {}
AuthConfig::ApiKeyHeader(config) => validate_auth_header_name(&config.header_name)?,
AuthConfig::ApiKeyQuery(config) => validate_auth_query_name(&config.param_name)?,
}
Ok(())
}
fn validate_auth_profile_kind(kind: AuthKind, config: &AuthConfig) -> Result<(), ApiError> {
let is_match = matches!(
(kind, config),
@@ -252,13 +628,145 @@ fn validate_auth_profile_kind(kind: AuthKind, config: &AuthConfig) -> Result<(),
}
fn validate_secret_payload(payload: &SecretPayload) -> Result<(), ApiError> {
if payload.name.trim().is_empty() {
return Err(ApiError::validation("secret name must not be empty"));
const MAX_SECRET_NAME_CHARS: usize = 128;
let name = payload.name.trim();
if name.is_empty() || name.chars().count() > MAX_SECRET_NAME_CHARS {
return Err(ApiError::validation(
"secret name must contain 1 to 128 characters",
));
}
if name.chars().any(char::is_control) {
return Err(ApiError::validation(
"secret name contains unsupported characters",
));
}
if payload.value.is_null() {
validate_secret_value(payload.kind, &payload.value)
}
fn validate_secret_value(
kind: crank_core::SecretKind,
value: &serde_json::Value,
) -> Result<(), ApiError> {
const MAX_SECRET_VALUE_BYTES: usize = 65_536;
const MAX_SECRET_STRING_BYTES: usize = 16_384;
if value.is_null() {
return Err(ApiError::validation("secret value must not be null"));
}
let encoded = serde_json::to_vec(value)
.map_err(|_| ApiError::validation("secret value must be valid JSON"))?;
if encoded.len() > MAX_SECRET_VALUE_BYTES {
return Err(ApiError::validation("secret value is too large"));
}
match kind {
crank_core::SecretKind::Token | crank_core::SecretKind::Header => {
if value.is_string() {
validate_non_empty_secret_string(value, "secret value", MAX_SECRET_STRING_BYTES)?;
} else {
let object = value.as_object().ok_or_else(|| {
ApiError::validation(
"token/header secret must be a string or a single token/value field",
)
})?;
let field = object
.get("token")
.or_else(|| object.get("value"))
.filter(|_| object.len() == 1)
.ok_or_else(|| {
ApiError::validation("token/header secret must contain only token or value")
})?;
validate_non_empty_secret_string(field, "secret value", MAX_SECRET_STRING_BYTES)?;
}
}
crank_core::SecretKind::UsernamePassword => {
let object = value.as_object().ok_or_else(|| {
ApiError::validation("username_password secret must contain username and password")
})?;
if object.len() != 2
|| !object.contains_key("username")
|| !object.contains_key("password")
{
return Err(ApiError::validation(
"username_password secret must contain only username and password",
));
}
validate_non_empty_secret_string(
&object["username"],
"secret username",
MAX_SECRET_STRING_BYTES,
)?;
validate_non_empty_secret_string(
&object["password"],
"secret password",
MAX_SECRET_STRING_BYTES,
)?;
}
crank_core::SecretKind::Generic => {}
}
Ok(())
}
fn validate_non_empty_secret_string(
value: &serde_json::Value,
field: &str,
max_bytes: usize,
) -> Result<(), ApiError> {
let value = value
.as_str()
.ok_or_else(|| ApiError::validation(format!("{field} must be a string")))?;
if value.is_empty() {
return Err(ApiError::validation(format!("{field} must not be empty")));
}
if value.len() > max_bytes {
return Err(ApiError::validation(format!("{field} is too large")));
}
Ok(())
}
fn validate_auth_header_name(value: &str) -> Result<(), ApiError> {
const MAX_HEADER_NAME_BYTES: usize = 128;
if value.is_empty()
|| value.len() > MAX_HEADER_NAME_BYTES
|| !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
{
return Err(ApiError::validation("auth header name is invalid"));
}
Ok(())
}
fn validate_auth_query_name(value: &str) -> Result<(), ApiError> {
const MAX_QUERY_NAME_BYTES: usize = 128;
if value.is_empty()
|| value.len() > MAX_QUERY_NAME_BYTES
|| !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b'[' | b']')
})
{
return Err(ApiError::validation("auth query parameter name is invalid"));
}
Ok(())
}