From 5f65e7abd7e5e3454c0048bdae11cdf91c19bfb2 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 6 Dec 2023 04:42:18 +0100 Subject: [PATCH] improve protected pointers, inherited read/write permissions --- compiler/compiler.ts | 13 +++++- datex_short.ts | 43 ++++++++++++++++++++ docs/manual/04 Pointer Synchronisation.md | 29 +++++++++++++- js_adapter/js_class_adapter.ts | 13 +++++- network/network_utils.ts | 4 +- runtime/crypto.ts | 27 ++++++++----- runtime/pointers.ts | 48 +++++++++++++++++++++-- runtime/runtime.ts | 20 ++++++++++ types/addressing.ts | 9 +++-- types/errors.ts | 6 ++- utils/history.ts | 2 + utils/interface-generator.ts | 6 ++- 12 files changed, 194 insertions(+), 26 deletions(-) diff --git a/compiler/compiler.ts b/compiler/compiler.ts index 0f0381a8..8a853478 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -1975,7 +1975,7 @@ export class Compiler { // remember if js type def modules should be added to this scope if (SCOPE.addJSTypeDefs == undefined) { - let receiver = Compiler.builder.getScopeReceiver(SCOPE); + const receiver = Compiler.builder.getScopeReceiver(SCOPE); SCOPE.addJSTypeDefs = !!jsTypeDefModule && receiver != Runtime.endpoint && receiver != LOCAL_ENDPOINT; } @@ -2220,6 +2220,14 @@ export class Compiler { return; } + // pointer is sent to receiver, so he gets access (TODO: improve) + if (Runtime.OPTIONS.PROTECT_POINTERS) { + const receiver = Compiler.builder.getScopeReceiver(SCOPE); + if (receiver !== Runtime.endpoint) { + p.grantAccessTo(receiver) + } + } + // pre extract per default if ((SCOPE).extract_pointers && action_type == ACTION_TYPE.GET) { Compiler.builder.insertExtractedVariable(SCOPE, BinaryCode.POINTER, buffer2hex(p.id_buffer)); @@ -2597,7 +2605,7 @@ export class Compiler { serializeValue: (v:any, SCOPE:compiler_scope):any => { if (SCOPE.serialized_values.has(v)) return SCOPE.serialized_values.get(v); else { - let receiver = Compiler.builder.getScopeReceiver(SCOPE); + const receiver = Compiler.builder.getScopeReceiver(SCOPE); const s = Runtime.serializeValue(v, receiver); SCOPE.serialized_values.set(v,s); return s; @@ -2614,6 +2622,7 @@ export class Compiler { } options = options.parent_scope?.options; } + if (!SCOPE.options?.to) SCOPE.options.to = receiver; return receiver; }, diff --git a/datex_short.ts b/datex_short.ts index 3395deb2..620f7610 100644 --- a/datex_short.ts +++ b/datex_short.ts @@ -280,6 +280,37 @@ export function pointer(value:RefOrValue, property?:unknown): unknown { } + +/** + * Add endpoint to allowed_access list + * @param endpoint + */ +export function grantAccess(value: any, endpoint: string|Endpoint) { + const pointer = Pointer.pointerifyValue(value); + if (pointer instanceof Pointer) pointer.grantAccessTo(typeof endpoint == "string" ? f(endpoint as "@") : endpoint) + else throw new Error("Cannot set read permissions for non-pointer value") +} + +/** + * Grant public access for pointer + * @param endpoint + */ +export function grantPublicAccess(value: any) { + const pointer = Pointer.pointerifyValue(value); + if (pointer instanceof Pointer) pointer.grantPublicAccess() + else throw new Error("Cannot set read permissions for non-pointer value") +} + +/** + * Remove endpoint from allowed_access list + * @param endpoint + */ +export function revokeAccess(value: any, endpoint: string|Endpoint) { + const pointer = Pointer.pointerifyValue(value); + if (pointer instanceof Pointer) pointer.revokeAccessFor(typeof endpoint == "string" ? f(endpoint as "@") : endpoint) + else throw new Error("Cannot set read permissions for non-pointer value") +} + export const $$ = pointer; interface $fn { @@ -445,6 +476,10 @@ export async function once(id_or_init:string|(()=>Promise|T), _init?:()=>P const _once = once; type val = typeof val; +type grantAccess = typeof grantAccess +type grantPublicAccess = typeof grantPublicAccess +type revokeAccess = typeof revokeAccess + declare global { const eternal: undefined const lazyEternal: undefined @@ -456,6 +491,10 @@ declare global { const eternalVar: (customIdentifier:string)=>undefined const lazyEternalVar: (customIdentifier:string)=>undefined const once: typeof _once; + + const grantAccess: grantAccess; + const grantPublicAccess: grantPublicAccess; + const revokeAccess: revokeAccess; } @@ -583,6 +622,10 @@ Object.defineProperty(globalThis, 'observeAndInit', {value:Ref.observeAndInit.bi Object.defineProperty(globalThis, 'unobserve', {value:Ref.unobserve.bind(Ref), configurable:false}) Object.defineProperty(globalThis, 'isolate', {value:Ref.disableCapturing.bind(Ref), configurable:false}) +Object.defineProperty(globalThis, 'grantAccess', {value:grantAccess, configurable:false}) +Object.defineProperty(globalThis, 'grantPublicAccess', {value:grantPublicAccess, configurable:false}) +Object.defineProperty(globalThis, 'revokeAccess', {value:revokeAccess, configurable:false}) + // @ts-ignore globalThis.get = get // @ts-ignore diff --git a/docs/manual/04 Pointer Synchronisation.md b/docs/manual/04 Pointer Synchronisation.md index 615672ef..90d34cdb 100644 --- a/docs/manual/04 Pointer Synchronisation.md +++ b/docs/manual/04 Pointer Synchronisation.md @@ -54,7 +54,7 @@ x.val // -> 42 This pointer is now accessible on any other endpoint in the supranet. > [!NOTE] -> Per default, pointers have no read/write restrictions and can be accessed by any endpoint. This can be prevented by defining pointer permissions. This behaviour might also change in the future. +> Per default, pointers have no read/write restrictions and can be accessed by any endpoint. This behaviour can be disabled by setting the [`PROTECT_POINTERS` runtime flag](#protecting-pointers) ### Pointer IDs @@ -114,6 +114,32 @@ x.val // -> 10 Pointer synchronisation does not just work with primitive values, but also with objects, maps, sets, etc. +## Protecting Pointers + +Per default, pointers have no read/write restrictions and can be accessed by any endpoint. +This behaviour will probably change in the future. +To disable default read/write access for all remote endpoints, you can set + +```ts +Datex.Runtime.OPTIONS.PROTECT_POINTERS = true +``` + +Now, pointers are only accessible by remote endpoints if they are explicitly sent to the endpoint from the +origin endpoint. + +You can also explicitly grant access for a pointer to a specific endpoint: + +```ts +const x = $$("private content"); +grantAccess(x, '@user1'); // @user1 can now read/write x +revokeAccess(x, '@user1'); // @user1 can no longer read/write x +``` + +You can also make a pointer publicly accessible by all endpoints: +```ts +grantPublicAccess(x); // any endpoint can now read/write x +``` + ## Global Garbage Collection (GGC) @@ -140,3 +166,4 @@ const x1 = await $.ABCDEF const x2 = await $.ABCDEF assert (x1 === x2) ``` + diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index 9d59931d..02deb8e5 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -889,7 +889,11 @@ function _old_publicStaticClass(original_class:Class) { // set allowed endpoints for this method //static_scope.setAllowedEndpointsForProperty(name, this.method_a_filters.get(name)) - let dx_function = Pointer.proxifyValue(DatexFunction.createFromJSFunction(current_value, original_class, name), true, undefined, false, true) ; // generate + const dx_function = Pointer.proxifyValue(DatexFunction.createFromJSFunction(current_value, original_class, name), true, undefined, false, true) ; // generate + + // public function + const ptr = Pointer.pointerifyValue(dx_function); + if (ptr instanceof Pointer) ptr.grantPublicAccess(true); static_scope.setVariable(name, dx_function); // add to static scope } @@ -897,7 +901,12 @@ function _old_publicStaticClass(original_class:Class) { // field else { // set static value (datexified) - let setProxifiedValue = (val:any) => static_scope.setVariable(name, Pointer.proxifyValue(val, true, undefined, false, true)); + const setProxifiedValue = (val:any) => { + static_scope.setVariable(name, Pointer.proxifyValue(val, true, undefined, false, true)) + // public function + const ptr = Pointer.proxifyValue(val); + if (ptr instanceof Pointer) ptr.grantPublicAccess(true); + }; setProxifiedValue(current_value); /*** handle new value assignments to this property: **/ diff --git a/network/network_utils.ts b/network/network_utils.ts index ed704616..f8b23e3d 100644 --- a/network/network_utils.ts +++ b/network/network_utils.ts @@ -9,7 +9,7 @@ export abstract class NetworkUtils { static _get_keys_data = {scope_name:"network", sign:false, filter:undefined}; static get_keys (endpoint:Endpoint):Promise<[ArrayBuffer, ArrayBuffer]> { if (!this._get_keys) this._get_keys = getProxyFunction("get_keys", this._get_keys_data); - this._get_keys_data.filter = Runtime.main_node ?? Runtime.endpoint + this._get_keys_data.filter = Runtime.main_node return this._get_keys(endpoint) } @@ -19,7 +19,7 @@ export abstract class NetworkUtils { static add_push_channel (channel:string, data:object):Promise { if (!this._add_push_channel) this._add_push_channel = getProxyFunction("add_push_channel", this._add_push_channel_data); - this._add_push_channel_data.filter = Runtime.main_node ?? Runtime.endpoint + this._add_push_channel_data.filter = Runtime.main_node return this._add_push_channel(channel, data) } } \ No newline at end of file diff --git a/runtime/crypto.ts b/runtime/crypto.ts index e11f4efd..7410ef87 100644 --- a/runtime/crypto.ts +++ b/runtime/crypto.ts @@ -138,7 +138,6 @@ export class Crypto { // keys not found, request from network else { return this.requestKeys(endpoint.main) - .catch(() => this.requestKeys(endpoint)) } } @@ -156,18 +155,25 @@ export class Crypto { if (verify_key && !(verify_key instanceof ArrayBuffer)) throw new ValueError("Invalid verify key"); if (enc_key && !(enc_key instanceof ArrayBuffer)) throw new ValueError("Invalid encryption key"); + // always bind to main endpoint + endpoint = endpoint.main; + if (this.public_keys.has(endpoint)) return false; // keys already exist const storage_item_key = "keys_"+endpoint; if (await Storage.hasItem(storage_item_key)) return false; // keys already in storage - try { - this.public_keys.set(endpoint, [ + try { + const keys = [ verify_key ? await Crypto.importVerifyKey(verify_key) : null, enc_key ? await Crypto.importEncKey(enc_key): null - ]) - this.public_keys_exported.set(endpoint, [verify_key, enc_key]); - await Storage.setItem(storage_item_key, [verify_key, enc_key]); + ] as [CryptoKey | null, CryptoKey | null]; + const exportedKeys = [verify_key, enc_key] as [ArrayBuffer, ArrayBuffer]; + + this.public_keys.set(endpoint.main, keys) + this.public_keys_exported.set(endpoint, exportedKeys); + + await Storage.setItem(storage_item_key, exportedKeys); return true; } catch(e) { logger.error(e); @@ -237,7 +243,8 @@ export class Crypto { // convert to CryptoKeys try { const keys:[CryptoKey, CryptoKey] = [await this.importVerifyKey(exported_keys[0])||null, await this.importEncKey(exported_keys[1])||null]; - this.public_keys.set(endpoint, keys); + this.public_keys.set(endpoint.main, keys); + logger.debug("saving keys for " + endpoint); resolve(keys); this.#waiting_key_requests.delete(endpoint); // remove from key promises return; @@ -308,9 +315,9 @@ export class Crypto { private static saveOwnPublicKeysInEndpointKeyMap () { // save in local endpoint key storage - if (!this.public_keys.has(Runtime.endpoint)) this.public_keys.set(Runtime.endpoint, [null,null]); - (<[CryptoKey?, CryptoKey?]>this.public_keys.get(Runtime.endpoint))[0] = this.rsa_verify_key; - (<[CryptoKey?, CryptoKey?]>this.public_keys.get(Runtime.endpoint))[1] = this.rsa_enc_key; + if (!this.public_keys.has(Runtime.endpoint)) this.public_keys.set(Runtime.endpoint.main, [null,null]); + (<[CryptoKey?, CryptoKey?]>this.public_keys.get(Runtime.endpoint.main))[0] = this.rsa_verify_key; + (<[CryptoKey?, CryptoKey?]>this.public_keys.get(Runtime.endpoint.main))[1] = this.rsa_enc_key; } // returns current public verify + encrypt keys diff --git a/runtime/pointers.ts b/runtime/pointers.ts index 41b4b402..390e198a 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file no-namespace -import { Endpoint, endpoints, endpoint_name, IdEndpoint, Person, Target, target_clause, LOCAL_ENDPOINT } from "../types/addressing.ts"; +import { Endpoint, endpoints, endpoint_name, IdEndpoint, Person, Target, target_clause, LOCAL_ENDPOINT, BROADCAST } from "../types/addressing.ts"; import { NetworkError, PermissionError, PointerError, RuntimeError, ValueError } from "../types/errors.ts"; import { Compiler, PrecompiledDXB } from "../compiler/compiler.ts"; import { DX_PTR, DX_REACTIVE_METHODS, DX_VALUE, INVALID, NOT_EXISTING, SET_PROXY, SHADOW_OBJECT, UNKNOWN_TYPE, VOID } from "./constants.ts"; @@ -1453,7 +1453,7 @@ export class Pointer extends Ref { this.loading_pointers.delete(id_string); - // check read permissions (works if PROTECT_POINTERS enabled) + // check read permissions pointer.assertEndpointCanRead(SCOPE?.sender) return pointer; @@ -1807,7 +1807,7 @@ export class Pointer extends Ref { sealed:boolean = false; // can the value be changed from the client side? (otherwise, it can only be changed via DATEX calls) #scheduler: UpdateScheduler|null = null // has fixed update_interval - #allowed_access: target_clause // who has access to this pointer?, undefined = all + #allowed_access?: target_clause // who has access to this pointer?, undefined = all #garbage_collectable = false; #garbage_collected = false; @@ -1852,6 +1852,14 @@ export class Pointer extends Ref { * @returns */ public assertEndpointCanRead(endpoint?: Endpoint) { + // logger.error(this.val) + // console.log("assert " + endpoint, Logical.matches(endpoint, this.allowed_access, Target), !( + // Runtime.OPTIONS.PROTECT_POINTERS + // && !(endpoint == Runtime.endpoint) + // && this.is_origin + // && (!endpoint || !Logical.matches(endpoint, this.allowed_access, Target)) + // && (endpoint && !Runtime.trustedEndpoints.get(endpoint.main)?.includes("protected-pointer-access")) + // ), this.allowed_access) if ( Runtime.OPTIONS.PROTECT_POINTERS && !(endpoint == Runtime.endpoint) @@ -1859,6 +1867,7 @@ export class Pointer extends Ref { && (!endpoint || !Logical.matches(endpoint, this.allowed_access, Target)) && (endpoint && !Runtime.trustedEndpoints.get(endpoint.main)?.includes("protected-pointer-access")) ) { + console.log("inv",new Error().stack) throw new PermissionError("Endpoint has no read permissions for this pointer") } } @@ -1869,6 +1878,37 @@ export class Pointer extends Ref { } + /** + * add endpoint to allowed_access list + * @param endpoint + */ + public grantAccessTo(endpoint: Endpoint, _force = false) { + if (!_force && !Runtime.OPTIONS.PROTECT_POINTERS) throw new Error("Read permissions are not enabled per default (set Datex.Runtime.OPTIONS.PROTECT_POINTERS to true)") + if (!this.#allowed_access || this.#allowed_access == BROADCAST) this.#allowed_access = new Disjunction() + if (this.#allowed_access instanceof Disjunction) this.#allowed_access.add(endpoint) + else throw new Error("Invalid access filter, cannot add endpoint (TODO)") + } + + + /** + * add public access to pointer + * @param endpoint + */ + public grantPublicAccess(_force = false) { + if (!_force && !Runtime.OPTIONS.PROTECT_POINTERS) throw new Error("Read permissions are not enabled per default (set Datex.Runtime.OPTIONS.PROTECT_POINTERS to true)") + this.#allowed_access = BROADCAST; + } + + + /** + * remove endpoint from allowed_access list + * @param endpoint + */ + public revokeAccessFor(endpoint: Endpoint, _force = false) { + if (!_force && !Runtime.OPTIONS.PROTECT_POINTERS) throw new Error("Read permissions are not enabled per default (set Datex.Runtime.OPTIONS.PROTECT_POINTERS to true)") + if (this.#allowed_access instanceof Disjunction) this.#allowed_access.delete(endpoint) + else throw new Error("Invalid access filter, cannot add endpoint (TODO)") + } /** @@ -2557,6 +2597,8 @@ export class Pointer extends Ref { } catch (e) { if (e !== Pointer.WEAK_EFFECT_DISPOSED) console.error(e); + // invalid result, no update + return; } // always cleanup capturing finally { diff --git a/runtime/runtime.ts b/runtime/runtime.ts index e53fb973..bd89ad6e 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -120,6 +120,10 @@ export class StaticScope { private constructor(name?:string){ const proxy = Pointer.proxifyValue(this, false, undefined, false); DatexObject.setWritePermission(>proxy, undefined); // make readonly + + const ptr = Pointer.pointerifyValue(proxy); + ptr.grantPublicAccess(true); + if (name) proxy.name = name; return proxy; } @@ -3026,6 +3030,10 @@ export class Runtime { let el = INNER_SCOPE.active_value; let did_assignment = false; + // make sure endpoint has access (TODO: INNER_SCOPE.active_value should never be set if no access) + const ptr = Pointer.pointerifyValue(el); + if (ptr instanceof Pointer) ptr.assertEndpointCanRead(SCOPE?.sender) + // ptrs if (INNER_SCOPE.waiting_ptrs?.size) { for (const p of INNER_SCOPE.waiting_ptrs) { @@ -3319,6 +3327,7 @@ export class Runtime { const o_parent:Pointer = Pointer.pointerifyValue(parent); if (o_parent instanceof Pointer) o_parent.assertEndpointCanRead(SCOPE?.sender) + key = Ref.collapseValue(key,true,true); // check read permission (throws an error) @@ -4022,6 +4031,11 @@ export class Runtime { async insertToScope(SCOPE:datex_scope, el:any, literal_value = false){ const INNER_SCOPE = SCOPE.inner_scope; + + // check pointer access permission + const pointer = el instanceof Pointer ? el : Pointer.getByValue(el); + if (pointer instanceof Pointer) pointer.assertEndpointCanRead(SCOPE?.sender) + // first make sure pointers are collapsed el = Ref.collapseValue(el) @@ -5091,6 +5105,12 @@ export class Runtime { else INNER_SCOPE.active_value = res; } + // apply / function call return value -> give access permission to current endpoint + const ptr = Pointer.pointerifyValue(INNER_SCOPE.active_value); + if (Runtime.OPTIONS.PROTECT_POINTERS && SCOPE.sender !== Runtime.endpoint && ptr instanceof Pointer) { + ptr.grantAccessTo(SCOPE.sender); + } + } diff --git a/types/addressing.ts b/types/addressing.ts index 03994307..4dbc97e7 100644 --- a/types/addressing.ts +++ b/types/addressing.ts @@ -48,9 +48,12 @@ export class Target implements ValueConsumer { // @implements LogicalComparator static logicalMatch(value: Target, against: Target) { - //console.log("logical match " + value + " against " + against); - // TODO: finish - return (value === against || (value instanceof Endpoint && against instanceof Endpoint && value.equals(against))) + return ( + against === BROADCAST || + value === against || + (value instanceof Endpoint && value.main === against) || + (value instanceof Endpoint && against instanceof Endpoint && value.equals(against)) + ) } // TODO filter diff --git a/types/errors.ts b/types/errors.ts index af44724b..e23ac703 100644 --- a/types/errors.ts +++ b/types/errors.ts @@ -14,9 +14,11 @@ export class Error extends globalThis.Error { constructor(message?:string|number|bigint|null) constructor(message?:string|number|bigint|null, scope?:datex_scope|null) constructor(message?:string|number|bigint, stack?:[Endpoint, string?][]|null) - constructor(message:string|number|bigint|null = '', stack:datex_scope|null|[Endpoint, string?][] = [[Runtime.endpoint]]) { + constructor(message:string|number|bigint|null = '', initialStack?:datex_scope|null|[Endpoint, string?][]) { super(); + const stack = initialStack ?? [[Runtime.endpoint]]; + // extract name from class name this.name = this.constructor.name.replace("Datex",""); @@ -27,7 +29,7 @@ export class Error extends globalThis.Error { this.addScopeToStack(stack) } // // stack already provided (as array) - // else if (Runtime.OPTIONS.ERROR_STACK_TRACES && stack instanceof Array) this.datex_stack = stack; + else if (Runtime.OPTIONS.ERROR_STACK_TRACES && initialStack instanceof Array) this.datex_stack = initialStack; // // no stack // else this.datex_stack = []; diff --git a/utils/history.ts b/utils/history.ts index b7cf15cf..758abce9 100644 --- a/utils/history.ts +++ b/utils/history.ts @@ -34,6 +34,8 @@ export class History { Ref.observe(pointer, (value, key, type, transform, is_child_update, previous) => { + // TODO: group atomic state changes (e.g. splice) + if (type == Pointer.UPDATE_TYPE.BEFORE_DELETE) return; // ignore if (type == Pointer.UPDATE_TYPE.BEFORE_REMOVE) return; // ignore diff --git a/utils/interface-generator.ts b/utils/interface-generator.ts index 2bed3fea..3d64b127 100644 --- a/utils/interface-generator.ts +++ b/utils/interface-generator.ts @@ -130,7 +130,7 @@ function getValueTSCode(module_name:string, name:string, value: any, no_pointer const is_datex_module = module_name.endsWith(".dx") || module_name.endsWith(".dxb") const type = Datex.Type.ofValue(value) - const is_pointer = (value instanceof Datex.Value) || !!(Datex.Pointer.getByValue(value)); + const is_pointer = (value instanceof Datex.Ref) || !!(Datex.Pointer.getByValue(value)); // if (no_pointer) { // // no pointer @@ -183,6 +183,10 @@ function getValueTSCode(module_name:string, name:string, value: any, no_pointer value[BACKEND_EXPORT] = true; } catch {} + + // add public permission + const ptr = Datex.Pointer.pointerifyValue(value); + if (ptr instanceof Datex.Pointer) ptr.grantPublicAccess(true) } // disable garbage collection