Skip to content

Commit

Permalink
Add migration for custom ACL actions (#1164)
Browse files Browse the repository at this point in the history
Custom actions for events 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.

Part of #1004
  • Loading branch information
LukasKalbertodt authored May 16, 2024
2 parents 383f4a2 + 6d176ce commit 0686824
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 3 deletions.
1 change: 1 addition & 0 deletions backend/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,4 +364,5 @@ static MIGRATIONS: Lazy<BTreeMap<u64, Migration>> = include_migrations![
29: "extend-series-block",
30: "realm-permissions",
31: "series-metadata",
32: "custom-actions",
];
45 changes: 45 additions & 0 deletions backend/src/db/migrations/32-custom-actions.sql
Original file line number Diff line number Diff line change
@@ -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();
37 changes: 37 additions & 0 deletions backend/src/db/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,40 @@ impl<'a> FromSql<'a> for ExtraMetadata {
<serde_json::Value as FromSql>::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<String, Vec<String>>);

impl ToSql for CustomActions {
fn to_sql(
&self,
ty: &postgres_types::Type,
out: &mut BytesMut,
) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
serde_json::to_value(self)
.expect("failed to convert `CustomActions` to JSON value")
.to_sql(ty, out)
}

fn accepts(ty: &postgres_types::Type) -> bool {
<serde_json::Value as ToSql>::accepts(ty)
}

postgres_types::to_sql_checked!();
}

impl<'a> FromSql<'a> for CustomActions {
fn from_sql(
ty: &postgres_types::Type,
raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
serde_json::from_value(<_>::from_sql(ty, raw)?).map_err(Into::into)
}

fn accepts(ty: &postgres_types::Type) -> bool {
<serde_json::Value as FromSql>::accepts(ty)
}
}
5 changes: 5 additions & 0 deletions backend/src/sync/harvest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<EventTrack>>();
let captions = captions.into_iter().map(Into::into).collect::<Vec<EventCaption>>();

Expand All @@ -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?;
Expand Down
8 changes: 5 additions & 3 deletions backend/src/sync/harvest/response.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -128,10 +128,12 @@ impl Into<EventCaption> for Caption {
}
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Acl {
#[serde(default)]
pub(crate) read: Vec<String>,
#[serde(default)]
pub(crate) write: Vec<String>,
#[serde(flatten)]
pub(crate) custom_actions: CustomActions,
}

0 comments on commit 0686824

Please sign in to comment.