Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config to lock video ACLs to their series #1305

Open
wants to merge 6 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions backend/src/api/model/search/series.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use juniper::GraphQLObject;
use juniper::{graphql_object, GraphQLObject};
use meilisearch_sdk::search::SearchResult;

use crate::{
api::{Context, Id, Node, NodeValue},
search, HasRoles,
api::{model::acl::{self, Acl}, Context, Id, Node, NodeValue},
search::{self, util::decode_acl},
HasRoles,
};

use super::{field_matches_for, ByteSpan, SearchRealm, ThumbnailInfo};
use super::{dbargs, field_matches_for, ApiResult, ByteSpan, SearchRealm, ThumbnailInfo};


#[derive(Debug, GraphQLObject)]
#[graphql(Context = Context, impl = NodeValue)]
#[derive(Debug)]
pub(crate) struct SearchSeries {
id: Id,
opencast_id: String,
Expand All @@ -19,6 +19,8 @@ pub(crate) struct SearchSeries {
host_realms: Vec<SearchRealm>,
thumbnails: Vec<ThumbnailInfo>,
matches: SearchSeriesMatches,
read_roles: Vec<String>,
write_roles: Vec<String>,
}


Expand Down Expand Up @@ -51,6 +53,8 @@ impl SearchSeries {
opencast_id: src.opencast_id,
title: src.title,
description: src.description,
read_roles: src.read_roles,
write_roles: src.write_roles,
host_realms: src.host_realms.into_iter()
.map(|r| SearchRealm::without_matches(r))
.collect(),
Expand All @@ -66,4 +70,45 @@ impl SearchSeries {
matches,
}
}

async fn load_acl(&self, context: &Context) -> ApiResult<Acl> {
let raw_roles_sql = "\
select unnest($1::text[]) as role, 'read' as action
union
select unnest($2::text[]) as role, 'write' as action
";

acl::load_for(context, raw_roles_sql, dbargs![
&decode_acl(&self.read_roles),
&decode_acl(&self.write_roles)
]).await
}
}

#[graphql_object(Context = Context, impl = NodeValue)]
impl SearchSeries {
fn id(&self) -> Id {
Node::id(self)
}
fn opencast_id(&self) -> &str {
&self.opencast_id
}
fn title(&self) -> &str {
&self.title
}
fn description(&self) -> Option<&String> {
self.description.as_ref()
}
fn host_realms(&self) -> &[SearchRealm] {
&self.host_realms
}
fn thumbnails(&self) -> &[ThumbnailInfo] {
&self.thumbnails
}
fn matches(&self) -> &SearchSeriesMatches {
&self.matches
}
async fn acl(&self, context: &Context) -> ApiResult<Acl> {
self.load_acl(context).await
}
}
7 changes: 7 additions & 0 deletions backend/src/config/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ pub(crate) struct GeneralConfig {
/// or takes an unusually long time to complete.
#[config(default = true)]
pub allow_acl_edit: bool,

/// Activating this will disable any ACL editing of events in the uploader and ACL editor
/// for that event.
/// Prerequisite for this is that the event is uploaded as part of a series, of which the ACL
/// will then also be used for the event.
#[config(default = false)]
pub lock_acl_to_series: bool,
}

