define([
"common/XMLUtilities",
"common/ValueUtilities",
"common/ValidationUtilities",
"models/sysmeta/AccessRule",
], (XMLUtilities, ValueUtilities, ValidationUtilities, AccessRule) => {
const { requireNonNegativeInteger } = ValueUtilities;
const { fixParsedValue } = ValidationUtilities;
const ACCESS_POLICY_CHILD_SEQUENCE = [
{ name: "allow", minOccurs: 1, maxOccurs: Infinity },
];
/**
* Normalize an input value to a positive integer and ensure it is within the
* valid range of indices. Throws an error if the index is out of bounds.
* @param {*} index Value to normalize as an array index.
* @param {Integer} length The length of the array to validate the index against.
* @returns {number} The normalized array index.
*/
const requireIndex = (index, length) => {
const normalizedIndex = requireNonNegativeInteger(index);
if (normalizedIndex >= length) {
throw new Error(
`AccessPolicy operation could not find rule at index ${normalizedIndex}.`,
);
}
return normalizedIndex;
};
/**
* Collection of access rules used by System Metadata.
* @class AccessPolicy
* @augments Array
* @classcategory Models/SysMeta
* @since 2.39.0
*/
class AccessPolicy extends Array {
/**
* Create an AccessPolicy instance.
* @param {Array<AccessRule|object>} [rules] Access rules to normalize.
*/
constructor(rules = []) {
super();
Array.from(rules).forEach((rule) => {
this.push(
rule instanceof AccessRule
? new AccessRule(rule.toJSON())
: new AccessRule(rule),
);
});
}
/**
* Append one or more access rules to the policy.
* @param {...(AccessRule|object)} rules Access rules to append.
* @returns {number} The new array length.
*/
push(...rules) {
return super.push(
...rules.map((rule) =>
rule instanceof AccessRule ? rule : new AccessRule(rule),
),
);
}
/**
* Append one access rule to the policy.
* @param {AccessRule|object} rule Access rule to append.
* @returns {AccessPolicy} The same policy instance.
*/
add(rule) {
this.push(rule);
return this;
}
/**
* Replace one access rule.
* @param {number} index Rule index to replace.
* @param {AccessRule|object} rule Replacement rule.
* @returns {AccessPolicy} The same policy instance.
* @throws {Error} When index is invalid or does not identify a rule
*/
replace(index, rule) {
const normalizedIndex = requireIndex(index, this.length);
this[normalizedIndex] =
rule instanceof AccessRule ? rule : new AccessRule(rule);
return this;
}
/**
* Remove one access rule.
* @param {number} index Rule index to remove.
* @returns {AccessPolicy} The same policy instance.
* @throws {Error} When index is invalid or does not identify a rule
*/
remove(index) {
const normalizedIndex = requireIndex(index, this.length);
this.splice(normalizedIndex, 1);
return this;
}
/**
* Remove all access rules.
* @returns {AccessPolicy} The same policy instance.
*/
clear() {
this.length = 0;
return this;
}
/**
* Normalize the policy in place.
* @returns {AccessPolicy} The same policy instance.
*/
normalize() {
const normalizedRules = Array.from(this, (rule) =>
rule instanceof AccessRule
? new AccessRule(rule.toJSON())
: new AccessRule(rule),
);
this.length = 0;
super.push(...normalizedRules);
this.removeEmptyRules();
return this;
}
/**
* Create an AccessPolicy from an `accessPolicy` XML element.
* @param {Element} element XML element to parse.
* @returns {AccessPolicy} Parsed access policy instance.
*/
static fromElement(element) {
const context = "AccessPolicy XML";
XMLUtilities.requireAllowedAttributeNames(element, [], context);
XMLUtilities.requireDirectChildSequence(
element,
ACCESS_POLICY_CHILD_SEQUENCE,
context,
);
return new AccessPolicy(
XMLUtilities.findDirectChildElements(element, "allow").map(
(ruleElement) => AccessRule.fromElement(ruleElement),
),
);
}
/**
* Parse an `accessPolicy` XML element while salvaging valid rules.
* Unknown child nodes and ordering issues are ignored silently.
* @param {Element|null} element XML element to parse.
* @param {Array<object>} [parseWarnings] Lossy parse warnings.
* @param {string} [path] Field path used in warnings and validation.
* @returns {AccessPolicy} Parsed access policy instance.
*/
static parse(element, parseWarnings = [], path = "accessPolicy") {
if (!element) return new AccessPolicy();
const rules = XMLUtilities.findDirectChildElements(element, "allow")
.map((ruleElement, index) =>
AccessRule.parse(ruleElement, parseWarnings, `${path}[${index}]`),
)
.filter(Boolean);
const policy = new AccessPolicy(rules);
return fixParsedValue({
value: policy,
path,
parseWarnings,
invalidMessage:
"Ignored invalid accessPolicy content while parsing system metadata.",
validate: (candidate) => candidate.validate(path),
fallback: () => new AccessPolicy(),
});
}
/**
* Coerce unknown input into an AccessPolicy instance.
* @param {AccessPolicy|Array<AccessRule|object>|AccessRule|object|null|undefined} value
* Access policy-like input.
* @returns {AccessPolicy} Normalized access policy instance.
*/
static fromValue(value) {
if (value === undefined || value === null) return new AccessPolicy();
if (value instanceof AccessPolicy) return new AccessPolicy(value);
// Legacy collections/AccessPolicy is a Backbone collection of AccessRule
// models; use its serialized rules so the legacy access-policy editor can
// save through the typed model.
if (
!Array.isArray(value) &&
Array.isArray(value.models) &&
typeof value.toJSON === "function"
) {
return new AccessPolicy(value.toJSON());
}
return new AccessPolicy(Array.isArray(value) ? value : [value]);
}
/** @returns {Array<AccessRule>} All access rules in the policy. */
getRules() {
return Array.from(this);
}
/** @returns {Array<AccessRule>} Rules missing a subject or permissions. */
getEmptyRules() {
return this.getRules().filter((rule) => rule.isEmpty());
}
/** @returns {Array<AccessRule>} Rules with a subject and permissions. */
getNonEmptyRules() {
return this.getRules().filter((rule) => !rule.isEmpty());
}
/** Remove rules missing a subject or permissions. */
removeEmptyRules() {
this.getEmptyRules().forEach((rule) => this.remove(this.indexOf(rule)));
}
/**
* Validate each access rule in the policy.
* @param {string} [path] Base path used in validation errors. Defaults to
* `accessPolicy`.
* @returns {Array<object>} Validation errors for invalid rules.
*/
validate(path = "accessPolicy") {
const errors = [];
this.getNonEmptyRules().forEach((rule, index) => {
errors.push(...rule.validate(`${path}[${index}]`));
});
return errors;
}
/**
* Serialize the access policy to XML.
* @param {XMLDocument} doc XML document used to create elements.
* @returns {Element|null} Serialized XML element, or `null` when empty.
*/
toElement(doc) {
const rules = this.getNonEmptyRules();
if (!rules.length) return null;
const element = doc.createElement("accessPolicy");
rules.forEach((rule) => {
const ruleElement = rule.toElement(doc);
if (ruleElement) element.appendChild(ruleElement);
});
return element.childNodes.length ? element : null;
}
/**
* Return a JSON-serializable representation of the access policy.
* @returns {Array<object>} Plain access rule data.
*/
toJSON() {
return this.getNonEmptyRules().map((rule) => rule.toJSON());
}
/**
* Check whether this policy grants the requested action to a subject.
* @param {"read"|"write"|"changePermission"} [action] Permission to check
* @param {string|Array<string>|null} [subject] Subject or candidate subjects
* Defaults to `public` when omitted.
* @returns {boolean} `true` when any access rule authorizes the request
*/
isAuthorized(action = "write", subject = null) {
return this.some((rule) => rule.isAuthorized(action, subject));
}
/**
* Check whether this policy grants public read access.
* @returns {boolean} `true` when public has read access directly or through
* the DataONE permission hierarchy.
*/
isPublic() {
return this.isAuthorized("read", "public");
}
}
return AccessPolicy;
});