feat: resolve auth profiles through secret ids
This commit is contained in:
@@ -1,45 +1,32 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
ids::{AuthProfileId, WorkspaceId},
|
||||
ids::{AuthProfileId, SecretId, WorkspaceId},
|
||||
protocol::AuthKind,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SecretRef(pub String);
|
||||
|
||||
impl SecretRef {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BearerAuthConfig {
|
||||
pub header_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
pub secret_id: SecretId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BasicAuthConfig {
|
||||
pub username_secret_ref: SecretRef,
|
||||
pub password_secret_ref: SecretRef,
|
||||
pub username_secret_id: SecretId,
|
||||
pub password_secret_id: SecretId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApiKeyHeaderAuthConfig {
|
||||
pub header_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
pub secret_id: SecretId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApiKeyQueryAuthConfig {
|
||||
pub param_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
pub secret_id: SecretId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -51,6 +38,17 @@ pub enum AuthConfig {
|
||||
ApiKeyQuery(ApiKeyQueryAuthConfig),
|
||||
}
|
||||
|
||||
impl AuthConfig {
|
||||
pub fn secret_ids(&self) -> Vec<&SecretId> {
|
||||
match self {
|
||||
Self::Bearer(config) => vec![&config.secret_id],
|
||||
Self::Basic(config) => vec![&config.username_secret_id, &config.password_secret_id],
|
||||
Self::ApiKeyHeader(config) => vec![&config.secret_id],
|
||||
Self::ApiKeyQuery(config) => vec![&config.secret_id],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AuthProfile {
|
||||
pub id: AuthProfileId,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub use access::{
|
||||
pub use agent::{Agent, AgentOperationBinding, AgentStatus, AgentVersion};
|
||||
pub use auth::{
|
||||
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
||||
BearerAuthConfig, SecretRef,
|
||||
BearerAuthConfig,
|
||||
};
|
||||
pub use ids::{
|
||||
AgentId, AsyncJobId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId, OperationId,
|
||||
|
||||
@@ -266,7 +266,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
auth::{AuthConfig, AuthProfile, BearerAuthConfig, SecretRef},
|
||||
auth::{AuthConfig, AuthProfile, BearerAuthConfig},
|
||||
ids::{AuthProfileId, OperationId, SampleId},
|
||||
operation::{
|
||||
ConfigExport, ExecutionConfig, GraphqlTarget, Operation, OperationStatus,
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_profile_serializes_secret_refs_without_secret_values() {
|
||||
fn auth_profile_serializes_secret_ids_without_secret_values() {
|
||||
let profile = AuthProfile {
|
||||
id: AuthProfileId::new("auth_01"),
|
||||
workspace_id: crate::ids::WorkspaceId::new("ws_01"),
|
||||
@@ -438,7 +438,7 @@ mod tests {
|
||||
kind: AuthKind::Bearer,
|
||||
config: AuthConfig::Bearer(BearerAuthConfig {
|
||||
header_name: "Authorization".to_owned(),
|
||||
secret_ref: SecretRef::new("secret://auth/crm-prod-token"),
|
||||
secret_id: crate::ids::SecretId::new("secret_crm_prod_token"),
|
||||
}),
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
@@ -448,8 +448,8 @@ mod tests {
|
||||
|
||||
assert_eq!(value["kind"], "bearer");
|
||||
assert_eq!(
|
||||
value["config"]["bearer"]["secret_ref"],
|
||||
"secret://auth/crm-prod-token"
|
||||
value["config"]["bearer"]["secret_id"],
|
||||
"secret_crm_prod_token"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ pub enum RegistryError {
|
||||
},
|
||||
#[error("secret with name {name} already exists in workspace {workspace_id}")]
|
||||
SecretNameAlreadyExists { workspace_id: String, name: String },
|
||||
#[error("secret {secret_id} is referenced by auth profile {auth_profile_id}")]
|
||||
SecretReferencedByAuthProfile {
|
||||
secret_id: String,
|
||||
auth_profile_id: String,
|
||||
},
|
||||
#[error("invocation log {log_id} was not found")]
|
||||
InvocationLogNotFound { log_id: String },
|
||||
#[error("agent {agent_id} was not found")]
|
||||
|
||||
@@ -3258,6 +3258,25 @@ impl PostgresRegistry {
|
||||
rows.iter().map(map_auth_profile).collect()
|
||||
}
|
||||
|
||||
pub async fn list_auth_profiles_referencing_secret(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
secret_id: &SecretId,
|
||||
) -> Result<Vec<AuthProfile>, RegistryError> {
|
||||
let profiles = self.list_auth_profiles(workspace_id).await?;
|
||||
|
||||
Ok(profiles
|
||||
.into_iter()
|
||||
.filter(|profile| {
|
||||
profile
|
||||
.config
|
||||
.secret_ids()
|
||||
.into_iter()
|
||||
.any(|candidate| candidate == secret_id)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn save_sample_metadata(
|
||||
&self,
|
||||
request: SaveSampleMetadataRequest<'_>,
|
||||
@@ -4331,7 +4350,7 @@ mod tests {
|
||||
use crank_core::{
|
||||
ApiKeyHeaderAuthConfig, AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, ConfigExport,
|
||||
ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, JobStatus,
|
||||
OperationId, OperationStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretRef,
|
||||
OperationId, OperationStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId,
|
||||
StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription, ToolExample,
|
||||
WorkspaceId,
|
||||
};
|
||||
@@ -4471,7 +4490,7 @@ mod tests {
|
||||
kind: AuthKind::ApiKeyHeader,
|
||||
config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig {
|
||||
header_name: "X-Api-Key".to_owned(),
|
||||
secret_ref: SecretRef::new("vault://crank/api-key"),
|
||||
secret_id: SecretId::new("secret_crank_api_key"),
|
||||
}),
|
||||
created_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
@@ -4539,6 +4558,43 @@ mod tests {
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_auth_profiles_referencing_secret() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let primary_secret_id = SecretId::new("secret_primary");
|
||||
let secondary_secret_id = SecretId::new("secret_secondary");
|
||||
let profile = AuthProfile {
|
||||
id: "auth_crank".into(),
|
||||
workspace_id: test_workspace_id(),
|
||||
name: "Crank basic auth".to_owned(),
|
||||
kind: AuthKind::Basic,
|
||||
config: AuthConfig::Basic(crank_core::BasicAuthConfig {
|
||||
username_secret_id: primary_secret_id.clone(),
|
||||
password_secret_id: secondary_secret_id.clone(),
|
||||
}),
|
||||
created_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
};
|
||||
|
||||
registry
|
||||
.save_auth_profile(SaveAuthProfileRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
profile: &profile,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let profiles = registry
|
||||
.list_auth_profiles_referencing_secret(&test_workspace_id(), &primary_secret_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profiles, vec![profile]);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_and_finishes_yaml_import_jobs() {
|
||||
let database = TestDatabase::new().await;
|
||||
|
||||
Reference in New Issue
Block a user