webstrate-permission-component/webstrate-permission-manager.js

/**
 *  Permission Manager
 *  Low-level interface to control permissions on a Webstrate
 * 
 *  Copyright 2020, 2021 Rolf Bagge, Janus B. Kristensen
 *  Copyright 2025, Janus B. Kristensen, CAVI
 *  Center for Advanced Visualization and Interacion, Aarhus University
 *  
 *    
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
**/

/**
 * Handles managing permissions
 */
class PermissionManager {
    constructor() {
        this.permissions = [];

        this.loadPermissions();

        this.observer = new MutationObserver((mutations)=>{
            this.loadPermissions();
        });

        this.startObserver();
    }

    async executeObserverless(executor) {
        let mutations = this.observer.takeRecords();
        if(mutations.length > 0) {
            this.loadPermissions();
        }
        this.observer.disconnect();

        await executor();

        this.startObserver();
    }

    startObserver() {
        this.observer.observe(document.querySelector("html"), {
            attributes: true,
            attributeFilter: ["data-auth"]
        });
    }

    loadPermissions() {
        let self = this;
        self.permissions = [];

        try {
            const permissionJson = JSON.parse(document.querySelector("html").getAttribute("data-auth"));

            if (permissionJson != null) {
                permissionJson.forEach((perm) => {
                    if (perm.webstrateId != null) {
                        //Inherit permissions
                        console.warn("This webstrate is inheriting permissions (Unsupported) from: ", perm.webstrateId);
                    } else {
                        self.permissions.push(new Permission(perm.username, perm.provider, perm.permissions));
                    }
                });
            }
        } catch (e) {
            console.warn("Failed to parse permissions: ", e);
            alert("Warning: Failed to parse permissions. Saving will overwrite existing malformed permissions. See console for details.");
        }

        this.checkAnonymousUser();

        EventSystem.triggerEvent("PermissionManager.Permissions.Changed", {
            permissions: this.permissions
        });
    }

    checkAnonymousUser() {
        if(this.permissions.length === 0){
            // STUB: We currently have no way of knowing the default permissions on this server so we assume "arw" for anonymous
            this.permissions.push(new Permission("anonymous", "", "arw"));
        }
    }

    setPermission(permission) {
        console.log("Setting permission: ", permission);

        let oldPermission = this.permissions.find((perm)=>{
            return permission.username === perm.username && permission.provider === perm.provider;
        });

        if(oldPermission != null) {
            oldPermission.permissions = permission.permissions;
        } else {
            this.permissions.push(permission);
        }

        this.checkAnonymousUser();

        EventSystem.triggerEvent("PermissionManager.Permissions.Changed", {
            permissions: this.permissions
        });
    }

    removePermission(permission) {
        this.permissions = this.permissions.filter((perm)=>{
            return permission.username !== perm.username || permission.provider !== perm.provider;
        });

        this.checkAnonymousUser();

        EventSystem.triggerEvent("PermissionManager.Permissions.Changed", {
            permissions: this.permissions
        });
    }

    save() {
        if(this.permissions.length === 0 || (this.permissions.length === 1 && this.permissions[0].username === "anonymous")) {
            //Empty permissions, remove auth property, everyone can do anything!
            if(confirm("After saving everyone has full access to this webstrate.\nContinue?")) {
                document.querySelector("html").removeAttribute("data-auth");
                return true;
            } else {
                return false;
            }
        } else {
            //Checks for the current user not locking himself out
            let ourPermissions = this.permissions.find((perm)=>{
                return perm.username === webstrate.user.username && perm.provider === webstrate.user.provider;
            });

            let anonPermissions = this.permissions.find((perm)=>{
                return perm.username === "anonymous" && perm.provider === "";
            });

            let anyAdmin = this.permissions.find((perm)=>{
                return perm.hasPermission("a");
            }) != null;

            if(ourPermissions == null) {
                //If our user does not exist, we are anon
                ourPermissions = anonPermissions;
            }

            let canRead = ourPermissions!=null?ourPermissions.hasPermission("r"):false;
            let canWrite = ourPermissions!=null?ourPermissions.hasPermission("w"):false;
            let canAdmin = ourPermissions!=null?((anyAdmin)?ourPermissions.hasPermission("a"):canWrite):false;

            let permissionsJson = JSON.stringify(this.permissions.map((perm)=>{
                return perm.toJson();
            }));

            if(!canAdmin) {
                if(confirm("You are removing your own admin permission.\nContinue?")) {
                    document.querySelector("html").setAttribute("data-auth", permissionsJson, {approved: true});
                    return true;
                } else {
                    return false;
                }
            } else {
                document.querySelector("html").setAttribute("data-auth", permissionsJson, {approved: true});
                return true;
            }
        }
    }
    
    static isAdmin(){
        return webstrate.user.permissions.includes("a");
    }
}

window.WebstrateComponents.PermissionManager = PermissionManager;

class Permission {
    constructor(username, provider, permissionString) {
        this.username = username;
        this.provider = provider;
        this.permissions = new Set();

        this.loadPermissions(permissionString);
    }

    loadPermissions(permissionString) {
        this.permissions.clear();

        for(let i = 0; i<permissionString.length; i++) {
            let p = permissionString.charAt(i);

            switch(p) {
                case "a":
                    this.permissions.add("a");
                case "w":
                    this.permissions.add("w");
                case "r":
                    this.permissions.add("r");
                    break;

                default:
                    console.warn("Unknown permission: ", p);
            }
        }
    }

    setPermission(perm) {
        this.permissions.clear();

        switch(perm) {
            case "a":
                this.permissions.add("a");
            case "w":
                this.permissions.add("w");
            case "r":
                this.permissions.add("r");
            default:
                console.warn("Unknown permission: ", perm);
        }
    }

    hasPermission(perm) {
        return this.permissions.has(perm);
    }

    toJson() {
        return {
            username: this.username,
            provider: this.provider,
            permissions: Array.from(this.permissions).join("")
        };
    }
}

window.WebstrateComponents.Permission = Permission;

//Load right away
WebstrateComponents.PermissionManager.singleton = new PermissionManager();