Source: access.ts

/**
 * @author Michael Hasenstein <hasenstein@yahoo.com>
 * @copyright REFINIO GmbH 2017
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

/**
 * Create Access or IdAccess objects giving Chum-sync access for a Person or a group to that
 * object or ID object (all versions).
 * @module
 */

/**
 * An `enum` to be used when calling the {@link access.module:ts} `access` module to set access
 * rights and create {@link IdAccess} or {@link Access} ONE objects.
 * @global
 * @typedef {('replace'|'add')} SetAccessMode
 */
export type SetAccessMode = (typeof SET_ACCESS_MODE)[keyof typeof SET_ACCESS_MODE];

/**
 * An array of `SetAccessParam` objects is expected by the {@link access.module:ts|"Lib/access"}
 * module.
 * @global
 * @typedef {object} SetAccessParam
 * @property {Reference} [object] - A reference to the unversioned or versioned object for which
 * access rights are set
 * @property {SHA256IdHash} [id] - An ID reference to the ID object of a versioned object for
 * which access rights are set *for all current and future versions*
 * @property {('replace'|'add')} mode - If there already is an Access object, add to its list of
 * grant hashes or ignore its contents and let the new Access object exclusively use the data
 * from this new set-access request? The parameter is *mandatory* to force developers to add
 * this piece of information each time access is set. This is to prevent errors when the code
 * makes an assumption but during runtime the actual behavior depends on if there is a previous
 * Access object or not.
 * @property {SHA256IdHash[]} person - An array of references to Person ID objects to whom
 * access is granted. All versions of A reference Person ID object are granted access - versions
 * in space (Person) vs. versions in time (Group) access.
 * @property {SHA256IdHash[]} group - An array of references to  Group ID objects to whom
 * access is granted. Only the latest version (at the time of actual access attempt) is granted
 * access - versions in space (Person) vs. versions in time (Group) access.
 */
export interface SetAccessParam {
    object?: SHA256Hash;
    id?: SHA256IdHash;
    person: Array<SHA256IdHash<Person>>;
    group: Array<SHA256IdHash<Group>>;
    mode: SetAccessMode;
}

import {createError} from './errors';
import type {Access, Group, IdAccess, Person} from './recipes';
import {SET_ACCESS_MODE} from './storage-base-common';
import type {VersionedObjectResult} from './storage-versioned-objects';
import {getObjectByIdObj, storeVersionedObject} from './storage-versioned-objects';
import type {SHA256Hash, SHA256IdHash} from './util/type-checks';
import {isHash} from './util/type-checks';

/**
 * @private
 * @param {SetAccessParam} accessRequest
 * @returns {boolean} Returns `true` if the given object is a {@link SetAccessParam} object
 * @throws {Error} If there are missing oir conflicting parameters an `Error` is thrown
 */
function isSetAccessParam(accessRequest: SetAccessParam): accessRequest is SetAccessParam {
    if (
        accessRequest.mode !== SET_ACCESS_MODE.ADD &&
        accessRequest.mode !== SET_ACCESS_MODE.REPLACE
    ) {
        return false;
    }

    if (accessRequest.object === undefined && accessRequest.id === undefined) {
        return false;
    }

    if (accessRequest.object !== undefined && accessRequest.id !== undefined) {
        return false;
    }

    if (accessRequest.object !== undefined && !isHash(accessRequest.object)) {
        return false;
    }

    if (accessRequest.id !== undefined && !isHash(accessRequest.id)) {
        return false;
    }

    // NOTE: An empty array is allowed! This creates an Access object without any grants, which
    // revokes any grants set in the previous version of the Access object. We place no
    // restriction on the `mode`, so it is possible to use mode="add" and an empty array to
    // create a new Access object that is exactly the same as the last one. Whether this writes
    // anything to storage - it would only affect the version map - depends on the version map
    // policy (`storeVersionedObject` parameter).

    if (
        accessRequest.person !== undefined &&
        (!Array.isArray(accessRequest.person) || !accessRequest.person.every(item => isHash(item)))
    ) {
        return false;
    }

    // noinspection RedundantIfStatementJS
    if (
        accessRequest.group !== undefined &&
        (!Array.isArray(accessRequest.group) || !accessRequest.group.every(item => isHash(item)))
    ) {
        return false;
    }

    return true;
}

