Skip to content

Commit

Permalink
Add ability to specify upload.acl concisely via object
Browse files Browse the repository at this point in the history
Before, one had to always specify an XML blob, which is suuuper verbose.
With this, Studio also allows a more concise representation that's also
suitable to be included in the URL. While each string in the object is
also treated as a mustache template, with the object notation, it is not
possible to conditionally add entries. At least not yet.
  • Loading branch information
LukasKalbertodt committed Jan 14, 2025
1 parent 29de717 commit 1db0f7b
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 78 deletions.
55 changes: 42 additions & 13 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,7 @@ further below for information on that.

# Defines which ACL to send when uploading the recording. See below for
# more information.
#acl = false
# -OR-
#acl = """
#<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
#<Policy ...>
# ...
#</Policy>
#"""
#acl = ...

# Defines a custom DC-Catalog (as a template). See below for more information.
#dcc = """
Expand Down Expand Up @@ -277,9 +270,10 @@ By default, this template is used:
With `upload.acl` you can configure which ACL is sent (as an attachment) to the Opencast server when uploading. Possible values:
- `true`: use the default ACL (this is the default behavior)
- `false`: do not send an ACL when uploading
- An object with string keys and string arrays as values, mapping from roles to list of allowed actions.
- A string containing a valid ACL XML template (note the `"""` multi line string in TOML)

