feat: add platform access foundation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, OperationId,
|
||||
OperationStatus, Workspace, WorkspaceId,
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, InvitationId,
|
||||
InvitationToken, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId,
|
||||
PlatformApiKeyStatus, User, UserId, Workspace, WorkspaceId,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
@@ -14,13 +15,14 @@ use crate::{
|
||||
error::RegistryError,
|
||||
migrations,
|
||||
model::{
|
||||
AgentSummary, AgentVersionRecord, CreateAgentRequest, CreateVersionRequest,
|
||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorMetadata,
|
||||
OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishAgentRequest,
|
||||
PublishRequest, PublishedAgentTool, RegistryOperation, SaveAgentBindingsRequest,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJob, YamlImportJobCompletion,
|
||||
YamlImportJobId, YamlImportJobStatus,
|
||||
AgentSummary, AgentVersionRecord, CreateAgentRequest, CreateInvitationRequest,
|
||||
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||
CreateYamlImportJobRequest, DescriptorMetadata, InvitationRecord, MembershipRecord,
|
||||
OperationSampleMetadata, OperationSummary, OperationVersionRecord, PlatformApiKeyRecord,
|
||||
PublishAgentRequest, PublishRequest, PublishedAgentTool, RegistryOperation,
|
||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJob,
|
||||
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,6 +64,231 @@ impl PostgresRegistry {
|
||||
rows.iter().map(map_workspace_record).collect()
|
||||
}
|
||||
|
||||
pub async fn list_memberships(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<MembershipRecord>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
m.workspace_id,
|
||||
m.user_id,
|
||||
m.role,
|
||||
to_char(m.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
u.email,
|
||||
u.display_name,
|
||||
u.status,
|
||||
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as user_created_at
|
||||
from memberships m
|
||||
join users u on u.id = m.user_id
|
||||
where m.workspace_id = $1
|
||||
order by u.email asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_membership_record).collect()
|
||||
}
|
||||
|
||||
pub async fn list_invitations(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<InvitationRecord>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
email,
|
||||
role,
|
||||
status,
|
||||
token_hash,
|
||||
to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
|
||||
from invitation_tokens
|
||||
where workspace_id = $1
|
||||
order by created_at desc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_invitation_record).collect()
|
||||
}
|
||||
|
||||
pub async fn create_invitation(
|
||||
&self,
|
||||
request: CreateInvitationRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
sqlx::query(
|
||||
"insert into invitation_tokens (
|
||||
id,
|
||||
workspace_id,
|
||||
email,
|
||||
role,
|
||||
status,
|
||||
token_hash,
|
||||
expires_at,
|
||||
created_at
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6, $7::timestamptz, $8::timestamptz
|
||||
)",
|
||||
)
|
||||
.bind(request.invitation.id.as_str())
|
||||
.bind(request.invitation.workspace_id.as_str())
|
||||
.bind(&request.invitation.email)
|
||||
.bind(serialize_enum_text(&request.invitation.role, "role")?)
|
||||
.bind(serialize_enum_text(&request.invitation.status, "status")?)
|
||||
.bind(&request.invitation.token_hash)
|
||||
.bind(&request.invitation.expires_at)
|
||||
.bind(&request.invitation.created_at)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_invitation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
invitation_id: &InvitationId,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result =
|
||||
sqlx::query("delete from invitation_tokens where workspace_id = $1 and id = $2")
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(invitation_id.as_str())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(RegistryError::InvitationNotFound {
|
||||
invitation_id: invitation_id.as_str().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_platform_api_keys(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<PlatformApiKeyRecord>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
prefix,
|
||||
scopes_json,
|
||||
status,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
|
||||
from platform_api_keys
|
||||
where workspace_id = $1
|
||||
order by created_at desc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_platform_api_key_record).collect()
|
||||
}
|
||||
|
||||
pub async fn create_platform_api_key(
|
||||
&self,
|
||||
request: CreatePlatformApiKeyRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result = sqlx::query(
|
||||
"insert into platform_api_keys (
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
prefix,
|
||||
secret_hash,
|
||||
scopes_json,
|
||||
status,
|
||||
created_at,
|
||||
last_used_at,
|
||||
revoked_at
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8::timestamptz, $9::timestamptz, $10::timestamptz
|
||||
)",
|
||||
)
|
||||
.bind(request.api_key.id.as_str())
|
||||
.bind(request.api_key.workspace_id.as_str())
|
||||
.bind(&request.api_key.name)
|
||||
.bind(&request.api_key.prefix)
|
||||
.bind(request.secret_hash)
|
||||
.bind(Json(serialize_json_value(&request.api_key.scopes)?))
|
||||
.bind(serialize_enum_text(&request.api_key.status, "status")?)
|
||||
.bind(&request.api_key.created_at)
|
||||
.bind(request.api_key.last_used_at.as_deref())
|
||||
.bind(Option::<&str>::None)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(sqlx::Error::Database(error))
|
||||
if error.constraint() == Some("platform_api_keys_workspace_name_idx") =>
|
||||
{
|
||||
Err(RegistryError::Storage(sqlx::Error::Database(error)))
|
||||
}
|
||||
Err(error) => Err(RegistryError::Storage(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn revoke_platform_api_key(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
key_id: &PlatformApiKeyId,
|
||||
revoked_at: &str,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result = sqlx::query(
|
||||
"update platform_api_keys
|
||||
set status = $1,
|
||||
revoked_at = $2::timestamptz
|
||||
where workspace_id = $3 and id = $4",
|
||||
)
|
||||
.bind(serialize_enum_text(
|
||||
&PlatformApiKeyStatus::Revoked,
|
||||
"status",
|
||||
)?)
|
||||
.bind(revoked_at)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(key_id.as_str())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(RegistryError::PlatformApiKeyNotFound {
|
||||
key_id: key_id.as_str().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_platform_api_key(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
key_id: &PlatformApiKeyId,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result =
|
||||
sqlx::query("delete from platform_api_keys where workspace_id = $1 and id = $2")
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(key_id.as_str())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(RegistryError::PlatformApiKeyNotFound {
|
||||
key_id: key_id.as_str().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -1417,6 +1644,51 @@ fn map_workspace_record(row: &PgRow) -> Result<WorkspaceRecord, RegistryError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn map_membership_record(row: &PgRow) -> Result<MembershipRecord, RegistryError> {
|
||||
Ok(MembershipRecord {
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
user: User {
|
||||
id: UserId::new(row.try_get::<String, _>("user_id")?),
|
||||
email: row.try_get("email")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
created_at: row.try_get("user_created_at")?,
|
||||
},
|
||||
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_invitation_record(row: &PgRow) -> Result<InvitationRecord, RegistryError> {
|
||||
Ok(InvitationRecord {
|
||||
invitation: InvitationToken {
|
||||
id: InvitationId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
email: row.try_get("email")?,
|
||||
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
token_hash: row.try_get("token_hash")?,
|
||||
expires_at: row.try_get("expires_at")?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_platform_api_key_record(row: &PgRow) -> Result<PlatformApiKeyRecord, RegistryError> {
|
||||
Ok(PlatformApiKeyRecord {
|
||||
api_key: PlatformApiKey {
|
||||
id: PlatformApiKeyId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
name: row.try_get("name")?,
|
||||
prefix: row.try_get("prefix")?,
|
||||
scopes: deserialize_json_value(row.try_get::<Json<Value>, _>("scopes_json")?.0)?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
last_used_at: row.try_get("last_used_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_agent_summary(row: &PgRow) -> Result<AgentSummary, RegistryError> {
|
||||
Ok(AgentSummary {
|
||||
id: AgentId::new(row.try_get::<String, _>("id")?),
|
||||
|
||||
Reference in New Issue
Block a user