define([
"models/dataONEServices/DataONEService",
"models/dataONEServices/DataONEHttpError",
"common/Utilities",
"common/ValueUtilities",
], (DataONEService, DataONEHttpError, Utilities, ValueUtilities) => {
const VALID_ACTIONS = Object.freeze(["read", "write", "changePermission"]);
const FALSE_STATUSES = Object.freeze([401, 403, 404]);
/**
* Service for DataONE MNAuthorization.isAuthorized checks.
* @class AuthorizationService
* @augments DataONEService
* @classcategory Models/DataONEServices
* @since 2.39.0
*/
class AuthorizationService extends DataONEService {
/**
* @param {object} [options] Options for the AuthorizationService. See
* {@link DataONEService.optionsFromDescriptor} for the shared option shape
* @param {string} [options.baseUrl] Endpoint-qualified isAuthorized URL
*/
constructor(options = {}) {
super(AuthorizationService.optionsFromDescriptor(options));
}
/**
* Check whether the current user can perform an action on a PID.
* @param {string} pid DataONE PID or SID
* @param {string} [action] Permission action
* @param {object} [options] Request options
* @returns {Promise<boolean>} Authorization result
* @throws {Error} When the PID or action is invalid, or the request fails
*/
async check(pid, action = "write", options = {}) {
const normalizedPid = this.constructor.normalizePid(
pid,
"pid",
"AuthorizationService.check requires a PID",
);
const normalizedAction = this.constructor.normalizeAction(action);
const path = this.constructor.buildPidPath(normalizedPid, {
query: `action=${normalizedAction}`,
});
try {
const response = await this.request(
this.constructor.buildRequestOptions({
options,
path,
method: "GET",
accept: "text/plain, text/xml, application/xml, */*",
}),
);
return this.constructor.parseAuthorizationResponse(response.data);
} catch (error) {
if (
error instanceof DataONEHttpError &&
FALSE_STATUSES.includes(error.status)
) {
return false;
}
throw error;
}
}
/**
* Check write permission for a PID.
* @param {string} pid DataONE PID or SID
* @param {object} [options] Request options
* @returns {Promise<boolean>} Authorization result
*/
checkWrite(pid, options = {}) {
return this.check(pid, "write", options);
}
/**
* Check read permission for a PID.
* @param {string} pid DataONE PID or SID
* @param {object} [options] Request options
* @returns {Promise<boolean>} Authorization result
*/
canView(pid, options = {}) {
return this.check(pid, "read", options);
}
/**
* Check changePermission access for a PID.
* @param {string} pid DataONE PID or SID
* @param {object} [options] Request options
* @returns {Promise<boolean>} Authorization result
*/
canChangePermission(pid, options = {}) {
return this.check(pid, "changePermission", options);
}
/**
* Check one action for multiple PIDs.
* @param {string[]} pids DataONE PIDs or SIDs
* @param {string} [action] Permission action
* @param {object} [options] Request options
* @param {Function} [options.onProgress] Progress callback
* @returns {Promise<object>} Object keyed by PID
* @throws {Error} When concurrency is invalid or a permission check fails
*/
async checkAll(pids = [], action = "write", options = {}) {
const normalizedPids = ValueUtilities.normalizeStringList(pids);
const { maxConcurrent = 4, onProgress, ...requestOptions } = options;
const limit = ValueUtilities.requirePositiveInteger(
maxConcurrent,
"AuthorizationService.checkAll maxConcurrent must be a positive integer",
);
const results = {};
let completed = 0;
if (normalizedPids.length && typeof onProgress === "function") {
onProgress({ completed, total: normalizedPids.length });
}
const { errors } = await Utilities.processConcurrently(
normalizedPids,
async (pid) => {
results[pid] = await this.check(pid, action, requestOptions);
},
{
maxConcurrent: limit,
stopOnError: true,
onItemComplete: (pid) => {
completed += 1;
if (typeof onProgress === "function") {
onProgress({
action,
completed,
pid,
total: normalizedPids.length,
});
}
},
},
);
if (errors.length) throw errors[0].error;
return results;
}
/**
* Resolve the current permission cache key.
* @returns {Promise<string>} Current user subject, or "public"
*/
async getUserKey() {
return (await this.getUserName()) || "public";
}
/**
* Normalize and validate a permission action.
* @param {*} action Candidate action
* @returns {string} Canonical action
*/
static normalizeAction(action = "write") {
return ValueUtilities.requireStringChoice(action, VALID_ACTIONS, {
fieldName: "authorization action",
});
}
/**
* Parse DataONE authorization response bodies.
* @param {*} value Response body
* @returns {boolean} Authorization result
*/
static parseAuthorizationResponse(value) {
const text = ValueUtilities.normalizeText(value);
if (!text) return true;
const normalized = text.toLowerCase();
if (normalized === "true") return true;
if (normalized === "false") return false;
if (/<[^>]*false[^>]*>/i.test(text) || />\s*false\s*</i.test(text)) {
return false;
}
if (/<[^>]*true[^>]*>/i.test(text) || />\s*true\s*</i.test(text)) {
return true;
}
return false;
}
}
/** @type {DataONEService#DataONEServiceConfig} */
AuthorizationService.config = {
endpoint: "isAuthorized",
appModelKeys: ["authServiceUrl"],
client: {
timeoutMs: 60 * 1000,
methods: ["GET"],
responseTypes: ["text"],
dedupeHeaders: ["Authorization", "Accept"],
},
persistPrivate: false,
defaultAuth: true,
};
AuthorizationService.actions = VALID_ACTIONS;
return AuthorizationService;
});