From 6d176cec95d962ff742745a64a6a61de2f8e30d9 Mon Sep 17 00:00:00 2001 From: Ole Wieners Date: Tue, 7 May 2024 10:21:36 +0200 Subject: [PATCH] Add migration for custom ACL actions Custom actions are now stored in DB with a new column. This is only the backend side of things, so they won't show up in the ACL UI yet. --- backend/src/db/migrations.rs | 1 + .../src/db/migrations/32-custom-actions.sql | 45 +++++++++++++++++++ backend/src/db/types.rs | 37 +++++++++++++++ backend/src/sync/harvest/mod.rs | 5 +++ backend/src/sync/harvest/response.rs | 8 ++-- 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 backend/src/db/migrations/32-custom-actions.sql diff --git a/backend/src/db/migrations.rs b/backend/src/db/migrations.rs index 760c4ed66..68e7e10d7 100644 --- a/backend/src/db/migrations.rs +++ b/backend/src/db/migrations.rs @@ -364,4 +364,5 @@ static MIGRATIONS: Lazy> = include_migrations![ 29: "extend-series-block", 30: "realm-permissions", 31: "series-metadata", + 32: "custom-actions", ]; diff --git a/backend/src/db/migrations/32-custom-actions.sql b/backend/src/db/migrations/32-custom-actions.sql new file mode 100644 index 000000000..4d4ce47ed --- /dev/null +++ b/backend/src/db/migrations/32-custom-actions.sql @@ -0,0 +1,45 @@ +-- Adds a field for custom actions to `events`. +-- The field's entries are mappings of individual custom actions +-- and the respective roles that are allowed to carry out that action. + +alter table events + add column custom_action_roles jsonb; + +-- The following function verifies that the custom action column and its +-- entries follow a predefined format. +create or replace function check_custom_actions_format() returns trigger as $$ +declare + col text := 'events.custom_action_roles'; + field record; + element jsonb; +begin + if jsonb_typeof(new.custom_action_roles) <> 'object' then + raise exception '% is %, but should be a JSON object', col, jsonb_typeof(new.custom_actions); + end if; + + for field in select * from jsonb_each(new.custom_action_roles) loop + if jsonb_typeof(field.value) <> 'array' then + raise exception '%: type of field "%" is %, but should be an array', + col, + field.key, + jsonb_typeof(field.value); + end if; + + for element in select * from jsonb_array_elements(field.value) loop + if jsonb_typeof(element) <> 'string' then + raise exception '%: found non-string element "%" in field "%", but that field should be a string array', + col, + element, + field.key; + end if; + end loop; + end loop; + + return new; +end; +$$ language plpgsql; + +create trigger check_custom_actions_format_on_upsert + before insert or update on events + for each row + execute procedure check_custom_actions_format(); diff --git a/backend/src/db/types.rs b/backend/src/db/types.rs index 97e6ecd3a..74371ff13 100644 --- a/backend/src/db/types.rs +++ b/backend/src/db/types.rs @@ -141,3 +141,40 @@ impl<'a> FromSql<'a> for ExtraMetadata { ::accepts(ty) } } + +/// Represents the type for the `custom_action_roles` field from `32-custom-actions.sql`. +/// This holds a mapping of actions to lists holding roles that are allowed +/// to carry out the respective action. +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct CustomActions(pub(crate) HashMap>); + +impl ToSql for CustomActions { + fn to_sql( + &self, + ty: &postgres_types::Type, + out: &mut BytesMut, + ) -> Result> { + serde_json::to_value(self) + .expect("failed to convert `CustomActions` to JSON value") + .to_sql(ty, out) + } + + fn accepts(ty: &postgres_types::Type) -> bool { + ::accepts(ty) + } + + postgres_types::to_sql_checked!(); +} + +impl<'a> FromSql<'a> for CustomActions { + fn from_sql( + ty: &postgres_types::Type, + raw: &'a [u8], + ) -> Result> { + serde_json::from_value(<_>::from_sql(ty, raw)?).map_err(Into::into) + } + + fn accepts(ty: &postgres_types::Type) -> bool { + ::accepts(ty) + } +} diff --git a/backend/src/sync/harvest/mod.rs b/backend/src/sync/harvest/mod.rs index 76809efe1..839f8f415 100644 --- a/backend/src/sync/harvest/mod.rs +++ b/backend/src/sync/harvest/mod.rs @@ -177,6 +177,10 @@ async fn store_in_db( acl.read.retain(|role| role != ROLE_ADMIN); acl.write.retain(|role| role != ROLE_ADMIN); + for (_, roles) in &mut acl.custom_actions.0 { + roles.retain(|role| role != ROLE_ADMIN); + } + let tracks = tracks.into_iter().map(Into::into).collect::>(); let captions = captions.into_iter().map(Into::into).collect::>(); @@ -199,6 +203,7 @@ async fn store_in_db( ("metadata", &metadata), ("read_roles", &acl.read), ("write_roles", &acl.write), + ("custom_action_roles", &acl.custom_actions), ("tracks", &tracks), ("captions", &captions), ]).await?; diff --git a/backend/src/sync/harvest/response.rs b/backend/src/sync/harvest/response.rs index 18bba37c5..483ca21ed 100644 --- a/backend/src/sync/harvest/response.rs +++ b/backend/src/sync/harvest/response.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -use crate::db::types::{EventTrack, ExtraMetadata, EventCaption}; +use crate::db::types::{CustomActions, EventCaption, EventTrack, ExtraMetadata}; /// What the harvesting API returns. @@ -128,10 +128,12 @@ impl Into for Caption { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub(crate) struct Acl { #[serde(default)] pub(crate) read: Vec, #[serde(default)] pub(crate) write: Vec, + #[serde(flatten)] + pub(crate) custom_actions: CustomActions, }