const INTERNAL_RESERVED_PATHS: &[&str] = &["favicon.ico", "robots.txt", ".well-known"];
Expand Down
1 change: 1 addition & 0 deletions backend/src/http/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ fn frontend_config(config: &Config) -> serde_json::Value {
"showDownloadButton": config.general.show_download_button,
"usersSearchable": config.general.users_searchable,
"allowAclEdit": config.general.allow_acl_edit,
"lockAclToSeries": config.general.lock_acl_to_series,
"footerLinks": config.general.footer_links,
"metadataLabels": config.general.metadata,
"paellaPluginConfig": config.player.paella_plugin_config,
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/setup/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@
# Default value: true
#allow_acl_edit = true

# Activating this will disable any ACL editing of events in the uploader and ACL editor
# for that event.
# Prerequisite for this is that the event is uploaded as part of a series, of which the ACL
# will then also be used for the event.
#
# Default value: false
#lock_acl_to_series = false


[db]
# The username of the database user.
Expand Down
1 change: 1 addition & 0 deletions frontend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Config = {
showDownloadButton: boolean;
usersSearchable: boolean;
allowAclEdit: boolean;
lockAclToSeries: boolean;
opencast: OpencastConfig;
footerLinks: FooterLink[];
metadataLabels: Record<string, Record<string, MetadataLabel>>;
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ user:
manage-content: Verwalten

login-page:
heading: Anmeldung
heading: Anmeldung
user-id: Nutzerkennung
password: Passwort
bad-credentials: 'Anmeldung fehlgeschlagen: Falsche Anmeldedaten.'
Expand Down Expand Up @@ -341,6 +341,8 @@ manage:
Änderung der Berechtigungen ist zurzeit nicht möglich, da das Video im Hintergrund verarbeitet wird.
<br />
Bitte versuchen Sie es später nochmal.
locked-to-series: >
Die Berechtigungen dieses Videos werden durch seine Serie bestimmt und können daher nicht bearbeitet werden.
users-no-options:
initial-searchable: Nach Name suchen oder exakten Nutzernamen/exakte E-Mail angeben
none-found-searchable: Keine Personen gefunden
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ manage:
Changing the access policy is not possible at this time, since the video is being processed in the background.
<br />
Please try again later.
locked-to-series: >
The access policy of this video is determined by its series and can't be edited.
users-no-options:
initial-searchable: Type to search for users by name (or enter exact email/username)
none-found-searchable: No user found
Expand Down
61 changes: 44 additions & 17 deletions frontend/src/routes/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { loadQuery } from "../relay";
import { UploadQuery } from "./__generated__/UploadQuery.graphql";
import { makeRoute } from "../rauta";
import { ErrorDisplay, errorDisplayInfo } from "../util/err";
import { useNavBlocker } from "./util";
import { mapAcl, useNavBlocker } from "./util";
import CONFIG from "../config";
import { Button, boxError, ErrorBox, Card } from "@opencast/appkit";
import { LinkButton } from "../ui/LinkButton";
Expand All @@ -23,7 +23,7 @@ import { FieldIsRequiredNote, InputContainer, TitleLabel } from "../ui/metadata"
import { PageTitle } from "../layout/header/ui";
import { useRouter } from "../router";
import { getJwt } from "../relay/auth";
import { VideoListSelector } from "../ui/SearchableSelect";
import { AclArray, VideoListSelector } from "../ui/SearchableSelect";
import { Breadcrumbs } from "../ui/Breadcrumbs";
import { ManageNav, ManageRoute } from "./manage";
import { COLORS } from "../color";
Expand Down Expand Up @@ -68,7 +68,10 @@ const query = graphql`
type Metadata = {
title: string;
description: string;
series?: string;
series?: {
id: string;
acl: AclArray;
};
acl: Acl;
};

Expand Down Expand Up @@ -680,6 +683,7 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
const titleFieldId = useId();
const descriptionFieldId = useId();
const seriesFieldId = useId();
const [lockedAcl, setLockedAcl] = useState<Acl>(new Map());

const defaultAcl: Acl = new Map([
[user.userRole, {
Expand Down Expand Up @@ -709,6 +713,13 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
},
});

useEffect(() => {
if (CONFIG.lockAclToSeries) {
const seriesAcl = mapAcl(seriesField.value?.acl);
setLockedAcl(seriesAcl);
}
}, [seriesField.value]);

const onSubmit = handleSubmit(data => onSave(data));

// We only allow submitting the form on clicking the button below so that
Expand Down Expand Up @@ -770,7 +781,10 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
inputId={seriesFieldId}
writableOnly
menuPlacement="top"
onChange={data => seriesField.onChange(data?.opencastId)}
onChange={data => seriesField.onChange({
id: data?.opencastId,
acl: data?.acl,
})}
onBlur={seriesField.onBlur}
required={CONFIG.upload.requireSeries}
/>
Expand All @@ -784,17 +798,30 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
marginBottom: 12,
fontSize: 22,
}}>{t("manage.my-videos.acl.title")}</h2>
<Controller
name="acl"
control={control}
render={({ field }) => <AclSelector
userIsRequired
onChange={field.onChange}
acl={field.value}
knownRoles={knownRoles}
permissionLevels={READ_WRITE_ACTIONS}
/>}
/>
{lockedAcl.size > 0 && (
<Card kind="info" iconPos="left" css={{
maxWidth: 700,
fontSize: 14,
marginBottom: 10,
}}>
{t("manage.access.locked-to-series")}
</Card>
)}
<div {...lockedAcl.size > 0 && { inert: "true" }} css={{
...lockedAcl.size > 0 && { opacity: .7 },
}}>
<Controller
name="acl"
control={control}
render={({ field }) => <AclSelector
userIsRequired
onChange={field.onChange}
acl={lockedAcl.size > 0 ? lockedAcl : field.value}
knownRoles={knownRoles}
permissionLevels={READ_WRITE_ACTIONS}
/>}
/>
</div>
</InputContainer>

{/* Submit button */}
Expand Down Expand Up @@ -1039,7 +1066,7 @@ const finishUpload = async (
}

// Add ACL
{
if (!CONFIG.lockAclToSeries || !metadata.series?.acl) {
const acl = constructAcl(metadata.acl);
const body = new FormData();
body.append("flavor", "security/xacml+episode");
Expand Down Expand Up @@ -1088,7 +1115,7 @@ const constructDcc = (metadata: Metadata, user: User): string => {
</dcterms:created>
${tag("dcterms:title", metadata.title)}
${tag("dcterms:description", metadata.description)}
${tag("dcterms:isPartOf", metadata.series)}
${tag("dcterms:isPartOf", metadata.series?.id)}
${tag("dcterms:creator", user.displayName)}
${tag("dcterms:spatial", "Tobira Upload")}
</dublincore>
Expand Down
9 changes: 3 additions & 6 deletions frontend/src/routes/manage/Realm/RealmPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { boxError } from "@opencast/appkit";
import { displayCommitError } from "./util";
import { currentRef } from "../../../util";
import { MODERATE_ADMIN_ACTIONS } from "../../../util/permissionLevels";
import { mapAcl } from "../../util";


const fragment = graphql`
Expand All @@ -36,12 +37,8 @@ export const RealmPermissions: React.FC<Props> = ({ fragRef, data }) => {
const ownerDisplayName = (realm.ancestors[0] ?? realm).ownerDisplayName;
const saveModalRef = useRef<ConfirmationModalHandle>(null);

const [initialAcl, inheritedAcl]: Acl[] = [realm.ownAcl, realm.inheritedAcl].map(acl => new Map(
acl.map(item => [item.role, {
actions: new Set(item.actions),
info: item.info,
}])
));
const [initialAcl, inheritedAcl]: Acl[] = [realm.ownAcl, realm.inheritedAcl]
.map(acl => mapAcl(acl));

const [selections, setSelections] = useState<Acl>(initialAcl);

Expand Down
19 changes: 12 additions & 7 deletions frontend/src/routes/manage/Video/Access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ConfirmationModalHandle } from "../../../ui/Modal";
import { displayCommitError } from "../Realm/util";
import { AccessUpdateAclMutation } from "./__generated__/AccessUpdateAclMutation.graphql";
import CONFIG from "../../../config";
import { mapAcl } from "../../util";


export const ManageVideoAccessRoute = makeManageVideoRoute(
Expand Down Expand Up @@ -107,18 +108,17 @@ type AccessUIProps = {
}

const AccessUI: React.FC<AccessUIProps> = ({ event, knownRoles }) => {
const { t } = useTranslation();
const saveModalRef = useRef<ConfirmationModalHandle>(null);
const [commitError, setCommitError] = useState<JSX.Element | null>(null);
const [commit, inFlight] = useMutation<AccessUpdateAclMutation>(updateVideoAcl);
const [editingBlocked, setEditingBlocked] = useState(event.hasActiveWorkflows);

const initialAcl: Acl = new Map(
event.acl.map(item => [item.role, {
actions: new Set(item.actions),
info: item.info,
}])
const aclLockedToSeries = CONFIG.lockAclToSeries && event.series;
const [editingBlocked, setEditingBlocked] = useState(
event.hasActiveWorkflows || aclLockedToSeries
);

const initialAcl: Acl = mapAcl(event.acl);

const [selections, setSelections] = useState<Acl>(initialAcl);

const onSubmit = async () => {
Expand All @@ -145,6 +145,11 @@ const AccessUI: React.FC<AccessUIProps> = ({ event, knownRoles }) => {
{event.hasActiveWorkflows && <Card kind="info" css={{ marginBottom: 20 }}>
<Trans i18nKey="manage.access.workflow-active" />
</Card>}
{aclLockedToSeries && (
<Card kind="info" iconPos="left" css={{ fontSize: 14, marginBottom: 10 }}>
{t("manage.access.locked-to-series")}
</Card>
)}
<div css={{ maxWidth: 1040 }}>
<div css={{
display: "flex",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/routes/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { match } from "@opencast/appkit";
import { Link, useRouter } from "../router";
import CONFIG from "../config";
import { LoginRoute, REDIRECT_STORAGE_KEY } from "./Login";
import { AclArray } from "../ui/SearchableSelect";


export const b64regex = "[a-zA-Z0-9\\-_]";
Expand Down Expand Up @@ -95,3 +96,10 @@ export const LoginLink: React.FC<LoginLinkProps> = ({ className, children }) =>
{...{ className }}
>{children}</Link>
);

export const mapAcl = (acl?: AclArray) => new Map(
acl?.map(item => [item.role, {
actions: new Set(item.actions),
info: item.info,
}])
);
1 change: 1 addition & 0 deletions frontend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ type SearchSeries implements Node {
hostRealms: [SearchRealm!]!
thumbnails: [ThumbnailInfo!]!
matches: SearchSeriesMatches!
acl: [AclItem!]!
}

type SearchSeriesMatches {
Expand Down
Loading
Loading