/**
 * The function creates an Access object or an IdAccess object
 * @private
 * @param {SetAccessParam} accessRequest
 * @returns {Promise<VersionedObjectResult>}
 * @throws {Error} If there are missing oir conflicting parameters an `Error` is thrown
 */
async function setAccessForOneObject(
    accessRequest: SetAccessParam
): Promise<VersionedObjectResult<Access | IdAccess>> {
    if (!isSetAccessParam(accessRequest)) {
        throw createError('ACC-CP1', {accessRequest});
    }

    const accessObj =
        accessRequest.id === undefined
            ? ({
                  $type$: 'Access',
                  object: accessRequest.object,
                  person: accessRequest.person || [],
                  group: accessRequest.group || []
              } as Access)
            : ({
                  $type$: 'IdAccess',
                  id: accessRequest.id,
                  person: accessRequest.person || [],
                  group: accessRequest.group || []
              } as IdAccess);

    // REPLACE or ADD
    if (accessRequest.mode === SET_ACCESS_MODE.ADD) {
        try {
            const {obj: previousAccessObj} = await getObjectByIdObj(accessObj);

            accessObj.person.push(...previousAccessObj.person);
            accessObj.group.push(...previousAccessObj.group);
        } catch (err) {
            // "File not found" is okay, there may not be a previous version
            if (err.name !== 'FileNotFoundError') {
                throw err;
            }
        }
    }

    return await storeVersionedObject(accessObj);
}

/**
 * Create one Access object for a given versioned ONE object.  Access objects have "references"
 * and not just a simple SHA-256 hash for linkage to the target object because objects should be
 * linked through reference links, but more importantly, only Reference object links trigger the
 * writing of reverse maps, which makes it possible to start from a given Person and find all
 * Access objects that reference it. For any given versioned object find all Access objects
 * referencing it (granting access to someone).
 * If the `person` or `group` array is empty an Access object without grants is created if no
 * previous Access object existed for the given object. If there already is one and `mode` is
 * `mode` is "add" (constant `Storage.SET_ACCESS_MODE.ADD`) only a new version map entry is
 * created, if `mode` is "replace" a new most current Access object version is created without
 * grants, thereby revoking any access rights that may previously have existed.
 *
 * @example
 *
 * // Set access right for two objects (Array of two SetAccessParam objects)
 * // Result: Array with two VersionedObjectResult<Access> objects
 * const access: ObjectCreation = await createAccess([
 *     {
 *         object: objectHash,
 *         person: [personIdHash, anotherPersonIdHash],
 *         group: [],
 *         mode: Storage.SET_ACCESS_MODE.ADD
 *     },
 *     {
 *         object: objectHash,
 *         person: personIdHash,
 *         group: [],
 *         mode: Storage.SET_ACCESS_MODE.REPLACE
 *     }
 * ]);
 *
 * @static
 * @async
 * @param {SetAccessParam[]} accessRequests - An array of object/person-references Reference
 * objects. For each object access rights are created for the corresponding Person objects.
 * @returns {Promise<VersionedObjectResult[]>} Returns the result of writing the Access
 * object.
 */
export async function createAccess(
    accessRequests: SetAccessParam[]
): Promise<Array<VersionedObjectResult<Access | IdAccess>>> {
    return await Promise.all(
        accessRequests.map(
            (accessRequest: SetAccessParam): Promise<VersionedObjectResult<Access | IdAccess>> =>
                setAccessForOneObject(accessRequest)
        )
    );
}