feat: resolve auth profiles through secret ids

This commit is contained in:
a.tolmachev
2026-04-07 00:43:35 +03:00
parent a6388e4353
commit 191e749b14
10 changed files with 237 additions and 40 deletions
+106 -1
View File
@@ -1520,6 +1520,18 @@ mod tests {
let upstream_base_url = spawn_upstream_server().await;
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let secret = client
.post(format!("{base_url}/secrets"))
.json(&json!({
"name": "crm-api-token",
"kind": SecretKind::Token,
"value": { "token": "super-secret-token" }
}))
.send()
.await
.unwrap();
let secret = assert_success_json(secret).await;
let secret_id = secret["id"].as_str().unwrap();
let auth_profile = client
.post(format!("{base_url}/auth-profiles"))
@@ -1529,7 +1541,7 @@ mod tests {
"config": {
"api_key_header": {
"header_name": "X-Api-Key",
"secret_ref": "secret://crm/api-key"
"secret_id": secret_id
}
}
}))
@@ -1571,6 +1583,10 @@ mod tests {
.unwrap();
assert_eq!(auth_profile["kind"], "api_key_header");
assert_eq!(
auth_profile["config"]["api_key_header"]["secret_id"],
secret_id
);
assert_eq!(imported["operation_id"], operation_id);
assert_eq!(imported["version"], 2);
assert_eq!(imported["import_mode"], "upsert");
@@ -1649,6 +1665,95 @@ mod tests {
assert_eq!(missing["error"]["code"], "not_found");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_auth_profile_with_missing_secret() {
let registry = test_registry().await;
let storage_root = test_storage_root("missing_secret_auth");
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let response = client
.post(format!("{base_url}/auth-profiles"))
.json(&json!({
"name": "crm-header",
"kind": "api_key_header",
"config": {
"api_key_header": {
"header_name": "X-Api-Key",
"secret_id": "secret_missing"
}
}
}))
.send()
.await
.unwrap();
let status = response.status();
let body = response.json::<Value>().await.unwrap();
assert_eq!(status, reqwest::StatusCode::NOT_FOUND);
assert_eq!(body["error"]["code"], "not_found");
assert_eq!(
body["error"]["message"],
"secret secret_missing was not found"
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn rejects_deleting_secret_referenced_by_auth_profile() {
let registry = test_registry().await;
let storage_root = test_storage_root("secret_references");
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let secret = client
.post(format!("{base_url}/secrets"))
.json(&json!({
"name": "crm-api-token",
"kind": SecretKind::Token,
"value": { "token": "super-secret-token" }
}))
.send()
.await
.unwrap();
let secret = assert_success_json(secret).await;
let secret_id = secret["id"].as_str().unwrap();
let auth_profile = client
.post(format!("{base_url}/auth-profiles"))
.json(&json!({
"name": "crm-header",
"kind": "api_key_header",
"config": {
"api_key_header": {
"header_name": "X-Api-Key",
"secret_id": secret_id
}
}
}))
.send()
.await
.unwrap();
let auth_profile = assert_success_json(auth_profile).await;
let auth_profile_id = auth_profile["id"].as_str().unwrap();
let response = client
.delete(format!("{base_url}/secrets/{secret_id}"))
.send()
.await
.unwrap();
let status = response.status();
let body = response.json::<Value>().await.unwrap();
assert_eq!(status, reqwest::StatusCode::CONFLICT);
assert_eq!(body["error"]["code"], "conflict");
assert_eq!(
body["error"]["message"],
format!("secret {secret_id} is referenced by auth profile {auth_profile_id}")
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn roundtrips_graphql_operation_through_yaml_upsert() {
+6
View File
@@ -183,6 +183,12 @@ impl From<RegistryError> for ApiError {
RegistryError::SecretNameAlreadyExists { workspace_id, name } => Self::conflict(
format!("secret with name {name} already exists in workspace {workspace_id}"),
),
RegistryError::SecretReferencedByAuthProfile {
secret_id,
auth_profile_id,
} => Self::conflict(format!(
"secret {secret_id} is referenced by auth profile {auth_profile_id}"
)),
RegistryError::InvalidStreamSessionTransition { .. }
| RegistryError::InvalidAsyncJobTransition { .. } => Self::conflict(value.to_string()),
RegistryError::UserEmailAlreadyExists { email } => {
+32 -5
View File
@@ -25,11 +25,11 @@ use crank_registry::{
CreateVersionRequest, CreateWorkspaceRequest, InvitationRecord, InvocationLogRecord,
ListInvocationLogsQuery, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
OperationSummary, OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord,
PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryOperation, RotateSecretRequest,
SampleKind, SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, StreamSessionFilter, UpdateWorkspaceRequest, UsageAgentBreakdown,
UsageBucket, UsageOperationBreakdown, UsageQuery, UsageSummary, UsageTimelinePoint,
WorkspaceMembershipRecord, WorkspaceRecord,
PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation,
RotateSecretRequest, SampleKind, SaveAgentBindingsRequest, SaveAuthProfileRequest,
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, StreamSessionFilter,
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
};
use crank_runtime::{PreparedRequest, RuntimeError, RuntimeExecutor, RuntimeOperation};
use crank_schema::Schema;
@@ -2085,6 +2085,19 @@ impl AdminService {
secret_id: &SecretId,
) -> Result<(), ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
if let Some(profile) = self
.registry
.list_auth_profiles_referencing_secret(workspace_id, secret_id)
.await?
.into_iter()
.next()
{
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?;
info!(secret_id = %secret_id.as_str(), "secret deleted");
Ok(())
@@ -2774,6 +2787,8 @@ impl AdminService {
) -> 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?;
let now = now_string()?;
let profile = AuthProfile {
@@ -2797,6 +2812,18 @@ impl AdminService {
Ok(profile)
}
async fn validate_auth_profile_secret_ids(
&self,
workspace_id: &WorkspaceId,
config: &AuthConfig,
) -> Result<(), ApiError> {
for secret_id in config.secret_ids() {
self.get_secret(workspace_id, secret_id).await?;
}
Ok(())
}
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version = query.version.unwrap_or_default(), mode = ?query.mode))]
pub async fn export_operation(
&self,