/** */ type type_conf = { server : { host : string; port : int; }; authentication : { timestamp_tolerance : float; hash_salt : string; }; authelia : { usersfile_path : string; }; }; /** */ type type_user_list_sparse = ( null | { users : Record< string, { disabled ?: boolean; displayname ?: string; email ?: string; groups ?: Array; password : string; } >; } ); /** */ type type_user_list_complete = { users : Record< string, { disabled : boolean; displayname : string; email : string; groups : Array; password : string; } >; }; /** */ async function conf_get( path : string ) : Promise { const raw : any = lib_plankton.json.decode(await lib_plankton.file.read(path));; const version_fallback : int = 1; if (! ("version" in raw)) { lib_plankton.log.warning( "conf_version_unspecified", { "assumed_version": version_fallback, } ); } else { // do nothing } const version : int = (raw["version"] ?? version_fallback); switch (version) { default: { throw (new Error("invalid version: " + version.toFixed(0))); break; } case 1: { return { "server": { "host": ((raw["server"] ?? {})["host"] ?? "::"), "port": ((raw["server"] ?? {})["port"] ?? 7463), }, "authentication": { "timestamp_tolerance": (raw["authentication"]["timestamp_tolerance"] ?? 2.0), "hash_salt": raw["authentication"]["hash_salt"], // required }, "authelia": { "usersfile_path": ((raw["authelia"] ?? {})["usersfile_path"] ?? "/var/authelia/users.yaml"), }, }; break; } } } /** */ function encode_user_list( user_list : (null | type_user_list_sparse) ) : string { let output : string = ""; output += "users:\n"; Object.entries((user_list === null) ? {} : user_list.users).forEach( ([key, value]) => { output += (" " + key + ":\n"); output += (" " + "disabled: " + ((value.disabled ?? false) ? "true" : "false") + "\n"); output += (" " + "displayname: " + (value.displayname ?? key) + "\n"); if ("email" in value) { output += (" " + "email: " + value.email + "\n"); } else { // do nothing } output += (" " + "groups: " + JSON.stringify(value.groups ?? []) + "\n"); output += (" " + "password: " + value.password + "\n"); } ); return output; } /** */ async function main( args_raw : Array ) : Promise { lib_plankton.log.conf_push( [ lib_plankton.log.channel_make( { "kind": "stdout", "data": { "threshold": "info", // "format": "human_readable", } } ), ] ); // args const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler({ "conf_path": lib_plankton.args.class_argument.volatile({ "indicators_long": ["conf_path"], "indicators_short": ["c"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "conf.json", "info": "path to conf file", "name": "conf-path", }), "help": lib_plankton.args.class_argument.volatile({ "indicators_long": ["help"], "indicators_short": ["h"], "type": lib_plankton.args.enum_type.boolean, "mode": lib_plankton.args.enum_mode.replace, "default": false, "info": "only show help and exit", "name": "help", }), }); const args : Record = arg_handler.read(lib_plankton.args.enum_environment.cli, args_raw.join(" ")); const conf : type_conf = await conf_get(args.conf_path); // exec const rest_subject : lib_plankton.rest.type_rest = lib_plankton.rest.make( { "title": "authelia-callbacks", "versioning_method": "header", "versioning_header_name": "X-Api-Version", "set_access_control_headers": true, "authentication": { "kind": "key_header", "parameters": {"name": "X-Session-Key"} }, } ); { lib_plankton.rest.register< null, string > ( rest_subject, lib_plankton.http.enum_method.get, "/meta/ping", { "description": "sendet ein 'pong' zurück; gedacht um die Erreichbarkeit des Backends zu prüfen", "input_schema": () => ({ "nullable": true, }), "output_schema": () => ({ "nullable": false, "type": "string", }), // "restriction": restriction_none, "execution": () => { return Promise.resolve({ "status_code": 200, "data": "pong", }); }, } ); lib_plankton.rest.register< null, any > ( rest_subject, lib_plankton.http.enum_method.get, "/meta/spec", { "description": "gibt die API-Spezifikation im OpenAPI-Format aus", "input_schema": () => ({ "nullable": true, }), "output_schema": () => ({ }), // "restriction": restriction_none, "execution": () => { return Promise.resolve({ "status_code": 200, "data": lib_plankton.rest.to_oas(rest_subject), }); }, } ); lib_plankton.rest.register< type_user_list_sparse, null >( rest_subject, lib_plankton.http.enum_method.put, "/users/set", { "description": "setzt die Nutzerliste", "query_parameters": [ { "name": "timestamp", "required": true, "description": "current UNIX timestamp", }, { "name": "auth", "required": true, "description": "authorization string", }, ], "input_schema": () => ({ "nullable": true, "type": "object", "additionalProperties": false, "properties": { "users": { "nullable": true, "type": "object", "additionalProperties": { "nullable": false, "type": "object", "additionalProperties": false, "properties": { "disabled": { "nullable": false, "type": "boolean", }, "displayname": { "nullable": false, "type": "string", }, "email": { "nullable": false, "type": "string", }, "groups": { "nullable": false, "type": "array", "items": { "nullable": false, "type": "string" } }, "password": { "nullable": false, "type": "string", }, }, "required": [ "password", ] }, "properties": {}, "required": [], } }, "required": [ "users" ] }), "output_schema": () => ({ "nullable": true, }), "restriction": async (stuff) => { const timestamp_local : float = lib_plankton.base.get_current_timestamp(); const timestamp_remote : float = parseFloat(stuff.query_parameters["timestamp"]); if (Math.abs(timestamp_local - timestamp_remote) > conf.authentication.timestamp_tolerance) { lib_plankton.log.notice( "restriction_access_denied_due_to_invalid_timestamp", { "timestamp_local": timestamp_local, "timestamp_remote": timestamp_remote, } ); return false; } else { const authhash_is : string = stuff.query_parameters["auth"]; const authhash_shall : string = lib_plankton.sha256.get( timestamp_remote.toFixed(0) + conf.authentication.hash_salt ); if (authhash_is !== authhash_shall) { lib_plankton.log.notice( "restriction_access_denied_due_to_mismatching_hashes", { "timestamp": timestamp_remote, "authhash_is": authhash_is, "authhash_shall": authhash_shall, } ); return false; } else { return true; } } }, "execution": async (stuff) => { await lib_plankton.file.write( conf.authelia.usersfile_path, encode_user_list(stuff.input) ); lib_plankton.log.notice( "userdata_updated", { } ); return Promise.resolve({ "status_code": 200, "data": null }); }, } ); } const server : lib_plankton.server.type_subject = lib_plankton.server.make( async (input, metadata) => { const http_request : lib_plankton.http.type_request = lib_plankton.http.decode_request(input.toString()); return ( lib_plankton.rest.call( rest_subject, http_request ) .catch( (error) => { lib_plankton.log.error( "server_request_processing_failed", { "error": String(error), } ); return Promise.resolve( { "version": "HTTP/1.1", "status_code": 500, "headers": {}, // @ts-ignore "body": Buffer.from(""), } ); } ) .then( (http_response) => { const output : string = lib_plankton.http.encode_response(http_response); return Promise.resolve(output); } ) ); }, { "host": conf.server.host, "port": conf.server.port, } ); lib_plankton.server.start(server); return Promise.resolve(undefined); } ( main(process.argv.slice(2)) .then( () => {} ) .catch( (error) => {process.stderr.write(String(error));} ) );