The ACL XML template is a [Mustache.js template](https://mustache.github.io/mustache.5.html).
The ACL XML as well as each string in the object is a [Mustache.js template](https://mustache.github.io/mustache.5.html).
Summary of the template format: you can insert variables with `{{ foo }}`.
You can also access subfields like `{{ foo.bar }}` (if `foo` or `bar` is `null`, the expression just evaluates to the empty string).
Conditionals/loops work with the `{{ #foo }} ... {{ /foo }}` syntax:
Expand All @@ -297,11 +291,26 @@ Otherwise, processing will fail.

#### Examples

<details>
<summary>The default ACL template</summary>
##### The default ACL template

The default ACL template simply gives read and write access to `user.userRole`:

```toml
[upload]
acl = { "{{ user.userRole }}" = ["read", "write"] }
```

Or in alternative TOML syntax:

```toml
[upload]
acl."{{ user.userRole }}" = ["read", "write"]
```

Or as XML template:

<details>

```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Policy PolicyId="mediapackage-1"
Expand Down Expand Up @@ -353,14 +362,34 @@ The default ACL template simply gives read and write access to `user.userRole`:

</details>

##### Specifying via URL

To specify the ACL via URL parameter, it's likely not feasible to use XML.
You can use the object notation tho, by specifying a JSON object like this:

```
https://studio.opencast.org/?upload.acl={"ROLE_FOO":["read"],"{{user.userRole}}":["read","write"]}
```

Remember to percent encode the whole JSON object!

This is equivalent to the following TOML:

```toml
[upload]
acl.ROLE_FOO = "read"
acl."{{ user.userRole }}" = ["read", "write"]
```

<details>
<summary>Also giving access to LTI instructors</summary>

##### Also giving access to LTI instructors

This extends the default template and additionally gives read and write access to the LTI instructors (`{{ lti.context_id }}_Instructor`), *if* the user comes from an LTI session.
Note the use of conditionals `{{ #lti.context_id }} ... {{ /lti.context_id }}`.
If you also want to add read access for learners, just add another `<Rule>` with `{{ lti.context_id }}_Learner`.

<details>

```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Policy PolicyId="mediapackage-1"
Expand Down
24 changes: 18 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"html-webpack-plugin": "^5.6.3",
"i18next": "^24.2.1",
"i18next-browser-languagedetector": "^8.0.2",
"is-plain-object": "^5.0.0",
"mustache": "^4.2.0",
"oscilloscope": "^1.3.0",
"react": "^18.3.1",
Expand Down
91 changes: 39 additions & 52 deletions src/opencast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Mustache from "mustache";
import { bug } from "@opencast/appkit";

import { recordingFileName, usePresentContext } from "./util";
import { Settings } from "./settings";
import { Acl, DEFAULT_ACL, Settings } from "./settings";
import { Recording } from "./studio-state";


Expand Down Expand Up @@ -513,9 +513,10 @@ export class Opencast {
mediaPackage: string;
uploadSettings: Settings["upload"];
}) {
const template = uploadSettings?.acl === true || (!uploadSettings?.acl)
? DEFAULT_ACL_TEMPLATE
: uploadSettings?.acl;
const aclConfig = uploadSettings?.acl;
const template = (aclConfig === true || !aclConfig)
? aclToXmlTemplate(DEFAULT_ACL)
: typeof aclConfig === "string" ? aclConfig : aclToXmlTemplate(aclConfig);
const acl = this.constructAcl(template);

const body = new FormData();
Expand Down Expand Up @@ -810,6 +811,40 @@ const renderTemplate = (template: string, view: object): string => {
return out;
};

const aclToXmlTemplate = (acl: Acl) => {
const rules = [...acl.entries()].flatMap(([role, actions], i) => actions.map((action, j) => `
<Rule RuleId="${i}:${j}" Effect="Permit">
<Target>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">${action}</AttributeValue>
<ActionAttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Condition>
<Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-is-in">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">${role}</AttributeValue>
<SubjectAttributeDesignator AttributeId="urn:oasis:names:tc:xacml:2.0:subject:role"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</Apply>
</Condition>
</Rule>`
));

return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Policy PolicyId="mediapackage-1"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:permit-overrides"
Version="2.0"
xmlns="urn:oasis:names:tc:xacml:2.0:policy:schema:os">
${rules.join("\n")}
</Policy>
`;
};

const DEFAULT_DCC_TEMPLATE = `<?xml version="1.0" encoding="UTF-8"?>
<dublincore xmlns="http://www.opencastproject.org/xsd/1.0/dublincore/"
xmlns:dcterms="http://purl.org/dc/terms/"
Expand All @@ -825,54 +860,6 @@ const DEFAULT_DCC_TEMPLATE = `<?xml version="1.0" encoding="UTF-8"?>
</dublincore>
`;

const DEFAULT_ACL_TEMPLATE = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Policy PolicyId="mediapackage-1"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:permit-overrides"
Version="2.0"
xmlns="urn:oasis:names:tc:xacml:2.0:policy:schema:os">
<Rule RuleId="user_read_Permit" Effect="Permit">
<Target>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">read</AttributeValue>
<ActionAttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Condition>
<Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-is-in">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">{{ user.userRole }}</AttributeValue>
<SubjectAttributeDesignator AttributeId="urn:oasis:names:tc:xacml:2.0:subject:role"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</Apply>
</Condition>
</Rule>
<Rule RuleId="user_write_Permit" Effect="Permit">
<Target>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">write</AttributeValue>
<ActionAttributeDesignator AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Condition>
<Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-is-in">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">{{ user.userRole }}</AttributeValue>
<SubjectAttributeDesignator AttributeId="urn:oasis:names:tc:xacml:2.0:subject:role"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</Apply>
</Condition>
</Rule>
</Policy>
`;

const smil = ({ start, end }: { start: number; end: number }) => `
<smil xmlns="http://www.w3.org/ns/SMIL">
<body>
Expand Down
45 changes: 38 additions & 7 deletions src/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { unreachable } from "@opencast/appkit";

import { decodeHexString, usePresentContext } from "./util";
import { DEFINES } from "./defines";
import { isPlainObject } from "is-plain-object";

const LOCAL_STORAGE_KEY = "ocStudioSettings";
const CONTEXT_SETTINGS_FILE = "settings.toml";
Expand All @@ -24,6 +25,13 @@ type SettingsSource = "src-server"| "src-url" | "src-local-storage";
const PRESENTER_SOURCES = ["opencast"] as const;
type PresenterSource = typeof PRESENTER_SOURCES[number];

/** Map from roles to array of permitted actions */
export type Acl = Map<string, string[]>;

export const DEFAULT_ACL: Acl = new Map([
["{{ user.userRole }}", ["read", "write"]],
]);

/** Opencast Studio runtime settings. */
export type Settings = {
opencast?: {
Expand All @@ -35,7 +43,7 @@ export type Settings = {
upload?: {
seriesId?: string;
workflowId?: string;
acl?: boolean | string;
acl?: boolean | string | Acl;
dcc?: string;
titleField?: FormFieldState;
presenterField?: FormFieldState;
Expand Down Expand Up @@ -380,8 +388,9 @@ const validate = (
const newValue = validation(value, allowParse, src);
return newValue === undefined ? value : newValue;
} catch (e) {
const printValue = typeof value === "object" ? JSON.stringify(value) : value;
console.warn(
`Validation of setting '${path}' (${sourceDescription}) with value '${value}' failed: `
`Validation of setting '${path}' (${sourceDescription}) with value '${printValue}' failed: `
+ `${e}. Ignoring.`
);
return null;
Expand Down Expand Up @@ -576,7 +585,27 @@ const SCHEMA = {
return v;
}

throw new Error("needs to be 'true', 'false' or an XML string");
let obj = v;
if (allowParse && typeof v === "string") {
const json = decodeURIComponent(v);
obj = JSON.parse(json);
}

if (typeof obj === "object" && obj) {
const out = new Map<string, string[]>();
for (const [key, value] of Object.entries(obj)) {
if (!Array.isArray(value) || value.some(x => typeof x !== "string")) {
throw new Error("values of ACL object need to be string arrays");
}

// "Useless" map to get rid of other properties inside array from toml parsing
out.set(key, value.map(v => v));
}

return out;
}

throw new Error("needs to be 'true', 'false', an object or an XML string");
},
dcc: types.string,
titleField: metaDataField,
Expand Down Expand Up @@ -632,10 +661,12 @@ const SCHEMA = {
// ==============================================================================================

// Customize array merge behavior
const merge = (a: Settings, b: Settings): Settings => deepmerge(a, b, { arrayMerge });
const mergeAll = (array: Settings[]) => deepmerge.all(array, { arrayMerge });
const arrayMerge: deepmerge.Options["arrayMerge"]
= (_destinationArray, sourceArray, _options) => sourceArray;
const mergeOptions: deepmerge.Options = {
arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray,
isMergeableObject: isPlainObject,
};
const merge = (a: Settings, b: Settings): Settings => deepmerge(a, b, mergeOptions);
const mergeAll = (array: Settings[]) => deepmerge.all(array, mergeOptions);


// ==============================================================================================
Expand Down

0 comments on commit 1db0f7b

Please sign in to comment.