Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/gatepass/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Gatepass -- A permission plugin.
* This plugin is part of the bricklib project.
*/

import * as bricklib from '../bricklib/index.js';
import { Player } from '@minecraft/server';
import { checkPerm } from './permission.js';


export * from './permission.js';

/* make this plugin discoverable */
bricklib.plugin.newPlugin('gatepass', () => {
/* no-op */
});


/**
* Returns an array of permission tags a player has.
* @param plr The player.
* @returns Permission tags of the player.
*/
export function getPermTags(plr: Player): string[]
{
return plr
.getTags()?.filter(t => t.startsWith('p:')).map(v => v.slice(2)) ?? [];
}

/**
* Check if a player has a certain permission.
* @param perm The permission to check.
* @param plr The player to check for.
* @returns True if the player has permission.
*/
export function hasPermission(perm: string, plr: Player): boolean
{
return checkPerm(perm, getPermTags(plr));
}

/**
* Asserts if a player has permission.
* @param perm The perm node.
* @param plr The player.
* @throws This will throw an error if the player doesn't have permission.
*/
export function assertPermission(perm: string, plr: Player): void
{
if (!hasPermission(perm, plr))
throw 'gatepass: no permission: ' + perm;
}
157 changes: 157 additions & 0 deletions src/gatepass/permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Base permission parsing and checking.
*/

/*
Priority levels (e.g.: a.b.c):
1. negated exact match (-a.b.c)
2. exact match (a.b.c)
3. negated close match (-a.b.*)
4. close match (a.b.*)
5. more specific negated ancestor (-a.b)
6. more specific ancestor (a.b)
7. less specific negated ancestor (-a.*)
8. less specific ancestor (a.*)
*/


/**
* Check if the permission node `perm` is allowed given the constraints in
* `tagList`.
* @param perm The perm node.
* @param tagList Array of perm tags.
* @returns True when matched, false otherwise.
*/
export function checkPerm(perm: string, tagList: string[]): boolean
{
const permLvls = splitPermNodeLvls(perm);
const permDepth = getPermDepth(perm);

let lastSpec = 0;
let lastMatched = false;

for (const tag of tagList) {
const tagToks = tokenizePermTag(tag);
const negate = tagToks[0] == '-';

if (!matchSpecs(permLvls, tagToks))
continue; /* unrelated tag (a.c.* has nothing to do with a.x.b) */

/* exact match */
if (!hasWildcard(tag) && negate && permDepth == getPermDepth(tag))
return false;

const tagSpec = getSpecificity(tagToks);
if (tagSpec < lastSpec)
continue;

lastMatched = tagSpec == lastSpec
? (negate ? false : lastMatched) : !negate;
lastSpec = tagSpec;
}

return lastMatched;
}

/**
* Match permission node `perm` with tag `tag`, regardless of the
* negation flag. This can match inheritance and wildcards.
* @param perm The split-ed perm node.
* @param tag The tokenized perm tag.
* @returns True if match, false otherwise.
*/
export function matchSpecs(perm: string[], tag: string[]): boolean
{
if (['+', '-'].includes(tag[0]))
tag = tag.slice(1);
if (tag.length > perm.length)
return false;

for (let i = 0; i < tag.length; i++) {
const permLvl = perm[i];
const tagLvl = tag[i];

if (tagLvl == '*')
continue;
if (permLvl != tagLvl)
return false;
}

return true;
}

/**
* Check whether a permission tag has a wildcard.
* @param tag The perm tag.
* @returns True if tag includes a '*'.
*/
export function hasWildcard(tag: string): boolean
{
return /(^[+-]?|\.)\*(\.|$)/.test(tag);
}

/**
* Returns the specificity of a permission tag.
* @param perm The tokenized perm tag.
* @returns The specificity.
*/
export function getSpecificity(perm: string[]): number
{
if (['+', '-'].includes(perm[0]))
perm = perm.slice(1);
let val = 0;

/* 'a.*' > '*.b' */
perm.forEach((lvl, idx) => {
if (lvl == '*')
val += (idx + 1) / (perm.length + 1);
else
val += (perm.length + 1) / (idx + 1);
});

return val;
}

/**
* Returns the number of levels a permission node has.
* @param perm The perm node.
* @returns The number of levels.
*/
export function getPermDepth(perm: string): number
{
let num = 1;
for (const c of perm)
if (c == '.')
num++;
return num;
}

/**
* Tokenize a permission tag.
* @param perm The perm tag.
* @returns An array of perm tag tokens.
*/
export function tokenizePermTag(perm: string): string[]
{
let result: string[] = [];

if ('+-'.includes(perm[0])) {
result = [ perm[0] ];
perm = perm.slice(1);
}

return result.concat(perm.split('.'));
}

/**
* Split a permission node's levels.
* @param perm The perm node.
* @returns An array of perm levels.
* @throws This can throw errors.
*/
export function splitPermNodeLvls(perm: string): string[]
{
if (hasWildcard(perm))
throw new Error('Permission nodes cannot have wildcards');
return perm.split('.');
}
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { FormCancelationReason } from '@minecraft/server-ui';
import * as bricklib from './bricklib/index.js';
import * as gatepass from './gatepass/index.js';

/* load plugins */
bricklib.plugin.loadPlugin('gatepass');


const mgr = new bricklib.command.CommandManager();
bricklib.command.enableCustomChatCmds(mgr, '\\');



mgr.registerCommand([ 'hello', 'hi' ], (src, args) => {
gatepass.assertPermission('basic.hello', src);
src.sendMessage(src.name + '\n' + args.join('\n') + '\nEND OF ARGS');
return 0;
});
Expand All @@ -23,6 +29,7 @@ const def = {
};

mgr.registerCommand(...bricklib.args.makeCommand(def, (args, src) => {
gatepass.assertPermission('chat.echo', src);
src.sendMessage(args.text.join(' '));
return 0;
}));
Expand All @@ -37,6 +44,7 @@ nextTick(() => {


mgr.registerCommand([ 'form' ], (src) => {
gatepass.assertPermission('dev.bricklib.form-cmd', src);
src.sendMessage('please close chats');
nextTick(() => showForm('action-frm', src));
return 0;
Expand Down