first pass at access control setup
This commit is contained in:
@@ -887,7 +887,7 @@ pub mod trigger {
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub entrypoint: String,
|
||||
pub runtime: Id,
|
||||
pub runtime_ref: String,
|
||||
@@ -915,7 +915,7 @@ pub mod action {
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub entrypoint: String,
|
||||
pub runtime: Option<Id>,
|
||||
/// Optional semver version constraint for the runtime
|
||||
@@ -965,7 +965,7 @@ pub mod rule {
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub action: Option<Id>,
|
||||
pub action_ref: String,
|
||||
pub trigger: Option<Id>,
|
||||
@@ -1221,6 +1221,7 @@ pub mod identity {
|
||||
pub display_name: Option<String>,
|
||||
pub password_hash: Option<String>,
|
||||
pub attributes: JsonDict,
|
||||
pub frozen: bool,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
@@ -1245,6 +1246,25 @@ pub mod identity {
|
||||
pub permset: Id,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct IdentityRoleAssignment {
|
||||
pub id: Id,
|
||||
pub identity: Id,
|
||||
pub role: String,
|
||||
pub source: String,
|
||||
pub managed: bool,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct PermissionSetRoleAssignment {
|
||||
pub id: Id,
|
||||
pub permset: Id,
|
||||
pub role: String,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Key/Value storage
|
||||
|
||||
@@ -725,8 +725,7 @@ impl<'a> PackComponentLoader<'a> {
|
||||
let description = data
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let enabled = data
|
||||
.get("enabled")
|
||||
@@ -745,7 +744,10 @@ impl<'a> PackComponentLoader<'a> {
|
||||
if let Some(existing) = TriggerRepository::find_by_ref(self.pool, &trigger_ref).await? {
|
||||
let update_input = UpdateTriggerInput {
|
||||
label: Some(label),
|
||||
description: Some(Patch::Set(description)),
|
||||
description: Some(match description {
|
||||
Some(description) => Patch::Set(description),
|
||||
None => Patch::Clear,
|
||||
}),
|
||||
enabled: Some(enabled),
|
||||
param_schema: Some(match param_schema {
|
||||
Some(value) => Patch::Set(value),
|
||||
@@ -778,7 +780,7 @@ impl<'a> PackComponentLoader<'a> {
|
||||
pack: Some(self.pack_id),
|
||||
pack_ref: Some(self.pack_ref.clone()),
|
||||
label,
|
||||
description: Some(description),
|
||||
description,
|
||||
enabled,
|
||||
param_schema,
|
||||
out_schema,
|
||||
@@ -858,8 +860,7 @@ impl<'a> PackComponentLoader<'a> {
|
||||
let description = data
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// ── Workflow file handling ──────────────────────────────────
|
||||
// If the action declares `workflow_file`, load the referenced
|
||||
@@ -876,7 +877,7 @@ impl<'a> PackComponentLoader<'a> {
|
||||
wf_path,
|
||||
&action_ref,
|
||||
&label,
|
||||
&description,
|
||||
description.as_deref().unwrap_or(""),
|
||||
&data,
|
||||
)
|
||||
.await
|
||||
@@ -956,7 +957,10 @@ impl<'a> PackComponentLoader<'a> {
|
||||
if let Some(existing) = ActionRepository::find_by_ref(self.pool, &action_ref).await? {
|
||||
let update_input = UpdateActionInput {
|
||||
label: Some(label),
|
||||
description: Some(description),
|
||||
description: Some(match description {
|
||||
Some(description) => Patch::Set(description),
|
||||
None => Patch::Clear,
|
||||
}),
|
||||
entrypoint: Some(entrypoint),
|
||||
runtime: runtime_id,
|
||||
runtime_version_constraint: Some(match runtime_version_constraint {
|
||||
@@ -1310,8 +1314,7 @@ impl<'a> PackComponentLoader<'a> {
|
||||
let description = data
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let enabled = data
|
||||
.get("enabled")
|
||||
@@ -1347,7 +1350,10 @@ impl<'a> PackComponentLoader<'a> {
|
||||
if let Some(existing) = SensorRepository::find_by_ref(self.pool, &sensor_ref).await? {
|
||||
let update_input = UpdateSensorInput {
|
||||
label: Some(label),
|
||||
description: Some(description),
|
||||
description: Some(match description {
|
||||
Some(description) => Patch::Set(description),
|
||||
None => Patch::Clear,
|
||||
}),
|
||||
entrypoint: Some(entrypoint),
|
||||
runtime: Some(sensor_runtime_id),
|
||||
runtime_ref: Some(sensor_runtime_ref.clone()),
|
||||
|
||||
@@ -21,10 +21,6 @@ pub enum Resource {
|
||||
Inquiries,
|
||||
Keys,
|
||||
Artifacts,
|
||||
Workflows,
|
||||
Webhooks,
|
||||
Analytics,
|
||||
History,
|
||||
Identities,
|
||||
Permissions,
|
||||
}
|
||||
@@ -40,6 +36,7 @@ pub enum Action {
|
||||
Cancel,
|
||||
Respond,
|
||||
Manage,
|
||||
Decrypt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -69,6 +66,8 @@ pub struct GrantConstraints {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub owner_types: Option<Vec<OwnerType>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub owner_refs: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub visibility: Option<Vec<ArtifactVisibility>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub execution_scope: Option<ExecutionScopeConstraint>,
|
||||
@@ -99,6 +98,7 @@ pub struct AuthorizationContext {
|
||||
pub pack_ref: Option<String>,
|
||||
pub owner_identity_id: Option<Id>,
|
||||
pub owner_type: Option<OwnerType>,
|
||||
pub owner_ref: Option<String>,
|
||||
pub visibility: Option<ArtifactVisibility>,
|
||||
pub encrypted: Option<bool>,
|
||||
pub execution_owner_identity_id: Option<Id>,
|
||||
@@ -115,6 +115,7 @@ impl AuthorizationContext {
|
||||
pack_ref: None,
|
||||
owner_identity_id: None,
|
||||
owner_type: None,
|
||||
owner_ref: None,
|
||||
visibility: None,
|
||||
encrypted: None,
|
||||
execution_owner_identity_id: None,
|
||||
@@ -162,6 +163,15 @@ impl Grant {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(owner_refs) = &constraints.owner_refs {
|
||||
let Some(owner_ref) = &ctx.owner_ref else {
|
||||
return false;
|
||||
};
|
||||
if !owner_refs.contains(owner_ref) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(visibility) = &constraints.visibility {
|
||||
let Some(target_visibility) = ctx.visibility else {
|
||||
return false;
|
||||
@@ -289,4 +299,28 @@ mod tests {
|
||||
.insert("team".to_string(), json!("infra"));
|
||||
assert!(!grant.allows(Resource::Packs, Action::Read, &ctx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_ref_constraint_requires_exact_value_match() {
|
||||
let grant = Grant {
|
||||
resource: Resource::Artifacts,
|
||||
actions: vec![Action::Read],
|
||||
constraints: Some(GrantConstraints {
|
||||
owner_types: Some(vec![OwnerType::Pack]),
|
||||
owner_refs: Some(vec!["python_example".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
let mut ctx = AuthorizationContext::new(1);
|
||||
ctx.owner_type = Some(OwnerType::Pack);
|
||||
ctx.owner_ref = Some("python_example".to_string());
|
||||
assert!(grant.allows(Resource::Artifacts, Action::Read, &ctx));
|
||||
|
||||
ctx.owner_ref = Some("other_pack".to_string());
|
||||
assert!(!grant.allows(Resource::Artifacts, Action::Read, &ctx));
|
||||
|
||||
ctx.owner_ref = None;
|
||||
assert!(!grant.allows(Resource::Artifacts, Action::Read, &ctx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct CreateActionInput {
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub entrypoint: String,
|
||||
pub runtime: Option<Id>,
|
||||
pub runtime_version_constraint: Option<String>,
|
||||
@@ -64,7 +64,7 @@ pub struct CreateActionInput {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateActionInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub description: Option<Patch<String>>,
|
||||
pub entrypoint: Option<String>,
|
||||
pub runtime: Option<Id>,
|
||||
pub runtime_version_constraint: Option<Patch<String>>,
|
||||
@@ -210,7 +210,10 @@ impl Update for ActionRepository {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
match description {
|
||||
Patch::Set(value) => query.push_bind(value),
|
||||
Patch::Clear => query.push_bind(Option::<String>::None),
|
||||
};
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -577,6 +577,14 @@ pub struct CreateArtifactVersionInput {
|
||||
}
|
||||
|
||||
impl ArtifactVersionRepository {
|
||||
fn select_columns_with_alias(alias: &str) -> String {
|
||||
format!(
|
||||
"{alias}.id, {alias}.artifact, {alias}.version, {alias}.content_type, \
|
||||
{alias}.size_bytes, NULL::bytea AS content, {alias}.content_json, \
|
||||
{alias}.file_path, {alias}.meta, {alias}.created_by, {alias}.created"
|
||||
)
|
||||
}
|
||||
|
||||
/// Find a version by ID (without binary content for performance)
|
||||
pub async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<ArtifactVersion>>
|
||||
where
|
||||
@@ -812,14 +820,11 @@ impl ArtifactVersionRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let query = format!(
|
||||
"SELECT av.{} \
|
||||
"SELECT {} \
|
||||
FROM artifact_version av \
|
||||
JOIN artifact a ON av.artifact = a.id \
|
||||
WHERE a.execution = $1 AND av.file_path IS NOT NULL",
|
||||
artifact_version::SELECT_COLUMNS
|
||||
.split(", ")
|
||||
.collect::<Vec<_>>()
|
||||
.join(", av.")
|
||||
Self::select_columns_with_alias("av")
|
||||
);
|
||||
sqlx::query_as::<_, ArtifactVersion>(&query)
|
||||
.bind(execution_id)
|
||||
@@ -847,3 +852,18 @@ impl ArtifactVersionRepository {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ArtifactVersionRepository;
|
||||
|
||||
#[test]
|
||||
fn aliased_select_columns_keep_null_content_expression_unqualified() {
|
||||
let columns = ArtifactVersionRepository::select_columns_with_alias("av");
|
||||
|
||||
assert!(columns.contains("av.id"));
|
||||
assert!(columns.contains("av.file_path"));
|
||||
assert!(columns.contains("NULL::bytea AS content"));
|
||||
assert!(!columns.contains("av.NULL::bytea AS content"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct UpdateIdentityInput {
|
||||
pub display_name: Option<String>,
|
||||
pub password_hash: Option<String>,
|
||||
pub attributes: Option<JsonDict>,
|
||||
pub frozen: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -37,7 +38,7 @@ impl FindById for IdentityRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity WHERE id = $1"
|
||||
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated FROM identity WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +50,7 @@ impl List for IdentityRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity ORDER BY login ASC"
|
||||
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated FROM identity ORDER BY login ASC"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,7 @@ impl Create for IdentityRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"INSERT INTO identity (login, display_name, password_hash, attributes) VALUES ($1, $2, $3, $4) RETURNING id, login, display_name, password_hash, attributes, created, updated"
|
||||
"INSERT INTO identity (login, display_name, password_hash, attributes) VALUES ($1, $2, $3, $4) RETURNING id, login, display_name, password_hash, attributes, frozen, created, updated"
|
||||
)
|
||||
.bind(&input.login)
|
||||
.bind(&input.display_name)
|
||||
@@ -111,6 +112,13 @@ impl Update for IdentityRepository {
|
||||
query.push("attributes = ").push_bind(attributes);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(frozen) = input.frozen {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("frozen = ").push_bind(frozen);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !has_updates {
|
||||
// No updates requested, fetch and return existing entity
|
||||
@@ -119,7 +127,7 @@ impl Update for IdentityRepository {
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
||||
query.push(
|
||||
" RETURNING id, login, display_name, password_hash, attributes, created, updated",
|
||||
" RETURNING id, login, display_name, password_hash, attributes, frozen, created, updated",
|
||||
);
|
||||
|
||||
query
|
||||
@@ -156,7 +164,7 @@ impl IdentityRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity WHERE login = $1"
|
||||
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated FROM identity WHERE login = $1"
|
||||
).bind(login).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -169,7 +177,7 @@ impl IdentityRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated
|
||||
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated
|
||||
FROM identity
|
||||
WHERE attributes->'oidc'->>'issuer' = $1
|
||||
AND attributes->'oidc'->>'sub' = $2",
|
||||
@@ -190,7 +198,7 @@ impl IdentityRepository {
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated
|
||||
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated
|
||||
FROM identity
|
||||
WHERE attributes->'ldap'->>'server_url' = $1
|
||||
AND attributes->'ldap'->>'dn' = $2",
|
||||
@@ -363,6 +371,27 @@ impl PermissionSetRepository {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn find_by_roles<'e, E>(executor: E, roles: &[String]) -> Result<Vec<PermissionSet>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
if roles.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
sqlx::query_as::<_, PermissionSet>(
|
||||
"SELECT DISTINCT ps.id, ps.ref, ps.pack, ps.pack_ref, ps.label, ps.description, ps.grants, ps.created, ps.updated
|
||||
FROM permission_set ps
|
||||
INNER JOIN permission_set_role_assignment psra ON psra.permset = ps.id
|
||||
WHERE psra.role = ANY($1)
|
||||
ORDER BY ps.ref ASC",
|
||||
)
|
||||
.bind(roles)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Delete permission sets belonging to a pack whose refs are NOT in the given set.
|
||||
///
|
||||
/// Used during pack reinstallation to clean up permission sets that were
|
||||
@@ -481,3 +510,231 @@ impl PermissionAssignmentRepository {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdentityRoleAssignmentRepository;
|
||||
|
||||
impl Repository for IdentityRoleAssignmentRepository {
|
||||
type Entity = IdentityRoleAssignment;
|
||||
fn table_name() -> &'static str {
|
||||
"identity_role_assignment"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateIdentityRoleAssignmentInput {
|
||||
pub identity: Id,
|
||||
pub role: String,
|
||||
pub source: String,
|
||||
pub managed: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for IdentityRoleAssignmentRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, IdentityRoleAssignment>(
|
||||
"SELECT id, identity, role, source, managed, created, updated FROM identity_role_assignment WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for IdentityRoleAssignmentRepository {
|
||||
type CreateInput = CreateIdentityRoleAssignmentInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, IdentityRoleAssignment>(
|
||||
"INSERT INTO identity_role_assignment (identity, role, source, managed)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, identity, role, source, managed, created, updated",
|
||||
)
|
||||
.bind(input.identity)
|
||||
.bind(&input.role)
|
||||
.bind(&input.source)
|
||||
.bind(input.managed)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for IdentityRoleAssignmentRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM identity_role_assignment WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityRoleAssignmentRepository {
|
||||
pub async fn find_by_identity<'e, E>(
|
||||
executor: E,
|
||||
identity_id: Id,
|
||||
) -> Result<Vec<IdentityRoleAssignment>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, IdentityRoleAssignment>(
|
||||
"SELECT id, identity, role, source, managed, created, updated
|
||||
FROM identity_role_assignment
|
||||
WHERE identity = $1
|
||||
ORDER BY role ASC",
|
||||
)
|
||||
.bind(identity_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn find_role_names_by_identity<'e, E>(
|
||||
executor: E,
|
||||
identity_id: Id,
|
||||
) -> Result<Vec<String>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_scalar::<_, String>(
|
||||
"SELECT role FROM identity_role_assignment WHERE identity = $1 ORDER BY role ASC",
|
||||
)
|
||||
.bind(identity_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn replace_managed_roles<'e, E>(
|
||||
executor: E,
|
||||
identity_id: Id,
|
||||
source: &str,
|
||||
roles: &[String],
|
||||
) -> Result<()>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
||||
{
|
||||
sqlx::query(
|
||||
"DELETE FROM identity_role_assignment WHERE identity = $1 AND source = $2 AND managed = true",
|
||||
)
|
||||
.bind(identity_id)
|
||||
.bind(source)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
for role in roles {
|
||||
sqlx::query(
|
||||
"INSERT INTO identity_role_assignment (identity, role, source, managed)
|
||||
VALUES ($1, $2, $3, true)
|
||||
ON CONFLICT (identity, role) DO UPDATE
|
||||
SET source = EXCLUDED.source,
|
||||
managed = EXCLUDED.managed,
|
||||
updated = NOW()",
|
||||
)
|
||||
.bind(identity_id)
|
||||
.bind(role)
|
||||
.bind(source)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PermissionSetRoleAssignmentRepository;
|
||||
|
||||
impl Repository for PermissionSetRoleAssignmentRepository {
|
||||
type Entity = PermissionSetRoleAssignment;
|
||||
fn table_name() -> &'static str {
|
||||
"permission_set_role_assignment"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreatePermissionSetRoleAssignmentInput {
|
||||
pub permset: Id,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for PermissionSetRoleAssignmentRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSetRoleAssignment>(
|
||||
"SELECT id, permset, role, created FROM permission_set_role_assignment WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for PermissionSetRoleAssignmentRepository {
|
||||
type CreateInput = CreatePermissionSetRoleAssignmentInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSetRoleAssignment>(
|
||||
"INSERT INTO permission_set_role_assignment (permset, role)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, permset, role, created",
|
||||
)
|
||||
.bind(input.permset)
|
||||
.bind(&input.role)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for PermissionSetRoleAssignmentRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM permission_set_role_assignment WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionSetRoleAssignmentRepository {
|
||||
pub async fn find_by_permission_set<'e, E>(
|
||||
executor: E,
|
||||
permset_id: Id,
|
||||
) -> Result<Vec<PermissionSetRoleAssignment>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSetRoleAssignment>(
|
||||
"SELECT id, permset, role, created
|
||||
FROM permission_set_role_assignment
|
||||
WHERE permset = $1
|
||||
ORDER BY role ASC",
|
||||
)
|
||||
.bind(permset_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::models::{rule::*, Id};
|
||||
use crate::{Error, Result};
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Patch, Repository, Update};
|
||||
|
||||
/// Filters for [`RuleRepository::list_search`].
|
||||
///
|
||||
@@ -41,7 +41,7 @@ pub struct RestoreRuleInput {
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub action: Option<Id>,
|
||||
pub action_ref: String,
|
||||
pub trigger: Option<Id>,
|
||||
@@ -70,7 +70,7 @@ pub struct CreateRuleInput {
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub action: Id,
|
||||
pub action_ref: String,
|
||||
pub trigger: Id,
|
||||
@@ -86,7 +86,7 @@ pub struct CreateRuleInput {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateRuleInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub description: Option<Patch<String>>,
|
||||
pub conditions: Option<serde_json::Value>,
|
||||
pub action_params: Option<serde_json::Value>,
|
||||
pub trigger_params: Option<serde_json::Value>,
|
||||
@@ -228,7 +228,10 @@ impl Update for RuleRepository {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
match description {
|
||||
Patch::Set(value) => query.push_bind(value),
|
||||
Patch::Clear => query.push_bind(Option::<String>::None),
|
||||
};
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -665,7 +665,7 @@ pub struct CreateSensorInput {
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub description: Option<String>,
|
||||
pub entrypoint: String,
|
||||
pub runtime: Id,
|
||||
pub runtime_ref: String,
|
||||
@@ -681,7 +681,7 @@ pub struct CreateSensorInput {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateSensorInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub description: Option<Patch<String>>,
|
||||
pub entrypoint: Option<String>,
|
||||
pub runtime: Option<Id>,
|
||||
pub runtime_ref: Option<String>,
|
||||
@@ -830,7 +830,10 @@ impl Update for SensorRepository {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
match description {
|
||||
Patch::Set(value) => query.push_bind(value),
|
||||
Patch::Clear => query.push_bind(Option::<String>::None),
|
||||
};
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::repositories::action::{ActionRepository, CreateActionInput, UpdateActionInput};
|
||||
use crate::repositories::workflow::{CreateWorkflowDefinitionInput, UpdateWorkflowDefinitionInput};
|
||||
use crate::repositories::Patch;
|
||||
use crate::repositories::{
|
||||
Create, Delete, FindByRef, PackRepository, Update, WorkflowDefinitionRepository,
|
||||
};
|
||||
@@ -270,7 +271,7 @@ impl WorkflowRegistrar {
|
||||
pack: pack_id,
|
||||
pack_ref: pack_ref.to_string(),
|
||||
label: effective_label.to_string(),
|
||||
description: workflow.description.clone().unwrap_or_default(),
|
||||
description: workflow.description.clone(),
|
||||
entrypoint,
|
||||
runtime: None,
|
||||
runtime_version_constraint: None,
|
||||
@@ -317,7 +318,10 @@ impl WorkflowRegistrar {
|
||||
// Update the existing companion action to stay in sync
|
||||
let update_input = UpdateActionInput {
|
||||
label: Some(effective_label.to_string()),
|
||||
description: workflow.description.clone(),
|
||||
description: Some(match workflow.description.clone() {
|
||||
Some(description) => Patch::Set(description),
|
||||
None => Patch::Clear,
|
||||
}),
|
||||
entrypoint: Some(format!("workflows/{}.workflow.yaml", workflow_name)),
|
||||
runtime: None,
|
||||
runtime_version_constraint: None,
|
||||
|
||||
@@ -66,7 +66,10 @@ async fn test_create_action_with_optional_fields() {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(action.label, "Full Test Action");
|
||||
assert_eq!(action.description, "Action with all optional fields");
|
||||
assert_eq!(
|
||||
action.description,
|
||||
Some("Action with all optional fields".to_string())
|
||||
);
|
||||
assert_eq!(action.entrypoint, "custom.py");
|
||||
assert!(action.param_schema.is_some());
|
||||
assert!(action.out_schema.is_some());
|
||||
@@ -204,7 +207,9 @@ async fn test_update_action() {
|
||||
|
||||
let update = UpdateActionInput {
|
||||
label: Some("Updated Label".to_string()),
|
||||
description: Some("Updated description".to_string()),
|
||||
description: Some(attune_common::repositories::Patch::Set(
|
||||
"Updated description".to_string(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -214,7 +219,7 @@ async fn test_update_action() {
|
||||
|
||||
assert_eq!(updated.id, action.id);
|
||||
assert_eq!(updated.label, "Updated Label");
|
||||
assert_eq!(updated.description, "Updated description");
|
||||
assert_eq!(updated.description, Some("Updated description".to_string()));
|
||||
assert_eq!(updated.entrypoint, action.entrypoint); // Unchanged
|
||||
assert!(updated.updated > original_updated);
|
||||
}
|
||||
@@ -338,7 +343,7 @@ async fn test_action_foreign_key_constraint() {
|
||||
pack: 99999,
|
||||
pack_ref: "nonexistent.pack".to_string(),
|
||||
label: "Test Action".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
entrypoint: "main.py".to_string(),
|
||||
runtime: None,
|
||||
runtime_version_constraint: None,
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn test_create_enforcement_minimal() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -121,7 +121,7 @@ async fn test_create_enforcement_with_event() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -189,7 +189,7 @@ async fn test_create_enforcement_with_conditions() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -255,7 +255,7 @@ async fn test_create_enforcement_with_any_condition() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -397,7 +397,7 @@ async fn test_find_enforcement_by_id() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -471,7 +471,7 @@ async fn test_get_enforcement_by_id() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -552,7 +552,7 @@ async fn test_list_enforcements() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -624,7 +624,7 @@ async fn test_update_enforcement_status() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -690,7 +690,7 @@ async fn test_update_enforcement_status_transitions() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -769,7 +769,7 @@ async fn test_update_enforcement_payload() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -832,7 +832,7 @@ async fn test_update_enforcement_both_fields() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -896,7 +896,7 @@ async fn test_update_enforcement_no_changes() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -981,7 +981,7 @@ async fn test_delete_enforcement() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1056,7 +1056,7 @@ async fn test_find_enforcements_by_rule() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Rule 1".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1078,7 +1078,7 @@ async fn test_find_enforcements_by_rule() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Rule 2".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1149,7 +1149,7 @@ async fn test_find_enforcements_by_status() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1239,7 +1239,7 @@ async fn test_find_enforcements_by_event() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1324,7 +1324,7 @@ async fn test_delete_rule_sets_enforcement_rule_to_null() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1390,7 +1390,7 @@ async fn test_enforcement_resolved_at_lifecycle() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
|
||||
@@ -449,7 +449,7 @@ async fn test_delete_event_enforcement_retains_event_id() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
|
||||
@@ -454,7 +454,7 @@ impl ActionFixture {
|
||||
pack_ref: self.pack_ref,
|
||||
r#ref: self.r#ref,
|
||||
label: self.label,
|
||||
description: self.description,
|
||||
description: Some(self.description),
|
||||
entrypoint: self.entrypoint,
|
||||
runtime: self.runtime,
|
||||
runtime_version_constraint: None,
|
||||
@@ -1088,7 +1088,7 @@ impl SensorFixture {
|
||||
pack: self.pack_id,
|
||||
pack_ref: self.pack_ref,
|
||||
label: self.label,
|
||||
description: self.description,
|
||||
description: Some(self.description),
|
||||
entrypoint: self.entrypoint,
|
||||
runtime: self.runtime_id,
|
||||
runtime_ref: self.runtime_ref,
|
||||
|
||||
@@ -219,6 +219,7 @@ async fn test_update_identity() {
|
||||
display_name: Some("Updated Name".to_string()),
|
||||
password_hash: None,
|
||||
attributes: Some(json!({"key": "updated", "new_key": "new_value"})),
|
||||
frozen: None,
|
||||
};
|
||||
|
||||
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
||||
@@ -252,6 +253,7 @@ async fn test_update_identity_partial() {
|
||||
display_name: Some("Only Display Name Changed".to_string()),
|
||||
password_hash: None,
|
||||
attributes: None,
|
||||
frozen: None,
|
||||
};
|
||||
|
||||
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
||||
@@ -274,6 +276,7 @@ async fn test_update_identity_not_found() {
|
||||
display_name: Some("Updated Name".to_string()),
|
||||
password_hash: None,
|
||||
attributes: None,
|
||||
frozen: None,
|
||||
};
|
||||
|
||||
let result = IdentityRepository::update(&pool, 999999, update_input).await;
|
||||
@@ -380,6 +383,7 @@ async fn test_identity_updated_changes_on_update() {
|
||||
display_name: Some("Updated".to_string()),
|
||||
password_hash: None,
|
||||
attributes: None,
|
||||
frozen: None,
|
||||
};
|
||||
|
||||
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
||||
|
||||
@@ -8,7 +8,7 @@ mod helpers;
|
||||
use attune_common::{
|
||||
repositories::{
|
||||
rule::{CreateRuleInput, RuleRepository, UpdateRuleInput},
|
||||
Create, Delete, FindById, FindByRef, List, Update,
|
||||
Create, Delete, FindById, FindByRef, List, Patch, Update,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
@@ -48,7 +48,7 @@ async fn test_create_rule() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "A test rule".to_string(),
|
||||
description: Some("A test rule".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -66,7 +66,7 @@ async fn test_create_rule() {
|
||||
assert_eq!(rule.pack, pack.id);
|
||||
assert_eq!(rule.pack_ref, pack.r#ref);
|
||||
assert_eq!(rule.label, "Test Rule");
|
||||
assert_eq!(rule.description, "A test rule");
|
||||
assert_eq!(rule.description, Some("A test rule".to_string()));
|
||||
assert_eq!(rule.action, Some(action.id));
|
||||
assert_eq!(rule.action_ref, action.r#ref);
|
||||
assert_eq!(rule.trigger, Some(trigger.id));
|
||||
@@ -105,7 +105,7 @@ async fn test_create_rule_disabled() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Disabled Rule".to_string(),
|
||||
description: "A disabled rule".to_string(),
|
||||
description: Some("A disabled rule".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -155,7 +155,7 @@ async fn test_create_rule_with_complex_conditions() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Complex Rule".to_string(),
|
||||
description: "Rule with complex conditions".to_string(),
|
||||
description: Some("Rule with complex conditions".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -200,7 +200,7 @@ async fn test_create_rule_duplicate_ref() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "First Rule".to_string(),
|
||||
description: "First".to_string(),
|
||||
description: Some("First".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -220,7 +220,7 @@ async fn test_create_rule_duplicate_ref() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Second Rule".to_string(),
|
||||
description: "Second".to_string(),
|
||||
description: Some("Second".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -274,7 +274,7 @@ async fn test_create_rule_invalid_ref_format_uppercase() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Upper Rule".to_string(),
|
||||
description: "Invalid uppercase ref".to_string(),
|
||||
description: Some("Invalid uppercase ref".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -316,7 +316,7 @@ async fn test_create_rule_invalid_ref_format_no_dot() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "No Dot Rule".to_string(),
|
||||
description: "Invalid ref without dot".to_string(),
|
||||
description: Some("Invalid ref without dot".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -362,7 +362,7 @@ async fn test_find_rule_by_id() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Find Rule".to_string(),
|
||||
description: "Rule to find".to_string(),
|
||||
description: Some("Rule to find".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -422,7 +422,7 @@ async fn test_find_rule_by_ref() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Find By Ref Rule".to_string(),
|
||||
description: "Find by ref".to_string(),
|
||||
description: Some("Find by ref".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -484,7 +484,7 @@ async fn test_list_rules() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("List Rule {}", i),
|
||||
description: format!("Rule {}", i),
|
||||
description: Some(format!("Rule {}", i)),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -538,7 +538,7 @@ async fn test_list_rules_ordered_by_ref() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: name.to_string(),
|
||||
description: name.to_string(),
|
||||
description: Some(name.to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -594,7 +594,7 @@ async fn test_update_rule_label() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Original Label".to_string(),
|
||||
description: "Original".to_string(),
|
||||
description: Some("Original".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -618,7 +618,7 @@ async fn test_update_rule_label() {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.label, "Updated Label");
|
||||
assert_eq!(updated.description, "Original"); // unchanged
|
||||
assert_eq!(updated.description, Some("Original".to_string())); // unchanged
|
||||
assert!(updated.updated > created.updated);
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ async fn test_update_rule_description() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test".to_string(),
|
||||
description: "Old description".to_string(),
|
||||
description: Some("Old description".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -662,7 +662,7 @@ async fn test_update_rule_description() {
|
||||
let created = RuleRepository::create(&pool, input).await.unwrap();
|
||||
|
||||
let update = UpdateRuleInput {
|
||||
description: Some("New description".to_string()),
|
||||
description: Some(Patch::Set("New description".to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -670,7 +670,7 @@ async fn test_update_rule_description() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.description, "New description");
|
||||
assert_eq!(updated.description, Some("New description".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -698,7 +698,7 @@ async fn test_update_rule_conditions() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -750,7 +750,7 @@ async fn test_update_rule_enabled() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -803,7 +803,7 @@ async fn test_update_rule_multiple_fields() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Old".to_string(),
|
||||
description: "Old".to_string(),
|
||||
description: Some("Old".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -819,7 +819,7 @@ async fn test_update_rule_multiple_fields() {
|
||||
|
||||
let update = UpdateRuleInput {
|
||||
label: Some("New Label".to_string()),
|
||||
description: Some("New Description".to_string()),
|
||||
description: Some(Patch::Set("New Description".to_string())),
|
||||
conditions: Some(json!({"updated": true})),
|
||||
action_params: None,
|
||||
trigger_params: None,
|
||||
@@ -831,7 +831,7 @@ async fn test_update_rule_multiple_fields() {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.label, "New Label");
|
||||
assert_eq!(updated.description, "New Description");
|
||||
assert_eq!(updated.description, Some("New Description".to_string()));
|
||||
assert_eq!(updated.conditions, json!({"updated": true}));
|
||||
assert!(!updated.enabled);
|
||||
}
|
||||
@@ -861,7 +861,7 @@ async fn test_update_rule_no_changes() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Test".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -914,7 +914,7 @@ async fn test_delete_rule() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "To Delete".to_string(),
|
||||
description: "Will be deleted".to_string(),
|
||||
description: Some("Will be deleted".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -995,7 +995,7 @@ async fn test_find_rules_by_pack() {
|
||||
pack: pack1.id,
|
||||
pack_ref: pack1.r#ref.clone(),
|
||||
label: format!("Rule {}", i),
|
||||
description: format!("Rule {}", i),
|
||||
description: Some(format!("Rule {}", i)),
|
||||
action: action1.id,
|
||||
action_ref: action1.r#ref.clone(),
|
||||
trigger: trigger1.id,
|
||||
@@ -1016,7 +1016,7 @@ async fn test_find_rules_by_pack() {
|
||||
pack: pack2.id,
|
||||
pack_ref: pack2.r#ref.clone(),
|
||||
label: "Pack2 Rule".to_string(),
|
||||
description: "Pack2".to_string(),
|
||||
description: Some("Pack2".to_string()),
|
||||
action: action2.id,
|
||||
action_ref: action2.r#ref.clone(),
|
||||
trigger: trigger2.id,
|
||||
@@ -1073,7 +1073,7 @@ async fn test_find_rules_by_action() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("Action1 Rule {}", i),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action1.id,
|
||||
action_ref: action1.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1094,7 +1094,7 @@ async fn test_find_rules_by_action() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Action2 Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action2.id,
|
||||
action_ref: action2.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1155,7 +1155,7 @@ async fn test_find_rules_by_trigger() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("Trigger1 Rule {}", i),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger1.id,
|
||||
@@ -1176,7 +1176,7 @@ async fn test_find_rules_by_trigger() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Trigger2 Rule".to_string(),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger2.id,
|
||||
@@ -1234,7 +1234,7 @@ async fn test_find_enabled_rules() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("Enabled {}", i),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1256,7 +1256,7 @@ async fn test_find_enabled_rules() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("Disabled {}", i),
|
||||
description: "Test".to_string(),
|
||||
description: Some("Test".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1312,7 +1312,7 @@ async fn test_cascade_delete_pack_deletes_rules() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Cascade Rule".to_string(),
|
||||
description: "Will be cascade deleted".to_string(),
|
||||
description: Some("Will be cascade deleted".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
@@ -1368,7 +1368,7 @@ async fn test_rule_timestamps() {
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Timestamp Rule".to_string(),
|
||||
description: "Test timestamps".to_string(),
|
||||
description: Some("Test timestamps".to_string()),
|
||||
action: action.id,
|
||||
action_ref: action.r#ref.clone(),
|
||||
trigger: trigger.id,
|
||||
|
||||
@@ -179,7 +179,7 @@ async fn test_create_sensor_duplicate_ref_fails() {
|
||||
pack: Some(pack.id),
|
||||
pack_ref: Some(pack.r#ref.clone()),
|
||||
label: "Duplicate Sensor".to_string(),
|
||||
description: "Test sensor".to_string(),
|
||||
description: Some("Test sensor".to_string()),
|
||||
entrypoint: "sensors/dup.py".to_string(),
|
||||
runtime: runtime.id,
|
||||
runtime_ref: runtime.r#ref.clone(),
|
||||
@@ -235,7 +235,7 @@ async fn test_create_sensor_invalid_ref_format_fails() {
|
||||
pack: Some(pack.id),
|
||||
pack_ref: Some(pack.r#ref.clone()),
|
||||
label: "Invalid Sensor".to_string(),
|
||||
description: "Test sensor".to_string(),
|
||||
description: Some("Test sensor".to_string()),
|
||||
entrypoint: "sensors/invalid.py".to_string(),
|
||||
runtime: runtime.id,
|
||||
runtime_ref: runtime.r#ref.clone(),
|
||||
@@ -276,7 +276,7 @@ async fn test_create_sensor_invalid_pack_fails() {
|
||||
pack: Some(99999), // Non-existent pack
|
||||
pack_ref: Some("invalid".to_string()),
|
||||
label: "Invalid Pack Sensor".to_string(),
|
||||
description: "Test sensor".to_string(),
|
||||
description: Some("Test sensor".to_string()),
|
||||
entrypoint: "sensors/invalid.py".to_string(),
|
||||
runtime: runtime.id,
|
||||
runtime_ref: runtime.r#ref.clone(),
|
||||
@@ -308,7 +308,7 @@ async fn test_create_sensor_invalid_trigger_fails() {
|
||||
pack: None,
|
||||
pack_ref: None,
|
||||
label: "Invalid Trigger Sensor".to_string(),
|
||||
description: "Test sensor".to_string(),
|
||||
description: Some("Test sensor".to_string()),
|
||||
entrypoint: "sensors/invalid.py".to_string(),
|
||||
runtime: runtime.id,
|
||||
runtime_ref: runtime.r#ref.clone(),
|
||||
@@ -340,7 +340,7 @@ async fn test_create_sensor_invalid_runtime_fails() {
|
||||
pack: None,
|
||||
pack_ref: None,
|
||||
label: "Invalid Runtime Sensor".to_string(),
|
||||
description: "Test sensor".to_string(),
|
||||
description: Some("Test sensor".to_string()),
|
||||
entrypoint: "sensors/invalid.py".to_string(),
|
||||
runtime: 99999, // Non-existent runtime
|
||||
runtime_ref: "invalid.runtime".to_string(),
|
||||
@@ -728,7 +728,7 @@ async fn test_update_description() {
|
||||
.unwrap();
|
||||
|
||||
let input = UpdateSensorInput {
|
||||
description: Some("New description for the sensor".to_string()),
|
||||
description: Some(Patch::Set("New description for the sensor".to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -736,7 +736,10 @@ async fn test_update_description() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.description, "New description for the sensor");
|
||||
assert_eq!(
|
||||
updated.description,
|
||||
Some("New description for the sensor".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -934,7 +937,7 @@ async fn test_update_multiple_fields() {
|
||||
|
||||
let input = UpdateSensorInput {
|
||||
label: Some("Multi Update".to_string()),
|
||||
description: Some("Updated multiple fields".to_string()),
|
||||
description: Some(Patch::Set("Updated multiple fields".to_string())),
|
||||
entrypoint: Some("sensors/multi.py".to_string()),
|
||||
enabled: Some(false),
|
||||
param_schema: Some(Patch::Set(json!({"type": "object"}))),
|
||||
@@ -946,7 +949,10 @@ async fn test_update_multiple_fields() {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.label, "Multi Update");
|
||||
assert_eq!(updated.description, "Updated multiple fields");
|
||||
assert_eq!(
|
||||
updated.description,
|
||||
Some("Updated multiple fields".to_string())
|
||||
);
|
||||
assert_eq!(updated.entrypoint, "sensors/multi.py");
|
||||
assert!(!updated.enabled);
|
||||
assert_eq!(updated.param_schema, Some(json!({"type": "object"})));
|
||||
|
||||
Reference in New Issue
Block a user