refactor: rework permission system

This commit is contained in:
TopchetoEU 2024-01-13 11:05:43 +02:00
parent 48bd1e2015
commit cf99845f6b
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
11 changed files with 330 additions and 171 deletions

View File

@ -7,22 +7,17 @@ public class Filename {
public final String protocol;
public final String path;
public String toString() {
@Override public String toString() {
return protocol + "://" + path;
}
@Override
public int hashCode() {
@Override public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + protocol.hashCode();
result = prime * result + path.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
@ -41,9 +36,6 @@ public class Filename {
return true;
}
public Filename(String protocol, String path) {
path = path.trim();
protocol = protocol.trim();

View File

@ -1,36 +1,38 @@
package me.topchetoeu.jscript.lib;
import java.io.IOException;
import java.io.OutputStream;
import me.topchetoeu.jscript.core.engine.values.Values;
import me.topchetoeu.jscript.utils.filesystem.FilesystemException;
import me.topchetoeu.jscript.utils.filesystem.FilesystemException.FSCode;
import me.topchetoeu.jscript.utils.filesystem.File;
import me.topchetoeu.jscript.utils.interop.Arguments;
import me.topchetoeu.jscript.utils.interop.Expose;
import me.topchetoeu.jscript.utils.interop.WrapperName;
@WrapperName("Console")
public class ConsoleLib {
private final OutputStream stream;
public static interface Writer {
void writeLine(String val) throws IOException;
}
private File file;
@Expose
public void __log(Arguments args) {
try {
var first = true;
for (var el : args.args) {
if (!first) stream.write(" ".getBytes());
first = false;
stream.write(Values.toReadable(args.ctx, el).getBytes());
}
stream.write((byte)'\n');
var res = new StringBuilder();
var first = true;
for (var el : args.args) {
if (!first) res.append(" ");
first = false;
res.append(Values.toReadable(args.ctx, el).getBytes());
}
catch (IOException e) {
throw new FilesystemException("stdout", FSCode.NO_PERMISSIONS_RW);
for (var line : res.toString().split("\n", -1)) {
file.write(line.getBytes());
}
}
public ConsoleLib(OutputStream stream) {
this.stream = stream;
public ConsoleLib(File file) {
this.file = file;
}
}

View File

@ -0,0 +1,7 @@
package me.topchetoeu.jscript.utils;
import java.io.IOException;
public interface LineWriter {
void writeLine(String value) throws IOException;
}

View File

@ -1,6 +1,14 @@
package me.topchetoeu.jscript.utils.filesystem;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import me.topchetoeu.jscript.common.Buffer;
import me.topchetoeu.jscript.utils.LineWriter;
import me.topchetoeu.jscript.utils.filesystem.FilesystemException.FSCode;
import me.topchetoeu.jscript.utils.permissions.Permission;
import me.topchetoeu.jscript.utils.permissions.PermissionsProvider;
public interface File {
int read(byte[] buff);
@ -8,6 +16,29 @@ public interface File {
long seek(long offset, int pos);
void close();
default File wrap(String name, PermissionsProvider perms, Permission read, Permission write, Permission seek, Permission close) {
var self = this;
return new File() {
@Override public int read(byte[] buff) {
if (read != null && perms.hasPermission(read, name)) return self.read(buff);
else throw new FilesystemException(name, FSCode.NO_PERMISSIONS_R);
}
@Override public void write(byte[] buff) {
if (write != null && perms.hasPermission(write, name)) self.write(buff);
else throw new FilesystemException(name, FSCode.NO_PERMISSIONS_RW);
}
@Override public long seek(long offset, int pos) {
if (seek != null && perms.hasPermission(seek, name)) return self.seek(offset, pos);
else throw new FilesystemException(name, FSCode.NO_PERMISSIONS_R);
}
@Override public void close() {
if (close != null && perms.hasPermission(close, name)) self.close();
else throw new FilesystemException(name, FSCode.NO_PERMISSIONS_R);
}
};
}
default String readToString() {
long len = seek(0, 2);
if (len < 0) return null;
@ -34,4 +65,75 @@ public interface File {
}
return new String(res.data());
}
public static File ofStream(String name, InputStream str) {
return new File() {
@Override public int read(byte[] buff) {
try {
return str.read(buff);
}
catch (IOException e) {
throw new FilesystemException(name, FSCode.NO_PERMISSIONS_R);
}
}
@Override public void write(byte[] buff) {
throw new FilesystemException(name, FSCode.NO_PERMISSIONS_RW);
}
@Override public long seek(long offset, int pos) {
throw new FilesystemException(name, FSCode.UNSUPPORTED_OPERATION);
}
@Override public void close() {
throw new FilesystemException(name, FSCode.UNSUPPORTED_OPERATION);
}
};
}
public static File ofStream(String name, OutputStream str) {
return new File() {
@Override public int read(byte[] buff) {
throw new FilesystemException(name, FSCode.NO_PERMISSIONS_R);
}
@Override public void write(byte[] buff) {
try {
str.write(buff);
}
catch (IOException e) {
throw new FilesystemException(name, FSCode.NO_PERMISSIONS_RW);
}
}
@Override public long seek(long offset, int pos) {
throw new FilesystemException(name, FSCode.UNSUPPORTED_OPERATION);
}
@Override public void close() {
throw new FilesystemException(name, FSCode.UNSUPPORTED_OPERATION);
}
};
}
public static File ofLineWriter(String name, LineWriter writer) {
var buff = new Buffer();
return new File() {
@Override public int read(byte[] buff) {
throw new FilesystemException(name, FSCode.NO_PERMISSIONS_R);
}
@Override public void write(byte[] val) {
for (var b : val) {
if (b == '\n') {
try {
writer.writeLine(new String(buff.data()));
}
catch (IOException e) {
throw new FilesystemException(name, FSCode.NO_PERMISSIONS_RW);
}
}
else buff.append(b);
}
}
@Override public long seek(long offset, int pos) {
throw new FilesystemException(name, FSCode.UNSUPPORTED_OPERATION);
}
@Override public void close() {
throw new FilesystemException(name, FSCode.UNSUPPORTED_OPERATION);
}
};
}
}

View File

@ -5,6 +5,7 @@ import java.util.Map;
import me.topchetoeu.jscript.common.Filename;
import me.topchetoeu.jscript.utils.filesystem.FilesystemException.FSCode;
import me.topchetoeu.jscript.utils.permissions.Matcher;
import me.topchetoeu.jscript.utils.permissions.PermissionsProvider;
public class RootFilesystem implements Filesystem {
@ -12,10 +13,10 @@ public class RootFilesystem implements Filesystem {
public final PermissionsProvider perms;
private boolean canRead(String _path) {
return perms.hasPermission("jscript.file.read:" + _path, '/');
return perms.hasPermission("jscript.file.read:" + _path, Matcher.fileWildcard());
}
private boolean canWrite(String _path) {
return perms.hasPermission("jscript.file.write:" + _path, '/');
return perms.hasPermission("jscript.file.write:" + _path, Matcher.fileWildcard());
}
private void modeAllowed(String _path, Mode mode) throws FilesystemException {

View File

@ -0,0 +1,24 @@
package me.topchetoeu.jscript.utils.filesystem;
import me.topchetoeu.jscript.core.engine.Extensions;
import me.topchetoeu.jscript.core.engine.values.Symbol;
import me.topchetoeu.jscript.core.exceptions.EngineException;
public class Stdio {
public static final Symbol STDIN = Symbol.get("IO.stdin");
public static final Symbol STDOUT = Symbol.get("IO.stdout");
public static final Symbol STDERR = Symbol.get("IO.stderr");
public static File stdout(Extensions exts) {
if (exts.hasNotNull(STDOUT)) return exts.get(STDOUT);
else throw EngineException.ofError("stdout is not supported.");
}
public static File stdin(Extensions exts) {
if (exts.hasNotNull(STDIN)) return exts.get(STDIN);
else throw EngineException.ofError("stdin is not supported.");
}
public static File stderr(Extensions exts) {
if (exts.hasNotNull(STDERR)) return exts.get(STDERR);
else throw EngineException.ofError("stderr is not supported.");
}
}

View File

@ -0,0 +1,69 @@
package me.topchetoeu.jscript.utils.permissions;
import java.util.LinkedList;
public interface Matcher {
static class State {
public final int predI, trgI, wildcardI;
public final boolean wildcard;
@Override
public String toString() {
return String.format("State [pr=%s;trg=%s;wildN=%s;wild=%s]", predI, trgI, wildcardI, wildcard);
}
public State(int predicateI, int targetI, int wildcardI, boolean wildcard) {
this.predI = predicateI;
this.trgI = targetI;
this.wildcardI = wildcardI;
this.wildcard = wildcard;
}
}
boolean match(String predicate, String value);
public static Matcher fileWildcard() {
return (predicate, value) -> execWildcard(predicate, value, '/');
}
public static Matcher namespaceWildcard() {
return (predicate, value) -> execWildcard(predicate, value, '.');
}
public static Matcher wildcard() {
return (predicate, value) -> execWildcard(predicate, value, '\0');
}
public static boolean execWildcard(String predicate, String target, char delim) {
if (predicate.equals("")) return target.equals("");
var queue = new LinkedList<State>();
queue.push(new State(0, 0, 0, false));
while (!queue.isEmpty()) {
var state = queue.poll();
var predEnd = state.predI >= predicate.length();
if (state.trgI >= target.length()) return predEnd;
var predC = predEnd ? 0 : predicate.charAt(state.predI);
var trgC = target.charAt(state.trgI);
if (state.wildcard) {
if (state.wildcardI == 2 || trgC != delim) {
queue.add(new State(state.predI, state.trgI + 1, state.wildcardI, true));
}
queue.add(new State(state.predI, state.trgI, 0, false));
}
else if (predC == '*') {
queue.add(new State(state.predI + 1, state.trgI, state.wildcardI + 1, false));
}
else if (state.wildcardI > 0) {
if (state.wildcardI > 2) throw new IllegalArgumentException("Too many sequential stars.");
queue.add(new State(state.predI, state.trgI, state.wildcardI, true));
}
else if (!predEnd && (predC == '?' || predC == trgC)) {
queue.add(new State(state.predI + 1, state.trgI + 1, 0, false));
}
}
return false;
}
}

View File

@ -1,124 +1,19 @@
package me.topchetoeu.jscript.utils.permissions;
import java.util.LinkedList;
public class Permission {
private static class State {
public final int predI, trgI, wildcardI;
public final boolean wildcard;
@Override
public String toString() {
return String.format("State [pr=%s;trg=%s;wildN=%s;wild=%s]", predI, trgI, wildcardI, wildcard);
}
public State(int predicateI, int targetI, int wildcardI, boolean wildcard) {
this.predI = predicateI;
this.trgI = targetI;
this.wildcardI = wildcardI;
this.wildcard = wildcard;
}
}
public final String namespace;
public final String value;
public final Matcher matcher;
public boolean match(Permission perm) {
if (!Permission.match(namespace, perm.namespace, '.')) return false;
if (value == null || perm.value == null) return true;
return Permission.match(value, perm.value);
}
public boolean match(Permission perm, char delim) {
if (!Permission.match(namespace, perm.namespace, '.')) return false;
if (value == null || perm.value == null) return true;
return Permission.match(value, perm.value, delim);
@Override public String toString() {
return namespace;
}
public boolean match(String perm) {
return match(new Permission(perm));
public Permission(String namespace, Matcher matcher) {
this.namespace = namespace;
this.matcher = matcher;
}
public boolean match(String perm, char delim) {
return match(new Permission(perm), delim);
}
@Override
public String toString() {
if (value != null) return namespace + ":" + value;
else return namespace;
}
public Permission(String raw) {
var i = raw.indexOf(':');
if (i > 0) {
value = raw.substring(i + 1);
namespace = raw.substring(0, i);
}
else {
value = null;
namespace = raw;
}
}
public static boolean match(String predicate, String target, char delim) {
if (predicate.equals("")) return target.equals("");
var queue = new LinkedList<State>();
queue.push(new State(0, 0, 0, false));
while (!queue.isEmpty()) {
var state = queue.poll();
var predEnd = state.predI >= predicate.length();
if (state.trgI >= target.length()) return predEnd;
var predC = predEnd ? 0 : predicate.charAt(state.predI);
var trgC = target.charAt(state.trgI);
if (state.wildcard) {
if (state.wildcardI == 2 || trgC != delim) {
queue.add(new State(state.predI, state.trgI + 1, state.wildcardI, true));
}
queue.add(new State(state.predI, state.trgI, 0, false));
}
else if (predC == '*') {
queue.add(new State(state.predI + 1, state.trgI, state.wildcardI + 1, false));
}
else if (state.wildcardI > 0) {
if (state.wildcardI > 2) throw new IllegalArgumentException("Too many sequential stars.");
queue.add(new State(state.predI, state.trgI, state.wildcardI, true));
}
else if (!predEnd && (predC == '?' || predC == trgC)) {
queue.add(new State(state.predI + 1, state.trgI + 1, 0, false));
}
}
return false;
}
public static boolean match(String predicate, String target) {
if (predicate.equals("")) return target.equals("");
var queue = new LinkedList<State>();
queue.push(new State(0, 0, 0, false));
while (!queue.isEmpty()) {
var state = queue.poll();
if (state.predI >= predicate.length() || state.trgI >= target.length()) {
return state.predI >= predicate.length() && state.trgI >= target.length();
}
var predC = predicate.charAt(state.predI);
var trgC = target.charAt(state.trgI);
if (predC == '*') {
queue.add(new State(state.predI, state.trgI + 1, state.wildcardI, true));
queue.add(new State(state.predI + 1, state.trgI, 0, false));
}
else if (predC == '?' || predC == trgC) {
queue.add(new State(state.predI + 1, state.trgI + 1, 0, false));
}
}
return false;
this(raw, null);
}
}

View File

@ -0,0 +1,44 @@
package me.topchetoeu.jscript.utils.permissions;
public class PermissionPredicate {
public final String namespace;
public final String value;
public final boolean denies;
public boolean match(Permission permission, String value) {
if (!match(permission)) return false;
if (this.value == null || value == null) return true;
if (permission.matcher == null) return true;
else return permission.matcher.match(this.value, value);
}
public boolean match(Permission permission) {
return Matcher.namespaceWildcard().match(namespace, permission.namespace);
}
@Override
public String toString() {
if (value != null) return namespace + ":" + value;
else return namespace;
}
public PermissionPredicate(String raw) {
raw = raw.trim();
if (raw.startsWith("!")) {
denies = true;
raw = raw.substring(1).trim();
}
else denies = false;
var i = raw.indexOf(':');
if (i > 0) {
value = raw.substring(i + 1);
namespace = raw.substring(0, i);
}
else {
value = null;
namespace = raw;
}
}
}

View File

@ -1,34 +1,59 @@
package me.topchetoeu.jscript.utils.permissions;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class PermissionsManager implements PermissionsProvider {
public static final PermissionsProvider ALL_PERMS = new PermissionsManager().add(new Permission("**"));
public final ArrayList<PermissionPredicate> predicates = new ArrayList<>();
public final ArrayList<Permission> allowed = new ArrayList<>();
public final ArrayList<Permission> denied = new ArrayList<>();
public PermissionsProvider add(Permission perm) {
allowed.add(perm);
public PermissionsProvider add(PermissionPredicate perm) {
predicates.add(perm);
return this;
}
public PermissionsProvider add(String perm) {
allowed.add(new Permission(perm));
predicates.add(new PermissionPredicate(perm));
return this;
}
@Override
public boolean hasPermission(Permission perm, char delim) {
for (var el : denied) if (el.match(perm, delim)) return false;
for (var el : allowed) if (el.match(perm, delim)) return true;
@Override public boolean hasPermission(Permission perm, String value) {
for (var el : predicates) {
if (el.match(perm, value)) {
if (el.denies) return false;
else return true;
}
}
return false;
}
@Override
public boolean hasPermission(Permission perm) {
for (var el : denied) if (el.match(perm)) return false;
for (var el : allowed) if (el.match(perm)) return true;
@Override public boolean hasPermission(Permission perm) {
for (var el : predicates) {
if (el.match(perm)) {
if (el.denies) return false;
else return true;
}
}
return false;
}
public PermissionsProvider addFromStream(InputStream stream) throws IOException {
var reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
var i = line.indexOf('#');
if (i >= 0) line = line.substring(0, i);
line = line.trim();
if (line.isEmpty()) continue;
add(line);
}
return this;
}
}

View File

@ -5,27 +5,25 @@ import me.topchetoeu.jscript.core.engine.values.Symbol;
public interface PermissionsProvider {
public static final Symbol ENV_KEY = new Symbol("Environment.perms");
public static final PermissionsProvider ALL_PERMS = (perm, value) -> true;
boolean hasPermission(Permission perm, char delim);
boolean hasPermission(Permission perm);
boolean hasPermission(Permission perm, String value);
default boolean hasPermission(String perm, char delim) {
return hasPermission(new Permission(perm), delim);
default boolean hasPermission(Permission perm) {
return hasPermission(perm, null);
}
default boolean hasPermission(String perm) {
return hasPermission(new Permission(perm));
default boolean hasPermission(String perm, String value, Matcher matcher) {
return hasPermission(new Permission(perm, matcher), value);
}
default boolean hasPermission(String perm, Matcher matcher) {
return hasPermission(new Permission(perm, matcher));
}
public static PermissionsProvider get(Extensions exts) {
return new PermissionsProvider() {
@Override public boolean hasPermission(Permission perm) {
if (exts.hasNotNull(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm);
else return true;
}
@Override public boolean hasPermission(Permission perm, char delim) {
if (exts.hasNotNull(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm, delim);
else return true;
}
return (perm, value) -> {
if (exts.hasNotNull(ENV_KEY)) return ((PermissionsProvider)exts.get(ENV_KEY)).hasPermission(perm);
else return true;
};
}
}