AS3W – ACCESS STRING PERMISSIONS

AS3W (Access String: Who, What, When) is a lightweight authorization protocol for associating access rules directly with data stored in a database.

Each data records in the database can contain an Access String describing:

  • who can access (user IDs and/or group IDs),
  • what actions are allowed,
  • when the access expires.

Authorization is performed by:

  1. Generating a normalized Access Request String based on the requesting user.
  2. Evaluating the Access Request Sting against the stored Access String using regex pattern-matching.

Access String

[RULENAME]\[USERS:@<ID>,@<ID>]|[GROUPS:@<ID>,@<ID>]\[ACTION:@<ACTION>,@<ACTION>]\[UNTIL:<TIMESTAMP>]\[COMMENT]

Fields

FieldDescription
USERSList of users allowed (e.g. users:@id1,@id2)
GROUPSList of groups allowed
ACTIONAllowed actions (e.g. data:read, data:write, permission:delete)
UNTILExpiration timestamp (epoch ms or s). Optional.
[RULENAME]Optional rule name
[COMMENT]Optional rule comment

Example

MyRule1\users:@e8f58e5f85e8f58\action:@read\until:176427694
users:@87844545445\groups:@devops\action:@read\until:176899568\just another rule
groups:@devops\action:@read\until:1764271971

Access Request String

The Access Request String is a compact expression of:

  • user’s ID(s),
  • user’s groups,
  • requested action,
  • current timestamp.

Example Access Request:

users:@e8f58e5f85e8f58|groups:@devops,admin\action:@read\until:176427694

Authorization Logic (Regex)

To determine access:

AS3W_VERIFY( access_string , access_request_string )

AS3W_VERIFY will return true if the access_request_string match the access_string, else it will return false

function esc(str) {
    return String(str)
        .replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")
        .trim();
}

async function _time_regex_filter(ts) {
    const digits = String(ts).split('').map(Number);
    let regex = '';

    for (let i = 0; i < digits.length; i++) {
        const n = digits[i];
        const L = digits.length - i - 1;
        regex += n < 9
            ? `([${n+1}-9]\\d{${L}}|${n}`
            : `(${n}`;
    }
    return regex + ")))))))))";
}



async function AS3W_ACCESS_STRING(users=[], groups=[], actions=[], time=null) {
    //TODO check that # is not par of users, groyps or actions.
    users = users.join(",#");
    groups = groups.join(",#");
    actions = actions.join(",#");
    
    if(users.includes("\\")){
        throw new Error("users include a forbidden character");
    }
    
    if(groups.includes("\\")){
        throw new Error("groups include a forbidden character");
    }
    
    if(actions.includes("\\")){
        throw new Error("actions include a forbidden character");
    }
    
    users = users ? `users:#${users}` : "";
    groups = groups ? `groups:#${groups}` : "";
    actions = actions ? `action:#${actions}` : "";
    time = time ? `until:${esc(time)}` : "";

    return [users, groups, actions, time]
        .filter(x => x !== "")
        .join("\\");
}


/**
 * Build the REGEX used to CHECK ACCESS
 */
async function AS3W_ACCESS_REQUEST(users = [], groups = [], actions = [], time = null) {
    // Sanitize arrays
    const escUsers = users.map(u => esc(u));
    const escGroups = groups.map(g => esc(g));
    const escActions = actions.map(a => esc(a));

    // Build alternation groups: (u1|u2|u3)
    const userAlternatives = escUsers.join("|") || "\\*";
    const groupAlternatives = escGroups.join("|") || "\\*";
    const actionAlternatives = escActions.join("|") || "\\*";

    let timeRegex = "";
    if (time) {
        timeRegex = await _time_regex_filter(time);
    }

    const regex =
        "^" +
        "([^\\\\]*\\\\)*" + // allow rule name + prefixes
        "(" +
            "users:.*#(" + userAlternatives + "|\\*)(,.*|\\b)" +
            "(\\\\groups:.*)*|(users:.*\\\\)*" +
            "groups:.*#(" + groupAlternatives + "|\\*)(,.*|\\b)" +
        ")" +
        "\\\\action:.*#(" + actionAlternatives + "|\\*)(,.*|\\b)" +
        (time
            ? "\\\\until:(" + timeRegex + ")"
            : "(\\\\until:(" + timeRegex + "))?") +
        "\\s*$";

    return regex;
}


async function AS3W_VERIFY(accessString, requestString) {
    // 1. Normalize stored Access String (remove spaces, trim)
    const normalizedAS = String(accessString).trim().replace(/\s+/g, "");

    // 3. Build regex object
    let requestRegex;
    try {
        requestRegex = new RegExp(requestString);
    } catch (err) {
        console.error("Invalid AS3W regex:", requestRegexString);
        return false;
    }

    // 4. Perform match
    return requestRegex.test(normalizedAS);
}






(async () => {
    
    
    //const stored = "users:@e8f58e5f85e8f58\\action:@read\\until:176427694";
    
    const access_string = await AS3W_ACCESS_STRING(
        ["[email protected]","[email protected]"],
        ["admin", "devops"],
        ["read","delete"],
        176427694
    )
    console.log(access_string);
    
    const access_request = await AS3W_ACCESS_REQUEST(
        ["[email protected]"],
        ["user"],
        ["write"],
        176427694
    )
    console.log(access_request);
    
    
    const ok = await AS3W_VERIFY(
        access_string,
        access_request
    );

    console.log("ACCESS GRANTED:", ok);
})();