From 000d62de1c9a575cc75632bc5098a2b5575b3ca7 Mon Sep 17 00:00:00 2001 From: gkc Date: Thu, 18 Jul 2024 13:33:08 +0100 Subject: [PATCH 01/40] feat: (initial sane commit): file-based policy service; policy service supplies a permitOpen list - made daemonAtsigns injectable via npa bootstrapper - added permitOpen to NPAAuthCheckResponse - renamed `SshnpdImpl.isFromAuthorizedAtsign` to `authCheck` and have it return an NPAAuthCheckResponse - use the NPAAuthCheckResponse to further check authorization for npt requests. After the SshnpdImpl checks its own permitOpen list, it will now also check the permitOpen list returned by the npa policy service. This enables nice single-jump-box configurations where the jump-box daemon could have permitOpen "*:*" but individual client atSigns may be restricted to "my_host:3389" or "*:22" or whatever is appropriate - added policy binary to the buildArchive and buildBinaries scripts, and to the multibuild.yaml workflow definition --- .github/workflows/multibuild.yaml | 3 +- .../dart/noports_core/lib/src/npa/npa.dart | 22 ++- .../noports_core/lib/src/npa/npa_impl.dart | 25 ++- .../noports_core/lib/src/npa/npa_params.dart | 3 +- .../noports_core/lib/src/npa/npa_rpcs.dart | 20 +- .../lib/src/sshnpd/sshnpd_impl.dart | 81 +++++--- .../sshnoports/bin/demo/npa_always_deny.dart | 5 +- .../dart/sshnoports/bin/demo/npa_cli.dart | 17 +- packages/dart/sshnoports/bin/policy.dart | 182 ++++++++++++++++++ packages/dart/sshnoports/buildArchive | 1 + packages/dart/sshnoports/buildBinaries | 1 + .../dart/sshnoports/lib/npa_bootstrapper.dart | 6 +- packages/dart/sshnoports/pubspec.lock | 2 +- packages/dart/sshnoports/pubspec.yaml | 1 + 14 files changed, 310 insertions(+), 59 deletions(-) create mode 100644 packages/dart/sshnoports/bin/policy.dart diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml index addbae063..ab33c8e15 100644 --- a/.github/workflows/multibuild.yaml +++ b/.github/workflows/multibuild.yaml @@ -85,6 +85,7 @@ jobs: dart compile exe bin/activate_cli.dart -v -o sshnp/at_activate${{ matrix.ext }} dart compile exe bin/sshnp.dart -v -o sshnp/sshnp${{ matrix.ext }} dart compile exe bin/npt.dart -v -o sshnp/npt${{ matrix.ext }} + dart compile exe bin/policy.dart -v -o sshnp/policy${{ matrix.ext }} dart compile exe bin/sshnpd.dart -v -o sshnp/sshnpd${{ matrix.ext }} dart compile exe bin/srv.dart -v -o sshnp/srv${{ matrix.ext }} - if: ${{ matrix.os != 'windows-latest' }} @@ -124,7 +125,7 @@ jobs: --prefix "com.atsign." \ --timestamp \ -v \ - sshnp/{sshnp,sshnpd,srv,srvd,at_activate,debug/srvd,npt} + sshnp/{sshnp,sshnpd,srv,srvd,at_activate,debug/srvd,npt,policy} # zip the build - if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-14' }} run: ditto -c -k --keepParent sshnp tarball/${{ matrix.output-name }}.zip diff --git a/packages/dart/noports_core/lib/src/npa/npa.dart b/packages/dart/noports_core/lib/src/npa/npa.dart index 9ebd0a74b..748a4d186 100644 --- a/packages/dart/noports_core/lib/src/npa/npa.dart +++ b/packages/dart/noports_core/lib/src/npa/npa.dart @@ -29,21 +29,25 @@ abstract class NPA implements AtRpcCallbacks { String get authorizerAtsign; - abstract Set daemonAtsigns; - - abstract NPARequestHandler handler; - - static Future fromCommandLineArgs(List args, - {required NPARequestHandler handler, - AtClient? atClient, - FutureOr Function(NPAParams)? atClientGenerator, - void Function(Object, StackTrace)? usageCallback}) async { + Set get daemonAtsigns; + + NPARequestHandler get handler; + + static Future fromCommandLineArgs( + List args, { + required NPARequestHandler handler, + AtClient? atClient, + FutureOr Function(NPAParams)? atClientGenerator, + void Function(Object, StackTrace)? usageCallback, + Set? daemonAtsigns, + }) async { return NPAImpl.fromCommandLineArgs( args, handler: handler, atClient: atClient, atClientGenerator: atClientGenerator, usageCallback: usageCallback, + daemonAtsigns: daemonAtsigns, ); } diff --git a/packages/dart/noports_core/lib/src/npa/npa_impl.dart b/packages/dart/noports_core/lib/src/npa/npa_impl.dart index ef84f0df0..fb1c0fb33 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_impl.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_impl.dart @@ -24,10 +24,10 @@ class NPAImpl implements NPA { String get authorizerAtsign => atClient.getCurrentAtSign()!; @override - Set daemonAtsigns; + final Set daemonAtsigns; @override - NPARequestHandler handler; + final NPARequestHandler handler; static const JsonEncoder jsonPrettyPrinter = JsonEncoder.withIndent(' '); @@ -42,11 +42,14 @@ class NPAImpl implements NPA { logger.logger.level = Level.SHOUT; } - static Future fromCommandLineArgs(List args, - {required NPARequestHandler handler, - AtClient? atClient, - FutureOr Function(NPAParams)? atClientGenerator, - void Function(Object, StackTrace)? usageCallback}) async { + static Future fromCommandLineArgs( + List args, { + required NPARequestHandler handler, + AtClient? atClient, + FutureOr Function(NPAParams)? atClientGenerator, + void Function(Object, StackTrace)? usageCallback, + Set? daemonAtsigns, + }) async { try { var p = await NPAParams.fromArgs(args); @@ -117,9 +120,11 @@ class NPAImpl implements NPA { return AtRpcResp( reqId: request.reqId, respType: AtRpcRespType.success, - payload: - NPAAuthCheckResponse(authorized: false, message: 'Exception: $e') - .toJson()); + payload: NPAAuthCheckResponse( + authorized: false, + message: 'Exception: $e', + permitOpen: [], + ).toJson()); } } diff --git a/packages/dart/noports_core/lib/src/npa/npa_params.dart b/packages/dart/noports_core/lib/src/npa/npa_params.dart index f1d79b92e..0f102622a 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_params.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_params.dart @@ -51,7 +51,8 @@ class NPAParams { parser.addOption( 'daemon-atsigns', - mandatory: true, + mandatory: false, + defaultsTo: '', help: 'Comma-separated list of daemon atSigns which use this authorizer', ); diff --git a/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart b/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart index 55709669a..58533f251 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart @@ -38,18 +38,26 @@ class NPAAuthCheckRequest { class NPAAuthCheckResponse { final bool authorized; final String? message; + final List permitOpen; - NPAAuthCheckResponse({required this.authorized, required this.message}); + NPAAuthCheckResponse({ + required this.authorized, + required this.message, + required this.permitOpen, + }); static NPAAuthCheckResponse fromJson(Map json) { return NPAAuthCheckResponse( - authorized: json['authorized'], - message: json['message'], - ); + authorized: json['authorized'], + message: json['message'], + permitOpen: json['permitOpen']); } - Map toJson() => - {'authorized': authorized, 'message': message}; + Map toJson() => { + 'authorized': authorized, + 'message': message, + 'permitOpen': permitOpen, + }; @override String toString() => jsonPrettyPrinter.convert(toJson()); diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart index 4d47b22c8..9d1134466 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart @@ -268,14 +268,12 @@ class SshnpdImpl implements Sshnpd { /// Notification handler for sshnpd void _notificationHandler(AtNotification notification) async { - bool authed; - String message; - (authed, message) = await isFromAuthorizedAtsign(notification); - if (!authed) { + NPAAuthCheckResponse auth = await authCheck(notification); + if (!auth.authorized) { // TODO IF $someConditions apply then send a 'nice' error // TODO message notification back to the requester logger.shout('Notification ignored from ${notification.from}' - ' which is not authorized: $message' + ' which is not authorized: ${auth.message}' ' Notification value was ${notification.value}'); return; } @@ -321,40 +319,47 @@ class SshnpdImpl implements Sshnpd { case 'npt_request': logger.info('$notificationKey received from ${notification.from}' ' ( ${notification.value} )'); - _handleNptRequestNotification(notification); + _handleNptRequestNotification(notification, auth); break; } } - Future<(bool, String)> isFromAuthorizedAtsign( - AtNotification notification) async { + Future authCheck(AtNotification notification) async { const authTimeoutSeconds = 10; - late bool authed; - late String message; String client = notification.from; if (managerAtsigns.contains(client)) { - return (true, '$client is in --managers list'); + return NPAAuthCheckResponse( + authorized: true, + message: '$client is in --managers list', + permitOpen: ['*:*'], + ); } if (authChecker != null) { + late NPAAuthCheckResponse resp; try { logger.info('Asking $policyManagerAtsign' ' whether $client may connect to this daemon'); - NPAAuthCheckResponse resp = await authChecker! + resp = await authChecker! .mayConnect(clientAtsign: client) .timeout(const Duration(seconds: authTimeoutSeconds)); - authed = resp.authorized; - message = resp.message ?? ''; } on TimeoutException { - authed = false; - message = 'Timed out waiting for authorizer response'; + resp = NPAAuthCheckResponse( + authorized: false, + message: 'Timed out waiting for authorizer response', + permitOpen: [], + ); } - return (authed, message); + return resp; } - return (false, '$client is not in --managers list'); + return NPAAuthCheckResponse( + authorized: false, + message: '$client is not in --managers list', + permitOpen: [], + ); } void _handlePingNotification(AtNotification notification) { @@ -420,7 +425,10 @@ class SshnpdImpl implements Sshnpd { } } - void _handleNptRequestNotification(AtNotification notification) async { + void _handleNptRequestNotification( + AtNotification notification, + NPAAuthCheckResponse auth, + ) async { String requestingAtsign = notification.from; // Extract the NPT request payload. @@ -461,10 +469,8 @@ class SshnpdImpl implements Sshnpd { } String requested = '${req.requestedHost}:${req.requestedPort}'; - if (!(permitOpen.contains(requested) || - permitOpen.contains('*:${req.requestedPort}') || - permitOpen.contains('${req.requestedHost}:*') || - permitOpen.contains('*:*'))) { + // Check if this *daemon* allows connections to the requested host / port + if (!_permittedToOpen(permitOpen, req)) { // Notify noports client that this session is NOT connected await _notify( atKey: _createResponseAtKey( @@ -475,6 +481,20 @@ class SshnpdImpl implements Sshnpd { return; } + + // Check if this *client* is allowed connections to the requested host / port + if (!_permittedToOpen(auth.permitOpen, req)) { + // Notify noports client that this session is NOT connected + await _notify( + atKey: _createResponseAtKey( + requestingAtsign: requestingAtsign, sessionId: req.sessionId), + value: 'Client is not permitted connections to $requested', + sessionId: req.sessionId, + ); + + return; + } + // Start our side of the tunnel await startNpt( requestingAtsign: requestingAtsign, @@ -482,6 +502,15 @@ class SshnpdImpl implements Sshnpd { ); } + bool _permittedToOpen (List po, NptSessionRequest req) { + String requested = '${req.requestedHost}:${req.requestedPort}'; + // Check if this daemon allows connections to the requested host / port + return (po.contains(requested) || + po.contains('*:${req.requestedPort}') || + po.contains('${req.requestedHost}:*') || + po.contains('*:*')); + } + Future startNpt({ required String requestingAtsign, required NptSessionRequest req, @@ -1358,8 +1387,10 @@ class _NPAAuthChecker implements AuthChecker, AtRpcCallbacks { 'Got non-success auth check response from ${sshnpd.policyManagerAtsign}' ' : $response'); completer.complete(NPAAuthCheckResponse( - authorized: false, - message: response.message ?? 'Got non-success response $response')); + authorized: false, + message: response.message ?? 'Got non-success response $response', + permitOpen: [], + )); break; } } diff --git a/packages/dart/sshnoports/bin/demo/npa_always_deny.dart b/packages/dart/sshnoports/bin/demo/npa_always_deny.dart index 4a58899db..f33547f2a 100644 --- a/packages/dart/sshnoports/bin/demo/npa_always_deny.dart +++ b/packages/dart/sshnoports/bin/demo/npa_always_deny.dart @@ -11,6 +11,9 @@ class AlwaysDeny implements NPARequestHandler { Future doAuthCheck( NPAAuthCheckRequest authCheckRequest) async { return NPAAuthCheckResponse( - authorized: false, message: 'Computer says "Noooo..."'); + authorized: false, + message: 'Computer says "Noooo..."', + permitOpen: [], + ); } } diff --git a/packages/dart/sshnoports/bin/demo/npa_cli.dart b/packages/dart/sshnoports/bin/demo/npa_cli.dart index 4c7f2a05b..5d22c1728 100644 --- a/packages/dart/sshnoports/bin/demo/npa_cli.dart +++ b/packages/dart/sshnoports/bin/demo/npa_cli.dart @@ -18,9 +18,18 @@ class CLI implements NPARequestHandler { decision = stdin.readLineSync()!; } final bool authorized = decision.toLowerCase().startsWith('a'); - return NPAAuthCheckResponse( - authorized: authorized, - message: authorized ? 'Approved via CLI' : 'Denied via CLI', - ); + if (authorized) { + return NPAAuthCheckResponse( + authorized: true, + message: 'Approved via CLI', + permitOpen: ['*:*'], + ); + } else { + return NPAAuthCheckResponse( + authorized: false, + message: 'Denied via CLI', + permitOpen: [], + ); + } } } diff --git a/packages/dart/sshnoports/bin/policy.dart b/packages/dart/sshnoports/bin/policy.dart new file mode 100644 index 000000000..6357c0246 --- /dev/null +++ b/packages/dart/sshnoports/bin/policy.dart @@ -0,0 +1,182 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:args/args.dart'; +import 'package:noports_core/npa.dart'; +import 'package:sshnoports/npa_bootstrapper.dart' as bootstrapper; +import 'package:yaml/yaml.dart'; + +void main(List args) async { + ArgParser parser = NPAParams.parser; + parser.addOption( + 'yaml', + mandatory: true, + help: 'Path to policy yaml', + ); + ArgResults r = parser.parse(args); + + YamlMap? yaml = loadYaml(File(r['yaml']).readAsStringSync()); + + FileBasedPolicy policy = FileBasedPolicy(yaml!); + await bootstrapper.run( + policy, + args, + daemonAtsigns: policy.daemonAtsigns, + ); +} + +/// - Client atSigns request access to a $deviceName at some $daemonAtSign for $someReason +/// - Daemons run with a deviceName and a deviceGroup +/// - Daemons send the daemonAtsign, clientAtSign, deviceName and deviceGroup to policy service +/// - The policy service needs to check if the clientAtsign is +/// - 1. permitted to talk to this daemonAtsign +/// - --> is there a value at permissions[@client][@daemon] +/// - and +/// - 2a. permitted to talk to this daemon's deviceName +/// - --> is there a value at permissions[@client][@daemon]['deviceNames'][$deviceName] +/// - or +/// - 2b. permitted to talk to this daemon's deviceGroupName +/// - --> is there a value at permissions[@client][@daemon]['deviceGroupNames'][$deviceGroupName] +/// +/// - The value at 2a or 2b should be a list of permitOpens (hostMask:portMask) +/// +/// We'll start with a map exactly as described above. By definition, it will be +/// fully denormalized. We will implement a more normalized structure later. +class FileBasedPolicy implements NPARequestHandler { + YamlMap yaml; + + final Set _daemonAtsigns = {}; + + FileBasedPolicy(this.yaml) { + // iterate through the map, gather all of the daemon atSigns + // top level keys are the client atSigns + for (String clientAtsign in yaml.keys) { + // Next level is a map of daemonAtsigns + for (String daemonAtsign in yaml[clientAtsign].keys) { + _daemonAtsigns.add(daemonAtsign); + } + } + } + + Set get daemonAtsigns => _daemonAtsigns; + + @override + Future doAuthCheck( + NPAAuthCheckRequest authCheckRequest) async { + /// - The policy service needs to check if the clientAtsign is + /// - 1. permitted to talk to this daemonAtsign + /// - --> is there a value at permissions[@client][@daemon] + final clientEntry = yaml[authCheckRequest.clientAtsign]; + if (clientEntry == null) { + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}', + permitOpen: [], + ); + } + final daemonEntry = clientEntry[authCheckRequest.daemonAtsign]; + if (daemonEntry == null) { + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}' + ' at ${authCheckRequest.daemonAtsign}', + permitOpen: [], + ); + } + + /// - and + /// - 2a. permitted to talk to this daemon's deviceName + /// - --> is there a value at permissions[@client][@daemon]['deviceNames'][$deviceName] + final deviceNames = daemonEntry['deviceNames']; + if (deviceNames != null) { + final deviceNameEntry = deviceNames[authCheckRequest.daemonDeviceName]; + if (deviceNameEntry != null) { + return NPAAuthCheckResponse( + authorized: true, + message: '${authCheckRequest.clientAtsign} has permission' + ' for device ${authCheckRequest.daemonDeviceName}' + ' at daemon ${authCheckRequest.daemonAtsign}', + permitOpen: List.from(deviceNameEntry), + ); + } + } + + /// - or + /// - 2b. permitted to talk to this daemon's deviceGroupName + /// - --> is there a value at permissions[@client][@daemon]['deviceGroupNames'][$deviceGroupName] + final deviceGroupNames = daemonEntry['deviceGroupNames']; + if (deviceGroupNames != null) { + final deviceGroupNameEntry = + deviceGroupNames[authCheckRequest.daemonDeviceGroupName]; + if (deviceGroupNameEntry != null) { + return NPAAuthCheckResponse( + authorized: true, + message: '${authCheckRequest.clientAtsign} has permission' + ' for device group ${authCheckRequest.daemonDeviceGroupName}' + ' at daemon ${authCheckRequest.daemonAtsign}', + permitOpen: List.from(deviceGroupNameEntry), + ); + } + } + + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}' + ' at ${authCheckRequest.daemonAtsign}' + ' for either the device ${authCheckRequest.daemonDeviceName}' + ' or the deviceGroup ${authCheckRequest.daemonDeviceGroupName}', + permitOpen: [], + ); + } + + // TODO move to unit tests + _randomChecks() { + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'mbp', + daemonDeviceGroupName: 'gary_home', + clientAtsign: '@garycasey', + )).then((resp) => print(resp)); + + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'gary_windows_box_1', + daemonDeviceGroupName: 'gary_home', + clientAtsign: '@garycasey', + )).then((resp) => print(resp)); + + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'mbp', + daemonDeviceGroupName: 'gary_home', + clientAtsign: '@cconstab', + )).then((resp) => print(resp)); + + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'gary_windows_box_1', + daemonDeviceGroupName: 'gary_home', + clientAtsign: '@cconstab', + )).then((resp) => print(resp)); + + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'mbp', + daemonDeviceGroupName: 'gary_home', + clientAtsign: '@colin', + )).then((resp) => print(resp)); + + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'gary_windows_box_1', + daemonDeviceGroupName: 'gary_home', + clientAtsign: '@colin', + )).then((resp) => print(resp)); + + doAuthCheck(NPAAuthCheckRequest( + daemonAtsign: '@baboonblue18', + daemonDeviceName: 'gary_lab_device_1', + daemonDeviceGroupName: 'gary_lab', + clientAtsign: '@colin', + )).then((resp) => print(resp)); + } +} diff --git a/packages/dart/sshnoports/buildArchive b/packages/dart/sshnoports/buildArchive index 927719421..dafc8299d 100755 --- a/packages/dart/sshnoports/buildArchive +++ b/packages/dart/sshnoports/buildArchive @@ -17,6 +17,7 @@ echo "Compiling sshnpd"; dart compile exe --verbosity error bin/sshnpd.dart -o b echo "Compiling srvd"; dart compile exe --verbosity error bin/srvd.dart -o build/sshnp/srvd & echo "Compiling sshnp"; dart compile exe --verbosity error bin/sshnp.dart -o build/sshnp/sshnp & echo "Compiling npt"; dart compile exe --verbosity error bin/npt.dart -o build/sshnp/npt & +echo "Compiling policy"; dart compile exe --verbosity error bin/policy.dart -o build/sshnp/policy & wait diff --git a/packages/dart/sshnoports/buildBinaries b/packages/dart/sshnoports/buildBinaries index 4aae01160..efbaead7b 100755 --- a/packages/dart/sshnoports/buildBinaries +++ b/packages/dart/sshnoports/buildBinaries @@ -9,6 +9,7 @@ echo "Compiling sshnpd"; dart compile exe --verbosity error bin/sshnpd.dart -o b echo "Compiling srvd"; dart compile exe --verbosity error bin/srvd.dart -o bin/srvd & echo "Compiling sshnp"; dart compile exe --verbosity error bin/sshnp.dart -o bin/sshnp & echo "Compiling npt"; dart compile exe --verbosity error bin/npt.dart -o bin/npt & +echo "Compiling policy"; dart compile exe --verbosity error bin/policy.dart -o bin/policy & echo "Compiling demo/npa_always_deny"; dart compile exe --verbosity error bin/demo/npa_always_deny.dart -o bin/demo/npa_always_deny & echo "Compiling demo/npa_cli"; dart compile exe --verbosity error bin/demo/npa_cli.dart -o bin/demo/npa_cli & diff --git a/packages/dart/sshnoports/lib/npa_bootstrapper.dart b/packages/dart/sshnoports/lib/npa_bootstrapper.dart index 9e9654ac8..d5e1a2d5d 100644 --- a/packages/dart/sshnoports/lib/npa_bootstrapper.dart +++ b/packages/dart/sshnoports/lib/npa_bootstrapper.dart @@ -8,7 +8,10 @@ import 'package:sshnoports/src/print_version.dart'; import 'package:sshnoports/src/service_factories.dart'; Future run( - NPARequestHandler handler, List commandLineArgs) async { + NPARequestHandler handler, + List commandLineArgs, { + Set? daemonAtsigns, +}) async { AtSignLogger.root_level = 'SHOUT'; AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; late final NPA sshnpa; @@ -17,6 +20,7 @@ Future run( sshnpa = await NPA.fromCommandLineArgs( commandLineArgs, handler: handler, + daemonAtsigns: daemonAtsigns, atClientGenerator: (NPAParams p) => createAtClientCli( atsign: p.authorizerAtsign, atKeysFilePath: p.atKeysFilePath, diff --git a/packages/dart/sshnoports/pubspec.lock b/packages/dart/sshnoports/pubspec.lock index ef51a772b..e1befab8e 100644 --- a/packages/dart/sshnoports/pubspec.lock +++ b/packages/dart/sshnoports/pubspec.lock @@ -899,7 +899,7 @@ packages: source: hosted version: "6.5.0" yaml: - dependency: transitive + dependency: "direct main" description: name: yaml sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" diff --git a/packages/dart/sshnoports/pubspec.yaml b/packages/dart/sshnoports/pubspec.yaml index 1b5a562dd..3911461e4 100644 --- a/packages/dart/sshnoports/pubspec.yaml +++ b/packages/dart/sshnoports/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: at_utils: 3.0.16 logging: ^1.2.0 chalkdart: ^2.2.1 + yaml: ^3.1.2 dependency_overrides: dartssh2: From 168bc5b2bcc8494236ff37e0bfb622eb48954020 Mon Sep 17 00:00:00 2001 From: gkc Date: Thu, 18 Jul 2024 13:35:15 +0100 Subject: [PATCH 02/40] chore: ran dart format; cleaned an 'unused function' lint --- packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart | 2 +- packages/dart/sshnoports/bin/policy.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart index 9d1134466..c8c36237d 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart @@ -502,7 +502,7 @@ class SshnpdImpl implements Sshnpd { ); } - bool _permittedToOpen (List po, NptSessionRequest req) { + bool _permittedToOpen(List po, NptSessionRequest req) { String requested = '${req.requestedHost}:${req.requestedPort}'; // Check if this daemon allows connections to the requested host / port return (po.contains(requested) || diff --git a/packages/dart/sshnoports/bin/policy.dart b/packages/dart/sshnoports/bin/policy.dart index 6357c0246..da2c3e504 100644 --- a/packages/dart/sshnoports/bin/policy.dart +++ b/packages/dart/sshnoports/bin/policy.dart @@ -129,6 +129,7 @@ class FileBasedPolicy implements NPARequestHandler { } // TODO move to unit tests + // ignore: unused_element _randomChecks() { doAuthCheck(NPAAuthCheckRequest( daemonAtsign: '@baboonblue18', From d1aa276ca5898cbe19165d8e5b215edaefe56370 Mon Sep 17 00:00:00 2001 From: gkc Date: Thu, 18 Jul 2024 17:09:52 +0100 Subject: [PATCH 03/40] refactor: renamed policy.dart to npa_file.dart fix: fixed a couple of small bugs uncovered during first road-test --- .github/workflows/multibuild.yaml | 4 ++-- packages/dart/noports_core/lib/src/npa/npa_impl.dart | 2 +- packages/dart/noports_core/lib/src/npa/npa_rpcs.dart | 2 +- packages/dart/sshnoports/bin/{policy.dart => npa_file.dart} | 0 packages/dart/sshnoports/buildArchive | 2 +- packages/dart/sshnoports/buildBinaries | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename packages/dart/sshnoports/bin/{policy.dart => npa_file.dart} (100%) diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml index ab33c8e15..9d1d9f17f 100644 --- a/.github/workflows/multibuild.yaml +++ b/.github/workflows/multibuild.yaml @@ -85,7 +85,7 @@ jobs: dart compile exe bin/activate_cli.dart -v -o sshnp/at_activate${{ matrix.ext }} dart compile exe bin/sshnp.dart -v -o sshnp/sshnp${{ matrix.ext }} dart compile exe bin/npt.dart -v -o sshnp/npt${{ matrix.ext }} - dart compile exe bin/policy.dart -v -o sshnp/policy${{ matrix.ext }} + dart compile exe bin/npa_file.dart -v -o sshnp/npa_file${{ matrix.ext }} dart compile exe bin/sshnpd.dart -v -o sshnp/sshnpd${{ matrix.ext }} dart compile exe bin/srv.dart -v -o sshnp/srv${{ matrix.ext }} - if: ${{ matrix.os != 'windows-latest' }} @@ -125,7 +125,7 @@ jobs: --prefix "com.atsign." \ --timestamp \ -v \ - sshnp/{sshnp,sshnpd,srv,srvd,at_activate,debug/srvd,npt,policy} + sshnp/{sshnp,sshnpd,srv,srvd,at_activate,debug/srvd,npt,npa_file} # zip the build - if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-14' }} run: ditto -c -k --keepParent sshnp tarball/${{ matrix.output-name }}.zip diff --git a/packages/dart/noports_core/lib/src/npa/npa_impl.dart b/packages/dart/noports_core/lib/src/npa/npa_impl.dart index fb1c0fb33..1eb4bdbeb 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_impl.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_impl.dart @@ -72,7 +72,7 @@ class NPAImpl implements NPA { var sshnpa = NPAImpl( atClient: atClient, homeDirectory: p.homeDirectory, - daemonAtsigns: p.daemonAtsigns, + daemonAtsigns: daemonAtsigns ?? p.daemonAtsigns, handler: handler, ); diff --git a/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart b/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart index 58533f251..f973fea17 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart @@ -50,7 +50,7 @@ class NPAAuthCheckResponse { return NPAAuthCheckResponse( authorized: json['authorized'], message: json['message'], - permitOpen: json['permitOpen']); + permitOpen: List.from(json['permitOpen'])); } Map toJson() => { diff --git a/packages/dart/sshnoports/bin/policy.dart b/packages/dart/sshnoports/bin/npa_file.dart similarity index 100% rename from packages/dart/sshnoports/bin/policy.dart rename to packages/dart/sshnoports/bin/npa_file.dart diff --git a/packages/dart/sshnoports/buildArchive b/packages/dart/sshnoports/buildArchive index dafc8299d..daaa0cd8b 100755 --- a/packages/dart/sshnoports/buildArchive +++ b/packages/dart/sshnoports/buildArchive @@ -17,7 +17,7 @@ echo "Compiling sshnpd"; dart compile exe --verbosity error bin/sshnpd.dart -o b echo "Compiling srvd"; dart compile exe --verbosity error bin/srvd.dart -o build/sshnp/srvd & echo "Compiling sshnp"; dart compile exe --verbosity error bin/sshnp.dart -o build/sshnp/sshnp & echo "Compiling npt"; dart compile exe --verbosity error bin/npt.dart -o build/sshnp/npt & -echo "Compiling policy"; dart compile exe --verbosity error bin/policy.dart -o build/sshnp/policy & +echo "Compiling npa_file"; dart compile exe --verbosity error bin/npa_file.dart -o build/sshnp/npa_file & wait diff --git a/packages/dart/sshnoports/buildBinaries b/packages/dart/sshnoports/buildBinaries index efbaead7b..ae8d24e3b 100755 --- a/packages/dart/sshnoports/buildBinaries +++ b/packages/dart/sshnoports/buildBinaries @@ -9,7 +9,7 @@ echo "Compiling sshnpd"; dart compile exe --verbosity error bin/sshnpd.dart -o b echo "Compiling srvd"; dart compile exe --verbosity error bin/srvd.dart -o bin/srvd & echo "Compiling sshnp"; dart compile exe --verbosity error bin/sshnp.dart -o bin/sshnp & echo "Compiling npt"; dart compile exe --verbosity error bin/npt.dart -o bin/npt & -echo "Compiling policy"; dart compile exe --verbosity error bin/policy.dart -o bin/policy & +echo "Compiling npa_file"; dart compile exe --verbosity error bin/npa_file.dart -o bin/npa_file & echo "Compiling demo/npa_always_deny"; dart compile exe --verbosity error bin/demo/npa_always_deny.dart -o bin/demo/npa_always_deny & echo "Compiling demo/npa_cli"; dart compile exe --verbosity error bin/demo/npa_cli.dart -o bin/demo/npa_cli & From 0d67c655460ab7b43516b7acece72616ad5c3b89 Mon Sep 17 00:00:00 2001 From: gkc Date: Tue, 23 Jul 2024 14:31:30 +0100 Subject: [PATCH 04/40] feat: some normalization of the yaml structure by adding userGroups --- packages/dart/sshnoports/bin/.gitignore | 1 + packages/dart/sshnoports/bin/npa_file.dart | 86 +++++++++++++++++++--- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/packages/dart/sshnoports/bin/.gitignore b/packages/dart/sshnoports/bin/.gitignore index 6bb8f1b72..16e3ff860 100644 --- a/packages/dart/sshnoports/bin/.gitignore +++ b/packages/dart/sshnoports/bin/.gitignore @@ -2,4 +2,5 @@ * # Allow dart files !*.dart +!*.yaml !.gitignore diff --git a/packages/dart/sshnoports/bin/npa_file.dart b/packages/dart/sshnoports/bin/npa_file.dart index da2c3e504..7abd2e56d 100644 --- a/packages/dart/sshnoports/bin/npa_file.dart +++ b/packages/dart/sshnoports/bin/npa_file.dart @@ -44,28 +44,92 @@ void main(List args) async { class FileBasedPolicy implements NPARequestHandler { YamlMap yaml; - final Set _daemonAtsigns = {}; + final Set _daemonAtSigns = {}; + final Map _userAtSigns = {}; FileBasedPolicy(this.yaml) { - // iterate through the map, gather all of the daemon atSigns - // top level keys are the client atSigns - for (String clientAtsign in yaml.keys) { - // Next level is a map of daemonAtsigns - for (String daemonAtsign in yaml[clientAtsign].keys) { - _daemonAtsigns.add(daemonAtsign); + // Get the full list of daemonAtSigns which this policy service will listen to + for (String userGroupName in yaml['userGroups'].keys ?? []) { + for (String daemonAtSign in yaml['userGroups'][userGroupName] + ['permissions']['daemonAtSigns'] ?? + []) { + _daemonAtSigns.add(daemonAtSign); + } + } + print(_daemonAtSigns); + + // Create a map of userAtSign->daemonAtSign->deviceNames/deviceGroupNames->[PermitOpens] + for (String userGroupName in yaml['userGroups'].keys ?? []) { + final group = yaml['userGroups'][userGroupName]; + for (String userAtSign in group['userAtSigns']) { + _userAtSigns.putIfAbsent( + userAtSign, () => {'userGroups': [], 'daemons': {}}); + ((_userAtSigns[userAtSign] as Map)['userGroups'] as List) + .add(userGroupName); + Map userDaemonPermissions = + (_userAtSigns[userAtSign] as Map)['daemons'] as Map; + for (String daemonAtSign + in group['permissions']['daemonAtSigns'] ?? []) { + userDaemonPermissions.putIfAbsent( + daemonAtSign, () => {'deviceNames': {}, 'deviceGroupNames': {}}); + + for (String deviceName + in (group['permissions']['deviceNames'] ?? {}).keys) { + Map devicesMap = + userDaemonPermissions[daemonAtSign]['deviceNames'] as Map; + devicesMap.putIfAbsent(deviceName, () => []); + final devicePermissions = devicesMap[deviceName] as List; + for (String permitOpen in List.from( + group['permissions']['deviceNames'][deviceName] ?? [])) { + if (!devicePermissions.contains(permitOpen)) { + devicePermissions.add(permitOpen); + } + } + } + + for (String deviceGroupName + in (group['permissions']['deviceGroupNames'] ?? {}).keys) { + Map deviceGroupsMap = + userDaemonPermissions[daemonAtSign]['deviceGroupNames'] as Map; + deviceGroupsMap.putIfAbsent(deviceGroupName, () => []); + final deviceGroupPermissions = + deviceGroupsMap[deviceGroupName] as List; + for (String permitOpen in List.from(group['permissions'] + ['deviceGroupNames'][deviceGroupName] ?? + [])) { + if (!deviceGroupPermissions.contains(permitOpen)) { + deviceGroupPermissions.add(permitOpen); + } + } + } + } + } + } + for (final u in _userAtSigns.keys) { + final user = _userAtSigns[u] as Map; + print('$u is member of groups ${user['userGroups']}'); + for (final d in user['daemons'].keys) { + print(' daemon: $d'); + var daemon = user['daemons'][d]; + for (final dn in (daemon['deviceNames']).keys) { + print(' device $dn: ${daemon['deviceNames'][dn]}'); + } + for (final dgn in (daemon['deviceGroupNames']).keys) { + print(' deviceGroup $dgn: ${daemon['deviceGroupNames'][dgn]}'); + } } } } - Set get daemonAtsigns => _daemonAtsigns; + Set get daemonAtsigns => _daemonAtSigns; @override Future doAuthCheck( NPAAuthCheckRequest authCheckRequest) async { /// - The policy service needs to check if the clientAtsign is /// - 1. permitted to talk to this daemonAtsign - /// - --> is there a value at permissions[@client][@daemon] - final clientEntry = yaml[authCheckRequest.clientAtsign]; + /// - --> is there a value at _userAtSigns[@client][@daemon] + final clientEntry = _userAtSigns[authCheckRequest.clientAtsign]; if (clientEntry == null) { return NPAAuthCheckResponse( authorized: false, @@ -73,7 +137,7 @@ class FileBasedPolicy implements NPARequestHandler { permitOpen: [], ); } - final daemonEntry = clientEntry[authCheckRequest.daemonAtsign]; + final daemonEntry = clientEntry['daemons'][authCheckRequest.daemonAtsign]; if (daemonEntry == null) { return NPAAuthCheckResponse( authorized: false, From 206ea45efca2be333130d085c70da02894117e53 Mon Sep 17 00:00:00 2001 From: gkc Date: Tue, 23 Jul 2024 14:34:59 +0100 Subject: [PATCH 05/40] docs: corrected code comments --- packages/dart/sshnoports/bin/npa_file.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/dart/sshnoports/bin/npa_file.dart b/packages/dart/sshnoports/bin/npa_file.dart index 7abd2e56d..72051ffaa 100644 --- a/packages/dart/sshnoports/bin/npa_file.dart +++ b/packages/dart/sshnoports/bin/npa_file.dart @@ -29,18 +29,16 @@ void main(List args) async { /// - Daemons send the daemonAtsign, clientAtSign, deviceName and deviceGroup to policy service /// - The policy service needs to check if the clientAtsign is /// - 1. permitted to talk to this daemonAtsign -/// - --> is there a value at permissions[@client][@daemon] +/// - --> is there a value at permissions[@client]['daemons'][@daemon] /// - and /// - 2a. permitted to talk to this daemon's deviceName -/// - --> is there a value at permissions[@client][@daemon]['deviceNames'][$deviceName] +/// - --> is there a value at permissions[@client]['daemons'][@daemon]['deviceNames'][$deviceName] /// - or /// - 2b. permitted to talk to this daemon's deviceGroupName -/// - --> is there a value at permissions[@client][@daemon]['deviceGroupNames'][$deviceGroupName] +/// - --> is there a value at permissions[@client]['daemons'][@daemon]['deviceGroupNames'][$deviceGroupName] /// -/// - The value at 2a or 2b should be a list of permitOpens (hostMask:portMask) +/// - The value at 2a or 2b will be a list of permitOpens (hostMask:portMask) /// -/// We'll start with a map exactly as described above. By definition, it will be -/// fully denormalized. We will implement a more normalized structure later. class FileBasedPolicy implements NPARequestHandler { YamlMap yaml; From 7b263a50767c1e9de0a5da95b3cb0f68921aad8a Mon Sep 17 00:00:00 2001 From: gkc Date: Fri, 23 Aug 2024 17:25:58 +0100 Subject: [PATCH 06/40] interim commit --- apps/admin/admin_api/.gitignore | 3 + apps/admin/admin_api/CHANGELOG.md | 3 + apps/admin/admin_api/README.md | 2 + apps/admin/admin_api/analysis_options.yaml | 16 + apps/admin/admin_api/bin/api.dart | 65 + apps/admin/admin_api/lib/src/expose_apis.dart | 62 + apps/admin/admin_api/pubspec.lock | 970 ++ apps/admin/admin_api/pubspec.yaml | 32 + .../admin/admin_api/test/policy_api_test.dart | 261 + apps/admin/webapp_proto/.gitignore | 24 + .../webapp_proto/.vscode/extensions.json | 3 + apps/admin/webapp_proto/README.md | 47 + apps/admin/webapp_proto/index.html | 17 + apps/admin/webapp_proto/jsconfig.json | 32 + apps/admin/webapp_proto/package-lock.json | 1113 ++ apps/admin/webapp_proto/package.json | 16 + apps/admin/webapp_proto/public/vite.svg | 1 + apps/admin/webapp_proto/src/App.svelte | 18 + apps/admin/webapp_proto/src/app.css | 11 + .../webapp_proto/src/assets/noports.avif | Bin 0 -> 23376 bytes .../admin/webapp_proto/src/assets/noports.jpg | Bin 0 -> 10748 bytes apps/admin/webapp_proto/src/assets/svelte.svg | 1 + apps/admin/webapp_proto/src/lib/Child.svelte | 13 + .../webapp_proto/src/lib/InPlaceEdit.svelte | 60 + .../src/lib/PolicyUserGroups.svelte | 284 + apps/admin/webapp_proto/src/main.js | 8 + apps/admin/webapp_proto/src/routes/+layout.js | 0 apps/admin/webapp_proto/src/vite-env.d.ts | 2 + apps/admin/webapp_proto/svelte.config.js | 7 + apps/admin/webapp_proto/vite.config.js | 7 + .../dart/noports_core/analysis_options.yaml | 1 + packages/dart/noports_core/lib/admin.dart | 5 + .../dart/noports_core/lib/src/admin/impl.dart | 86 + .../noports_core/lib/src/admin/interface.dart | 45 + .../noports_core/lib/src/admin/models.dart | 105 + .../noports_core/lib/src/admin/models.g.dart | 74 + packages/dart/noports_core/pubspec.yaml | 4 +- packages/dart/sshnoports/pubspec.lock | 2 +- packages/dart/sshnp_flutter/pubspec.lock | 24 +- policy_gui/.editorconfig | 16 + policy_gui/.gitignore | 42 + policy_gui/.vscode/extensions.json | 4 + policy_gui/.vscode/launch.json | 20 + policy_gui/.vscode/tasks.json | 42 + policy_gui/README.md | 27 + policy_gui/angular.json | 96 + policy_gui/package-lock.json | 14258 ++++++++++++++++ policy_gui/package.json | 38 + policy_gui/public/favicon.ico | Bin 0 -> 15086 bytes policy_gui/src/app/app.component.css | 0 policy_gui/src/app/app.component.html | 336 + policy_gui/src/app/app.component.spec.ts | 29 + policy_gui/src/app/app.component.ts | 13 + policy_gui/src/app/app.config.ts | 8 + policy_gui/src/app/app.routes.ts | 3 + policy_gui/src/index.html | 13 + policy_gui/src/main.ts | 6 + policy_gui/src/styles.css | 1 + policy_gui/tsconfig.app.json | 15 + policy_gui/tsconfig.json | 32 + policy_gui/tsconfig.spec.json | 15 + 61 files changed, 18424 insertions(+), 14 deletions(-) create mode 100644 apps/admin/admin_api/.gitignore create mode 100644 apps/admin/admin_api/CHANGELOG.md create mode 100644 apps/admin/admin_api/README.md create mode 100644 apps/admin/admin_api/analysis_options.yaml create mode 100644 apps/admin/admin_api/bin/api.dart create mode 100644 apps/admin/admin_api/lib/src/expose_apis.dart create mode 100644 apps/admin/admin_api/pubspec.lock create mode 100644 apps/admin/admin_api/pubspec.yaml create mode 100644 apps/admin/admin_api/test/policy_api_test.dart create mode 100644 apps/admin/webapp_proto/.gitignore create mode 100644 apps/admin/webapp_proto/.vscode/extensions.json create mode 100644 apps/admin/webapp_proto/README.md create mode 100644 apps/admin/webapp_proto/index.html create mode 100644 apps/admin/webapp_proto/jsconfig.json create mode 100644 apps/admin/webapp_proto/package-lock.json create mode 100644 apps/admin/webapp_proto/package.json create mode 100644 apps/admin/webapp_proto/public/vite.svg create mode 100644 apps/admin/webapp_proto/src/App.svelte create mode 100644 apps/admin/webapp_proto/src/app.css create mode 100644 apps/admin/webapp_proto/src/assets/noports.avif create mode 100644 apps/admin/webapp_proto/src/assets/noports.jpg create mode 100644 apps/admin/webapp_proto/src/assets/svelte.svg create mode 100644 apps/admin/webapp_proto/src/lib/Child.svelte create mode 100644 apps/admin/webapp_proto/src/lib/InPlaceEdit.svelte create mode 100644 apps/admin/webapp_proto/src/lib/PolicyUserGroups.svelte create mode 100644 apps/admin/webapp_proto/src/main.js create mode 100644 apps/admin/webapp_proto/src/routes/+layout.js create mode 100644 apps/admin/webapp_proto/src/vite-env.d.ts create mode 100644 apps/admin/webapp_proto/svelte.config.js create mode 100644 apps/admin/webapp_proto/vite.config.js create mode 100644 packages/dart/noports_core/lib/admin.dart create mode 100644 packages/dart/noports_core/lib/src/admin/impl.dart create mode 100644 packages/dart/noports_core/lib/src/admin/interface.dart create mode 100644 packages/dart/noports_core/lib/src/admin/models.dart create mode 100644 packages/dart/noports_core/lib/src/admin/models.g.dart create mode 100644 policy_gui/.editorconfig create mode 100644 policy_gui/.gitignore create mode 100644 policy_gui/.vscode/extensions.json create mode 100644 policy_gui/.vscode/launch.json create mode 100644 policy_gui/.vscode/tasks.json create mode 100644 policy_gui/README.md create mode 100644 policy_gui/angular.json create mode 100644 policy_gui/package-lock.json create mode 100644 policy_gui/package.json create mode 100644 policy_gui/public/favicon.ico create mode 100644 policy_gui/src/app/app.component.css create mode 100644 policy_gui/src/app/app.component.html create mode 100644 policy_gui/src/app/app.component.spec.ts create mode 100644 policy_gui/src/app/app.component.ts create mode 100644 policy_gui/src/app/app.config.ts create mode 100644 policy_gui/src/app/app.routes.ts create mode 100644 policy_gui/src/index.html create mode 100644 policy_gui/src/main.ts create mode 100644 policy_gui/src/styles.css create mode 100644 policy_gui/tsconfig.app.json create mode 100644 policy_gui/tsconfig.json create mode 100644 policy_gui/tsconfig.spec.json diff --git a/apps/admin/admin_api/.gitignore b/apps/admin/admin_api/.gitignore new file mode 100644 index 000000000..3a8579040 --- /dev/null +++ b/apps/admin/admin_api/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/apps/admin/admin_api/CHANGELOG.md b/apps/admin/admin_api/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/apps/admin/admin_api/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/apps/admin/admin_api/README.md b/apps/admin/admin_api/README.md new file mode 100644 index 000000000..3816eca3a --- /dev/null +++ b/apps/admin/admin_api/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/apps/admin/admin_api/analysis_options.yaml b/apps/admin/admin_api/analysis_options.yaml new file mode 100644 index 000000000..ac2b7eb70 --- /dev/null +++ b/apps/admin/admin_api/analysis_options.yaml @@ -0,0 +1,16 @@ +# Defines a default set of lint rules enforced for +# projects at Google. For details and rationale, +# see https://pub.dev/packages/lints. +include: package:lints/recommended.yaml + +# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. +# Uncomment to specify additional rules. +linter: + rules: + annotate_overrides: true + prefer_final_fields: true + camel_case_types : true + unnecessary_string_interpolations : true + await_only_futures : true + unawaited_futures: true + depend_on_referenced_packages : false diff --git a/apps/admin/admin_api/bin/api.dart b/apps/admin/admin_api/bin/api.dart new file mode 100644 index 000000000..cc89543dc --- /dev/null +++ b/apps/admin/admin_api/bin/api.dart @@ -0,0 +1,65 @@ +import 'dart:io'; + +import 'package:admin_api/src/expose_apis.dart' as expose; +import 'package:alfred/alfred.dart'; +import 'package:at_cli_commons/at_cli_commons.dart'; +import 'package:noports_core/admin.dart'; + +void main(List args) async { + CLIBase cli = await CLIBase.fromCommandLineArgs(args); + final api = PolicyService.withAtClient(atClient: cli.atClient); + + await _createUsers(api); + await _createGroups(api); + + final app = Alfred(); + app.all('*', cors(origin: 'http://localhost:5173')); + app.get('/static/*', (req, res) => Directory('static')); + await expose.policy(app, '/api/policy', api); + await app.listen(); +} + +Future _createUsers(PolicyService api) async { + await api.updateUser(User(atSign: '@alice', name: 'Alice')); + await api.updateUser(User(atSign: '@bob', name: 'Bob')); + await api.updateUser(User(atSign: '@chuck', name: 'chuck')); +} + +Future _createGroups(PolicyService api) async { + UserGroup sysAdmins = UserGroup( + name: 'SysAdmins', + description: 'System Administrators - full access', + userAtSigns: ['@alice'], + daemonAtSigns: ['@delta'], + devices: [ + Device(name: 'bastion1', permitOpens: ['*:*']) + ], + deviceGroups: [], + ); + + await api.updateUserGroup(sysAdmins); + + UserGroup policyOwners = UserGroup( + name: 'PolicyOwners', + description: 'Policy Owners - can connect to policy API', + userAtSigns: ['@bob'], + daemonAtSigns: ['@delta'], + devices: [ + Device(name: 'bastion1', permitOpens: ['localhost:15001']) + ], + deviceGroups: [], + ); + await api.updateUserGroup(policyOwners); + + UserGroup rdpUsers = UserGroup( + name: 'RdpUsers', + description: 'RDP Users - can connect to RDP ports on this network', + userAtSigns: ['@alice', '@bob', '@chuck'], + daemonAtSigns: ['@delta'], + devices: [ + Device(name: 'bastion1', permitOpens: ['*:3389']) + ], + deviceGroups: [], + ); + await api.updateUserGroup(rdpUsers); +} diff --git a/apps/admin/admin_api/lib/src/expose_apis.dart b/apps/admin/admin_api/lib/src/expose_apis.dart new file mode 100644 index 000000000..4cb927865 --- /dev/null +++ b/apps/admin/admin_api/lib/src/expose_apis.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:alfred/alfred.dart'; +import 'package:at_client/at_client.dart'; +import 'package:noports_core/admin.dart'; + +policy(Alfred app, String pathPrefix, PolicyService api) { + // all users TODO add query parameters for search, pagination etc + app.get('$pathPrefix/user', (req, res) async => jsonEncode(await api.getUsers())); + + // get individual user - {"atSign":"@alice","name":"Joe Smith"} + app.get('/user/:atsign', (req, res) async { + final atSign = Uri.decodeFull(req.uri.toString()).split('/').last; + return jsonEncode(await api.getUser(atSign)); + }); + + // add or update a user + app.post('$pathPrefix/user', (req, res) async { + User u; + try { + u = User.fromJson((await req.body)! as Map); + } catch (_) { + throw IllegalArgumentException('Unable to construct User from this json'); + } + await api.updateUser(u); + }); + + // delete a user + app.delete('$pathPrefix/user/:atsign', (req, res) async { + final atSign = Uri.decodeFull(req.uri.toString()).split('/').last; + await api.deleteUser(atSign); + }); + + // get groups that a user is a member of + app.get('$pathPrefix/user/:atsign/groups', (req, res) async { + final atSign = Uri.decodeFull(req.uri.toString()).split('/').last; + return jsonEncode(await api.getGroupsForUser(atSign)); + }); + + // all groups TODO add query parameters for search, pagination etc + app.get('$pathPrefix/group', (req, res) async => jsonEncode(await api.getUserGroups())); + + // get individual group - + // {"name":"sysadmins", + // "userAtSigns":["@alice", ...], + // "permissions":{ + // "daemonAtSigns":["@bob", ...], + // "devices":{ + // "name":"some_device_name", + // "permitOpens":["localhost:3000", ...] + // }, + // "deviceGroups":{ + // "name":"some_device_group_name", + // "permitOpens":["localhost:3000", ...] + // } + // } + // } + app.get( + '$pathPrefix/group/:name', + (req, res) async => + jsonEncode(await api.getUserGroup(req.params['name'].toString()))); +} diff --git a/apps/admin/admin_api/pubspec.lock b/apps/admin/admin_api/pubspec.lock new file mode 100644 index 000000000..dbef71f77 --- /dev/null +++ b/apps/admin/admin_api/pubspec.lock @@ -0,0 +1,970 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" + url: "https://pub.dev" + source: hosted + version: "73.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + alfred: + dependency: "direct main" + description: + name: alfred + sha256: "61c74bfccd41447ddb7eb76e0e1204e1b1da0c099d441d00fa8d2b86de5415e3" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" + url: "https://pub.dev" + source: hosted + version: "6.8.0" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: "direct main" + description: + path: "." + ref: "gkc/show-aliases-in-usage" + resolved-ref: ece6d42302acc5bae7a4ba793440ccb0945d48f5 + url: "https://github.com/gkc/args" + source: git + version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + at_auth: + dependency: transitive + description: + name: at_auth + sha256: "28f72f0fc26ec7f5f58d28fd29f964c9b2b35ecdc8dd4805ed7174851da2cbcc" + url: "https://pub.dev" + source: hosted + version: "2.0.5" + at_base2e15: + dependency: transitive + description: + name: at_base2e15 + sha256: "06ee6ffba9b3439f1c41f9bf0c01f579ce0a8b25f42da8c374ba3a14d721937f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + at_chops: + dependency: transitive + description: + name: at_chops + sha256: "825171a3132b3756119bd16b6fd1fa6257f74a64babaf13cae2d82d53b8c6be1" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + at_cli_commons: + dependency: "direct main" + description: + name: at_cli_commons + sha256: "23db4c959e5cefdc8dbcfb563172eeee1c3c42a16974cf2f6df5fa2d8b91747a" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + at_client: + dependency: "direct main" + description: + name: at_client + sha256: "12b7f5cacbb726e33a76ed4d069cb552df1333d30026cb9237f3b8b83bc0e6e4" + url: "https://pub.dev" + source: hosted + version: "3.0.78" + at_commons: + dependency: transitive + description: + name: at_commons + sha256: "2d0490a0c5bcd43c6a37911d85b71c133767aec47abc65bd8ecb20c8caaddeab" + url: "https://pub.dev" + source: hosted + version: "4.0.11" + at_demo_data: + dependency: transitive + description: + name: at_demo_data + sha256: bbaa979d9e6552472c5f5c755ebd2fef8c9c965140219f1faeaff1c7bb4e6ea7 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + at_lookup: + dependency: transitive + description: + name: at_lookup + sha256: e989099d5f2cd6415097c8e4353340bd2048c9ee1bc82665f2b4f7c4615ad055 + url: "https://pub.dev" + source: hosted + version: "3.0.47" + at_onboarding_cli: + dependency: transitive + description: + name: at_onboarding_cli + sha256: "6728eccbf2d89a83d872ef82f10ec36b0174699cfc1dadaaf6a9a53b35773115" + url: "https://pub.dev" + source: hosted + version: "1.6.1" + at_persistence_secondary_server: + dependency: transitive + description: + name: at_persistence_secondary_server + sha256: "1ec73b56e61b8aee94104ad4610c17cf07e366239337bedd43fa80c7765a391d" + url: "https://pub.dev" + source: hosted + version: "3.0.63" + at_persistence_spec: + dependency: transitive + description: + name: at_persistence_spec + sha256: ea8e550368ccee9150247ae7abdc256dccf78467bb48c11d1d0d66b843b21ba7 + url: "https://pub.dev" + source: hosted + version: "2.0.14" + at_server_status: + dependency: transitive + description: + name: at_server_status + sha256: "316c3e6717592677207d4f0a836b013271ca0f729e8b575c9195d19cfc57e71b" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + at_utf7: + dependency: transitive + description: + name: at_utf7 + sha256: c88e964e307bfe0e53e0048cff1ebf5ab60e23ceb4273f1ca664e724a9a5c5c9 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + at_utils: + dependency: transitive + description: + name: at_utils + sha256: ec28600e4eec321ee5e22be051109fa7b2e94590dc51d9f957730c2540beb681 + url: "https://pub.dev" + source: hosted + version: "3.0.16" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "0b7ec5c6a6bafd1445500632c00c573722bd7736e491675d4ac3fe560bbd9cfe" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + url: "https://pub.dev" + source: hosted + version: "1.19.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "576aaab8b1abdd452e0f656c3e73da9ead9d7880e15bdc494189d9c1a1baf0db" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + cron: + dependency: transitive + description: + name: cron + sha256: d98aa8cdad0cccdb6b098e6a1fb89339c180d8a229145fa4cd8c6fc538f0e35f + url: "https://pub.dev" + source: hosted + version: "0.5.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + crypton: + dependency: transitive + description: + name: crypton + sha256: "17b6631fbf89e389d421b46629132287ed37d601b2ad1357445826ab85022271" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + dart_periphery: + dependency: transitive + description: + name: dart_periphery + sha256: "03fef538e07124346ca89e214c34e505e6ff2d2766d7927cac099bae53601113" + url: "https://pub.dev" + source: hosted + version: "0.9.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dartssh2: + dependency: transitive + description: + name: dartssh2 + sha256: "53a230c7dd6f487b704ceef1b29323ad64d19be89e786ccbc81e157a70417a56" + url: "https://pub.dev" + source: hosted + version: "2.8.2" + ecdsa: + dependency: transitive + description: + name: ecdsa + sha256: b71687a843151255fced9fead63b09816cc59e9ae7b954e6a852bdc344ae1aca + url: "https://pub.dev" + source: hosted + version: "0.1.0" + elliptic: + dependency: transitive + description: + name: elliptic + sha256: "98e2fa89a714c649174553c823db2612dc9581814477fe1264a499d448237b6b" + url: "https://pub.dev" + source: hosted + version: "0.3.10" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + internet_connection_checker: + dependency: transitive + description: + name: internet_connection_checker + sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: "direct main" + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + mime_type: + dependency: transitive + description: + name: mime_type + sha256: "2ad6e67d3d2de9ac0f8ef5352d998fd103cb21351ae8c02fb0c78b079b37d275" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + ninja_asn1: + dependency: transitive + description: + name: ninja_asn1 + sha256: b0f04877243fda51c475ec2bcaadb55a92759baee9f02888124c60775760ccf7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + noports_core: + dependency: "direct main" + description: + path: "../../../packages/dart/noports_core" + relative: true + source: path + version: "6.1.0" + openssh_ed25519: + dependency: transitive + description: + name: openssh_ed25519 + sha256: eb65bfb9158c05c294d653f639bb9b4b18aca6dfd010986e4f325e439a93655f + url: "https://pub.dev" + source: hosted + version: "1.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: "3a5503637587d635647c93ea9a8fecf48a420cc7deebe6f1fc85c2a5637ab327" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + queue: + dependency: transitive + description: + name: queue + sha256: "9a41ecadc15db79010108c06eae229a45c56b18db699760f34e8c9ac9b831ff9" + url: "https://pub.dev" + source: hosted + version: "3.1.0+2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + socket_connector: + dependency: transitive + description: + name: socket_connector + sha256: "3c641546699aa58e9ab8be9841627a30af3c1ffcf4461ca5d00d7c56392ab63a" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + url: "https://pub.dev" + source: hosted + version: "1.25.8" + test_api: + dependency: transitive + description: + name: test_api + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.dev" + source: hosted + version: "0.7.3" + test_core: + dependency: transitive + description: + name: test_core + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + version: + dependency: transitive + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + zxing2: + dependency: transitive + description: + name: zxing2 + sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c" + url: "https://pub.dev" + source: hosted + version: "0.2.3" +sdks: + dart: ">=3.5.0-259.0.dev <4.0.0" diff --git a/apps/admin/admin_api/pubspec.yaml b/apps/admin/admin_api/pubspec.yaml new file mode 100644 index 000000000..865e7a1ed --- /dev/null +++ b/apps/admin/admin_api/pubspec.yaml @@ -0,0 +1,32 @@ +name: admin_api +description: A sample command-line application. +version: 1.0.0 +publish_to: none +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.4.3 + +# Add regular dependencies here. +dependencies: + alfred: ^1.1.1 + at_client: ^3.0.78 + json_annotation: ^4.9.0 + at_cli_commons: ^1.1.0 + args: 2.5.0 + meta: ^1.15.0 + noports_core: + path: ../../../packages/dart/noports_core + +dependency_overrides: + args: + git: + ref: gkc/show-aliases-in-usage + url: https://github.com/gkc/args + +dev_dependencies: + lints: ^4.0.0 + test: ^1.25.8 + mocktail: ^1.0.4 + build_runner: ^2.4.12 + json_serializable: ^6.8.0 diff --git a/apps/admin/admin_api/test/policy_api_test.dart b/apps/admin/admin_api/test/policy_api_test.dart new file mode 100644 index 000000000..3977750b2 --- /dev/null +++ b/apps/admin/admin_api/test/policy_api_test.dart @@ -0,0 +1,261 @@ +import 'package:noports_core/admin.dart'; +import 'package:at_client/at_client.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAtClient extends Mock implements AtClient {} + +void main() { + MockAtClient atClient = MockAtClient(); + + group('core create retrieve update delete', () { + PolicyServiceWithAtClient api = + PolicyService.withAtClient(atClient: atClient) + as PolicyServiceWithAtClient; + + setUp(() async { + expect(api.users, isEmpty); + expect(api.groups, isEmpty); + }); + + test('add user', () async { + String as = '@alice'; + expect((await api.getUser(as)), isNull); + + await api.updateUser(User(atSign: as, name: 'Alice')); + expect(api.users.containsKey(as), true); + expect(api.users[as]!.atSign, '@alice'); + expect(api.users[as]!.name, 'Alice'); + + User? u = await api.getUser(as); + expect(u, isNotNull); + expect(u!.atSign, as); + expect(u.name, 'Alice'); + }); + + test('update user', () async { + String as = '@alice'; + expect((await api.getUser(as)), isNull); + + await api.updateUser(User(atSign: as, name: 'Alice')); + await api.updateUser(User(atSign: '@bob', name: 'Bob')); + + User u = (await api.getUser(as))!; + expect(u.atSign, as); + expect(u.name, 'Alice'); + + await api.updateUser(User(atSign: as, name: 'Still Alice')); + u = (await api.getUser(as))!; + expect(u.atSign, as); + expect(u.name, 'Still Alice'); + + expect(api.users.length, 2); + }); + + test('delete user', () async { + String as = '@alice'; + expect((await api.getUser(as)), isNull); + + await api.updateUser(User(atSign: as, name: 'Alice')); + await api.updateUser(User(atSign: '@bob', name: 'Bob')); + expect(api.users.length, 2); + + await api.deleteUser(as); + expect(api.users.length, 1); + expect(api.users.keys, contains('@bob')); + }); + + test('add group', () async { + String gid = 'sysadmins'; + String d = 'Description'; + expect((await api.getUserGroup(gid)), isNull); + + await api.updateUserGroup( + UserGroup.empty( + name: gid, + description: d, + ), + ); + + expect(api.groups.containsKey(gid), true); + expect(api.groups[gid]!.name, gid); + expect(api.groups[gid]!.description, d); + + UserGroup? ug = await api.getUserGroup(gid); + expect(ug, isNotNull); + expect(ug!.name, gid); + expect(ug.description, d); + expect(ug.daemonAtSigns, isEmpty); + expect(ug.devices, isEmpty); + expect(ug.deviceGroups, isEmpty); + expect(ug.userAtSigns, isEmpty); + }); + + test('update group', () async { + String n1 = 'sysadmins'; + String d1 = 'Description'; + String n2 = 'some other group'; + String d2 = 'some other group description'; + expect((await api.getUserGroup(n1)), isNull); + + await api.updateUserGroup( + UserGroup.empty(name: n1, description: d1), + ); + expect(api.groups.length, 1); + + await api.updateUserGroup( + UserGroup.empty(name: n2, description: d2), + ); + expect(api.groups.length, 2); + + UserGroup g1 = (await api.getUserGroup(n1))!; + expect(g1.name, n1); + expect(g1.description, d1); + + UserGroup g2 = (await api.getUserGroup(n2))!; + expect(g2.name, n2); + expect(g2.description, d2); + + await api.updateUserGroup( + UserGroup.empty( + name: n1, + description: 'Updated description', + ), + ); + expect(api.groups.length, 2); + + g1 = (await api.getUserGroup(n1))!; + expect(g1.name, n1); + expect(g1.description, 'Updated description'); + + g2 = (await api.getUserGroup(n2))!; + expect(g2.name, n2); + expect(g2.description, d2); + }); + + test('delete group', () async { + String n1 = 'sysadmins'; + String d1 = 'Description'; + String n2 = 'some other group'; + String d2 = 'some other group description'; + expect((await api.getUserGroup(n1)), isNull); + + await api.updateUserGroup( + UserGroup.empty(name: n1, description: d1), + ); + expect(api.groups.length, 1); + + await api.updateUserGroup( + UserGroup.empty(name: n2, description: d2), + ); + expect(api.groups.length, 2); + + expect(api.groups.keys.contains(n1), true); + await api.deleteUserGroup(n1); + expect(api.groups.length, 1); + expect(api.groups.keys.contains(n1), false); + expect(api.groups.keys.contains(n2), true); + }); + + tearDown(() async { + for (String gid in List.from(api.groups.keys)) { + await api.deleteUserGroup(gid); + } + for (String uas in List.from(api.users.keys)) { + await api.deleteUser(uas); + } + }); + }); + + group('group memberships', () { + PolicyServiceWithAtClient api = + PolicyService.withAtClient(atClient: atClient) + as PolicyServiceWithAtClient; + + late UserGroup sa; + late UserGroup ug2; + setUp(() async { + expect(api.users, isEmpty); + expect(api.groups, isEmpty); + + sa = UserGroup.empty( + name: 'sysadmins', + description: 'Description', + ); + ug2 = UserGroup.empty( + name: 'other_user_group', + description: 'Some other user group', + ); + + await api.updateUserGroup(sa); + await api.updateUserGroup(ug2); + }); + + test('cannot add an unknown user to a group', () async { + sa.userAtSigns.add('@unknown'); + await expectLater(api.updateUserGroup(sa), throwsStateError); + }); + + test('delete user when member of one group', () async { + var atSign = '@alice'; + await api.updateUser(User(atSign: atSign, name: 'Alice')); + sa.userAtSigns.add(atSign); + await api.updateUserGroup(sa); + await expectLater( + api.deleteUser(atSign), + throwsA(predicate( + (e) => + e is StateError && + e.message == + 'May not delete a user' + ' who is still a member of any group.' + ' Currently member of {sysadmins}', + ))); + }); + + test('delete user when member of two groups', () async { + var atSign = '@alice'; + await api.updateUser(User(atSign: atSign, name: 'Alice')); + sa.userAtSigns.add(atSign); + await api.updateUserGroup(sa); + ug2.userAtSigns.add(atSign); + await api.updateUserGroup(ug2); + await expectLater( + api.deleteUser(atSign), + throwsA(predicate((e) => + e is StateError && + e.message.contains('{sysadmins, other_user_group}')))); + }); + + test('test get users groups', () async { + var atSign = '@alice'; + await api.updateUser(User(atSign: atSign, name: 'Alice')); + + sa.userAtSigns.add(atSign); + await api.updateUserGroup(sa); + List groups = await api.getGroupsForUser(atSign); + Map groupsMap = Map.fromIterable(groups, + key: (group) => group.name, value: (group) => group); + expect(groupsMap.containsKey(sa.name), true); + expect(groups.length, 1); + + ug2.userAtSigns.add(atSign); + await api.updateUserGroup(ug2); + groups = await api.getGroupsForUser(atSign); + groupsMap = Map.fromIterable(groups, + key: (group) => group.name, value: (group) => group); + expect(groupsMap.containsKey(sa.name), true); + expect(groupsMap.containsKey(ug2.name), true); + expect(groups.length, 2); + }); + + tearDown(() async { + for (String gid in List.from(api.groups.keys)) { + await api.deleteUserGroup(gid); + } + for (String uas in List.from(api.users.keys)) { + await api.deleteUser(uas); + } + }); + }); +} diff --git a/apps/admin/webapp_proto/.gitignore b/apps/admin/webapp_proto/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/apps/admin/webapp_proto/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/admin/webapp_proto/.vscode/extensions.json b/apps/admin/webapp_proto/.vscode/extensions.json new file mode 100644 index 000000000..bdef82015 --- /dev/null +++ b/apps/admin/webapp_proto/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/apps/admin/webapp_proto/README.md b/apps/admin/webapp_proto/README.md new file mode 100644 index 000000000..382941e05 --- /dev/null +++ b/apps/admin/webapp_proto/README.md @@ -0,0 +1,47 @@ +# Svelte + Vite + +This template should help get you started developing with Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + +This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `checkJs` in the JS template?** + +It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```js +// store.js +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/apps/admin/webapp_proto/index.html b/apps/admin/webapp_proto/index.html new file mode 100644 index 000000000..e356a950e --- /dev/null +++ b/apps/admin/webapp_proto/index.html @@ -0,0 +1,17 @@ + + + + + + + + + NoPorts Policy Manager + + +
+ + + diff --git a/apps/admin/webapp_proto/jsconfig.json b/apps/admin/webapp_proto/jsconfig.json new file mode 100644 index 000000000..5696a2de7 --- /dev/null +++ b/apps/admin/webapp_proto/jsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/apps/admin/webapp_proto/package-lock.json b/apps/admin/webapp_proto/package-lock.json new file mode 100644 index 000000000..2453908f7 --- /dev/null +++ b/apps/admin/webapp_proto/package-lock.json @@ -0,0 +1,1113 @@ +{ + "name": "webapp_proto", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webapp_proto", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "svelte": "^4.2.18", + "vite": "^5.4.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", + "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", + "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", + "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", + "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", + "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", + "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", + "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", + "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", + "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", + "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", + "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", + "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", + "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", + "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", + "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", + "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.10", + "svelte-hmr": "^0.16.0", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", + "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", + "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.0", + "@rollup/rollup-android-arm64": "4.21.0", + "@rollup/rollup-darwin-arm64": "4.21.0", + "@rollup/rollup-darwin-x64": "4.21.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", + "@rollup/rollup-linux-arm-musleabihf": "4.21.0", + "@rollup/rollup-linux-arm64-gnu": "4.21.0", + "@rollup/rollup-linux-arm64-musl": "4.21.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", + "@rollup/rollup-linux-riscv64-gnu": "4.21.0", + "@rollup/rollup-linux-s390x-gnu": "4.21.0", + "@rollup/rollup-linux-x64-gnu": "4.21.0", + "@rollup/rollup-linux-x64-musl": "4.21.0", + "@rollup/rollup-win32-arm64-msvc": "4.21.0", + "@rollup/rollup-win32-ia32-msvc": "4.21.0", + "@rollup/rollup-win32-x64-msvc": "4.21.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", + "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-hmr": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", + "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz", + "integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + } + } +} diff --git a/apps/admin/webapp_proto/package.json b/apps/admin/webapp_proto/package.json new file mode 100644 index 000000000..1375d4ef1 --- /dev/null +++ b/apps/admin/webapp_proto/package.json @@ -0,0 +1,16 @@ +{ + "name": "webapp_proto", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "svelte": "^4.2.18", + "vite": "^5.4.1" + } +} diff --git a/apps/admin/webapp_proto/public/vite.svg b/apps/admin/webapp_proto/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/apps/admin/webapp_proto/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/webapp_proto/src/App.svelte b/apps/admin/webapp_proto/src/App.svelte new file mode 100644 index 000000000..caab93ad7 --- /dev/null +++ b/apps/admin/webapp_proto/src/App.svelte @@ -0,0 +1,18 @@ + + +
+

NoPorts Policy Manager

+ +
+ + +
Parent: Value is {value} Depth is {depth}
+
+ +
+
diff --git a/apps/admin/webapp_proto/src/app.css b/apps/admin/webapp_proto/src/app.css new file mode 100644 index 000000000..887414d23 --- /dev/null +++ b/apps/admin/webapp_proto/src/app.css @@ -0,0 +1,11 @@ +.table td { + background-color: aliceblue; +} +.table th { + background-color: lightcyan; +} + +.selected { + font-weight: bold; + background-color: coral; +} \ No newline at end of file diff --git a/apps/admin/webapp_proto/src/assets/noports.avif b/apps/admin/webapp_proto/src/assets/noports.avif new file mode 100644 index 0000000000000000000000000000000000000000..de30002dc6794e0af755f9bdaf0b8a8e457878c6 GIT binary patch literal 23376 zcmYJa1CS<7(>8jKZQHhO+qP}bj&0kvZDYsg?%1|<_Ibbf<77l-UWtnA?&zqf&aMOi z0GQ^kUXDiYR_1_zWM^g0WM^gc4+B}+nYkMMXZuG)mZrAO{|5m8M=KN8|A+sdgm<)Z zwfmm}^pDe8**ci~r-=dpz<~cO{|0L`06;$aPye3=6afIRfc(RpR#x`^OZ`6&+CRnY zzmWfC7`Zbt{RjVl<3G>;Gh$mgoBiKB007>|&C~XOnf}XFZ{_Ih_)kVzIhr{9lc-iM zj%NSlKLQH)$CUmJSR;2PkwAfe2mnBIwDPq24}d^ILjDu~=Oe)V&-CxU{?7;WpNI2b z9+CgdZqD}Hc8+Ei|6{`3CRQen+%8@&u4Z;F-2Y;%Y^{u(y||5R9W9Oi3&*o^wEG|U zhyOhf5TGCs05B*}2sju3pfzu2YUGLxfCeYntAB)1Wqvj@1!4q10zd)4KtMo%fPjCO z{h!wP6U`KrXr^yS7`oIYFNRd{XIAsmWer=_Irg|x3 zd-!a`WC`YmbKR&EdSLB&E@Y#lgz28jV5JaBt`7}On%*vMYmuCP$F-G;S69aPUb;3F zM9v`~d{)(VIt|P*>#$UU-3aBVBL}tg2E<{exx1F*(}2@kSX%&Y)6kMof40iuIIyfw zd*Y8M}}CM(f(6<(_vXMGJ2#9}eq^JSG=PzEPIO1XjgF4Ds${N3M1&BR)fNB7&jkzQ? z@FiyWIysD)ShbIu2zHS$d!D*SFc1Xy-$0#=CtuKw8+&9T_aH7$g*29KG_8fIKq#WL z+K0|R54X}Y4o+|pJm22O7t!3FG~sfZ<}mUXavrV7f*0|GcY%vX)-e8J$N{(IfBDt9 z2}uhnQyrXgOz{lA93XOdnr<3MrB@1mEPHOTyfHA+T>j@_1F< z{?(N`_LP(UTBzhGs336OAA}*wDPfsYvhRWUSAr1ctP3@_HSXUVTPU9YQnSN2hpikK z>j1(pw*gd|%Hhh^4_;pucz?Z{{-s-ehC9}y#oL0gnHPE4AZHY!Da_H_k-(GFo7lFG zar#*;6c7jW>*en*@Z~vvv#57LvVGzx6DZXXNp9(B>;XCyvKx|BBU`W z<@Ub3lgdWxV@}Lky@=fKL*n%KgFq!WoiENE9x76)_hMrlc(u}8=2PtRjBkdYjA&f2 zYl8|>!J1&Z3QYX_^obOH$c^-RKq0^#_1?VvVJSo-1i4q2l!rVyI%{MIZwjh}7?qp) z+Ulwd=C&h2ex=c*eXc3^jr4*1!}4=XS2T9AQmiY<3M`X^G7+-|1^X9iz`o?92NeAf z`#h`FwG>V&`ie=+6l<}9^N_S9hW~T$H2~9~ABr^icamvuR4To0%3C%>@98vjY+Ji^ z8=kStyQQTpW?6tqpf3CA7r=2f@T?}^JIPydYD;G_?Iike) zI`f=TGR{!(J{1gI`XPvK%&Dc|i^?ucx z+wPIpzfevsdHl;c`zPGhp{3O?LK;)e{-JZCN^X#y#Als@Aoj%Kad2=X{aRneU?rsL z)Yt}v@LsQ+4>v-uT8p=+Y;~<;qbX`{{oV49F3HM|odqJsf%>_Vh7ps9X}OM;q9~OF z)+ykt$%>Yn;wlL4&UgKBUIhElCk2|gd=0K(^{n3mhoLKD%=|;SzU6GfgH}6z0YlC_ zi!s8_B9g9k06rIf>jOX0MtuV6f-Omtdf5%a)&mHvLLyLwHF(TVx`}JYVS~Lm4LIng zx#(bV+9>ietdlW+_NdFngr?$KC&7H0zr!CzPsUu$iBDh12Qlfq%I=OU+fouQ%|hWG ze>JIMgGXvE@If)oFGi-+{&rY7ceJzyy3PF;&NZ}=L_aWw2i^$#5JR|3)V8s$gxiAT zg#2;*w2l6)p4j%-xLv&baU4?qR}dAI#zKcL3A2t1<{Ba@nVZo7o{8=FkHZEhQZSn=f$*)p8ncuD1}tOtdsS^a)Vvegh2|tp>>V=2r4(4v4}AVU!IVzw)lUj# z|KK4>Z}0|y`9V`s=onViOKVh6ov;*&Uplvd!UG+*zJUz$MnqM_B zs=*r4%=BoR|1@O zCB+CqwoG5bj!?J=+!0gZ?;vv+C&TVH)f9g>RP?{OuG9?37q2mhIh8{AGa>`po(pFo zocynFo;LBCaMn*O{$BFPM`iICj9EYxnshTvzrs0g$p&Zg?sFw0&Mso)F=7jK#Llxf zsbmSyF>O{fVZzt_*&71PGoTdt?e^afUV4($_M>LCi2tsX3lZ`#HwMy2)_3D{91evi zK8aq0-uNnu@z_W>T{Rfl03l@fAK`;B;(H}#HH(w9iPR@4PYOu zH0-Ak5EZn&7c1OE0v`n_Fo!PygV~fIJ5cAFg%Xv3%t^Y70mWG#Xy@u}}fApHH7mTh5XdMRdIsa|rVR~v= z?eu50`E*W(5sn{?IP63m!NP|@E+;(u(&ml8W9!d(@-cbUrGjDfDt;<%s{$XfMz?dI ziu2*4GZ#h(yae0)-MH3?zTU}NY;$l$qnCU8%B9hl{| zgudg5-acjk?%a3RMpLQ3=p8P6n~xcY`9ncg@cI?X{v0AQIEj9cSlHJ)t}iD7^7aS>*7Bx2<7E6mi#Ez4NJ_Bp z#DImnEOR6MjTNpty)qDH(*RFSQ?>aKC^u-@S5yeqoJh7_n*kW!qRV{R4 ze^<4927!T^m_V|U+^L7>@JRCw*3qFzVcjLYcihC%yRh`;S0pCND57AFDJRv=}H1G+zR~^Zm;ApuNMz0tooX%^u%x8!zc%^$x;BjhwWxi(i zR}p03qH@`AByLqlTUMQ0I#+A<)@JcIqvSYACY=aea(7B8>`=?6}8X&N#q{)tIa4?Vk%!McR;rGoN4Y>3);QDpk zyzWJ>QuDdc#GsI5JqYh>>0WAZm~{rhm()VT@TK#UIMx2KvP+8U3VH}^v)l@E_0)!Y z-S$L8b^|AaiJ3%(jyK-{F{Vn@!`GgQKcx%;t!hRh8{iz3%jQ2PE1gzUrXr zKMhZCA)e|GCEwnr360))nzs!iP}{{rqlhaPH0@HOu!LV_OV3WXY#h!LB>CcHDfb6g z<8is!P7ez=0++j}uuGzcl8q!jl4h7Qf`JpO=YZQRx2Y&T@0+tmUvMUw6+Z?wZwI80 z={%f)!@BT5mT9Lcp$lZX_fVDTco1dTbdK1Cq75)F?e@CmQLmy#(ATr>!pOzqvBJ8R zS8VZWn?9Tdzijf1D>X1S)jiPVdiU+GC3D*5vF&m98c2=L?8jYav@n;VLXiB>erIw!w@(^{` znzN0()_jvAl&x(ZSNEj53fcUTg&CawLxC zw&?q?OE?4)=u|+ins-gL;JzeC`zCnOE@>(4qxB;qiC+elw)kY>y(kt{TW@lsO5+;; zuCz&{Far@iCwn)l6Vj(Kw}!6!AQKl5iHEHYGJ$wys88<{-x!KcfpE^WBk3{phpAcL zmFS6-zW4GO#NH^f6fz5(Wgx9oFew%Z%9ys4iU*Q$rpwAMA;)df5qEZbxSZ$67yUky zf5!h#of+WJb#lnot%}fD{PJX)TwQCKJ_*zd_2AL^mrnfWRATI7xJR zSsU2LDlhh9dTST&SE6${wVz310y?9IhkWK52z*P71~(@1vd@j+`{wvwRT};KCU_kj4Xh*7uF{t)Cl$WT8T-Tb$|*OYLF}cN8t1rH3$Y58 zfY759qZ{^7QtNC*+0s(5;3-p%TK<{S^4XFd4Q#}u$G^yNNV4Rta`_uX7)0%LYUWM( z0xn9SAgR~0>lk$OTf{A8m?>8pq>-i`S~WrP{RxRs6Lm1t*gzQaT^3>QKjU#LA9oqDmRlM3-*@}{-Vw3q-6)& z@5G^L8(f*maLQULiyGi0i1TnVf$`v5!l_~J(nU59jVIwEd@2>4YJ1oEOYIR-<~G^# z;h{^b@`Bb)WsFN@sh`Xl6Q@QJprE;g4By6%TBpTSFn>wT{vSr&C%d`97w9#O*sfl= zIfqsP|mK=Y6!pjn;ak?9t z%_R6B&xrGknTEzx^eA}okFV0zKOam5*PbwHAe31tVt-wKau&o693pXKA?8gYKp8Gk z{abi)k)aAG9Pr)cNCpu#0h_vTigx&dNmJm?4=)uZs5IdI4=bi69(chf^-(^knS`cI znSq#I#E*nU=0HqW-Jh1lcQ~HxXF6_3h#p%#Fz|OWT;3earrV|%iMh5VG5(F%)Lbae z=)~85wnYn=CLLQicF|Q-f4MOXI#Q>x4WGTNOf`LCD+nz5o{gdEu8!zrD5C$7qYb3IPj1nPsxLcu zynMj( zyb{_<`XcP1Eed)eV@v!f5PSzQjk;IY|H@5e{<=?Dk~Z`b%}i1g9nOICdi1`TAl@ZH#@3cL{Q15eyu3x7h?CgF_9kD z!C?iL2jP6P47NyFc($ueCiIqUw<7VjHIJTk7E5_bsFDhE`UByoGs21f7umQNhgY;# zo{8{^(TrTNfDvW;5`w_@t4M~&YMTQ&I$L4yz1+>35(hdYjs_Q{2NTPHwVMd3^xHDE ztJ&dmh-3f(XzK%n6&PLb$3vfdeNP$sVn4-N&vcZ?(z%yvSj(KpE3Zu3+XVnDwY~}U z^)$^<+n}08m?7S0sT^7ola3v3(2oBdVtg&R4DH8xNdvzcO1VpZh?Z40?Q3+CR;t($ zcBqiDVbJ(@LVjp=kuGxqYO>wEtElg~^r)+K(2$YTqOQtU887CF(moUkTosGy54Z4e ze?cWNn8N!F=vMH8$wNY~w(Q)Y3M1KURmLX_x}`qV%h)AykV}cJ(X(=1D{}C(#&8t) zm(VA9=|IlO@eXh~Z=akZ$n2xEpLVFA`K3V-e>U36fep97=Am)@5ev1kPr)=X*X{lh zync`iNNTzXVzJ)N?<=2=urjTLLSxppm;uuwdRjecG_cU9jcN%|8YcQ?yN^~|9uBl|$$^x;^+Zbshp^`8 z79ZPrt~QaP$Vvw+X)9V}{03rI;J$3L46@e<+j<$t^>6>F*k%3mQ{C+68I{jj|!^;C^MfiHr!pTCWe7V@K z%nhmHSqKenXXf?$Q=)eL!i>ym8#LS2Sz3W2@W0|X=+|Gn=V7=TsXohhikW=Tf3T|Y(=<79KWTs+zkB(O3 z&Qx)uQlyRzo@apQ#+zc*S@o=NKWN)aP$1>y8E~N*Uc67-bAaWaqP^>Gx8qZ}-q zI8BHBR^v$*UoxAYcE}F3&>2*yUa44~?jQLLPv~U^c8W|xYSSs1MirpW0G@4_t~pdc|LT1R3+mM$04P8F#4NFt$~ zxtz(FX;frKIhM>#vo8OFE{z+zJk}CYZd2<=&K_5di90Nd>Uy7NhF@;I^eo`2D6Vgo z7vzJzaXagilGV2y&4ggFTub`f*i)-h9@VM-q629ETzSSyVzyN*%ZTIt)t8-KFwm z(vP!QwoNYxg1d@YkLeyW{PIU>n~e8NbPy=liwBR_QGGHC%E5p#F@8{<903h2*g$dW z2K2lCT3^{E{Y_4$VG%*cAitUe-;aNa9rym-DIO63zzPl+f^fuC@OT~PRm$n8BpF15 zeRop>#Vtm58cgnZYIQRtD1(kNA*1%DE00bUCB$%X*@H*d?q_QvV+b+}S&VKh?EcA6Mb=;qC0VvsS4Np5&zX!^rXbsygMgR*98JENoC%xol_<^o1 ztJL%x?(5q+lnAyevHEa*B9W6?^v`BJxPpjO==^{UNIhQ8F)gpO9^b@sFZ9x|uy;5M zFn1V!UyV#*BZa0x2Pa;jV+-Hld(T}_8;bPRLFOq#{%D;|1Dj<${%X{v9xoq=sGTn? zr9MvGcqnDKOOoBdgI)_R8Pg2l+Mqg^S3ka~1M9q>9G@wRgRA=3y(vFxs3YwJ)wFNi z-PURa?Tn^RcPJ6d%K;}WwACxn??=x*sm2wOW)ivF=|7`CdXiB5>}@!?*CH^e^;q#| z{7T3N2&pM}yG_pS!Y2@&k;bx5XzJP_w%IEF-e~)7(AO;rxy^te%T{!=(~!61sfS_x zLT#2PK!y`)rZYC!Go(89i{N{b%~4EjR*Ot&W0?%>Ax&5Len4}OCed&C4yH3!|Idgb z<2$-#E>$kWewHsonPs9isy{#&Bo!Rm_jojr(#L4!X&=qTTmAQI&@;@JuOWT0b2mF& z30^_D?8lEJugTEfS=p4jBg_)sjG2g4pl3*1)s4!OKpAMX%+GV#TR{o0_R#xzIc|eS zi?hK=L3pr|5+TG=FOZ1`*P?U3rHAR3mR%ug^CRBUFz1w)S3@hB@Sc)08;7|KLd)H* zHznhGE?nbWb;8tjvk*0A*zsaOwiuFhKVUNag78K?xN9ev=}~e;4dK~+HeV=8i%6+4 zU}lU3^maO_$MwMqjOyIcOatyJjDW;gOd(~)TP3_Saol=-1H%6mU~XCQVsT#37+Oza z0I#<{CTjI;kMG7***wu6bCI_@*lv`rwO&u;bEhhhsee>SrJ~K%n3h||5Ml>5eMHt1 zlCrrzkgzDSG9(EsCYZS#G@gD+m`ESz`rz4N6=xL90Azp6p!XqWXg#GYr%yc*kiPzI zu9}kWn_zv;N;^`^28kX3PtOqmeYA{5*iZHLufQW`VHf6KKnn>kY0XX6v`-lsa@p9< zJ2|WL-$8UwXcWLraR7Y0*&fCa)(GK4+faOSSm~q~yUyFm0lJ!ez%}$(GT%1OhN!U^ zoPYTud3oSKLWE;t4xf8JF3SdD!Q_%wm8abQ8s7c1V@tUq4p~DP=&fOlw1rx+k;cr3 zp+@JSDveJBqxOoYkWwK7HA}{C$PDQ*M}$jF6|5;v9%UMWs*Q?=1&j{_LLYcRpKJX4 zws|K)DFyrMc56j#i1=kt7v<;|hJ-aK>r8sYP^Ic2i#Q>nRGbSk{L^1fk9ytt)wmpr zR7J(g^}<`-&u-V17|1)FAT>a&VlC3@Ik<&nWfd-XKQBYPDU4?nWxlbvf;8H^esJ?k z-Yey`5^R_AsB_Y?6{8{dG7OHGIY0gRUVnugdZ|!RvIPQCsf)7er$xfoph>kX+qv-3 ztcs@E13Do|Fi)2*T6CkQ^K2ZV@AA2f0z!ZGQUhpx1uc@8=JV;Lwwgkd z?bH|u)S|NfK-=bD^}YjHAe+H3#R7ovu+{ zQtNs2qSpKC2S3l%`3TH9AuTf+e1Vr0W^_(&c z_^Aaz`dQh>QDR=u_c!uxHv=&K^hgKN*prHxpc{)4nAb$83{I7hB%G2Mc=6S^F1KP> z?zDZfxbgRmK-LtvVCbo(Sc({`azxh{E-?5*#o$Tg(xBTK$nHCsv>6c)V3j!&%46fD zm~dbcJuw#IKDGVKY!;jvH8Gbj?wDJ*hnyKDL7x%Fhk$lsvN^$Xsy*LzJ@?2u6;mN6 zTOqc7n&i}Fnk^$J4HFWlKgy}Zcs^{R9Z^AUKY%O{A?0GRNYHg~4h29ZGD`V~8>xNd z03cT|;?s=#_Bxn3Z<9+5t@X_}CGBEgn^LH>+YKp}8b74FC{`=#hM2?UT?W#4n!R1j z>-$45Ph0Frp6(O8dVRDEWb*#bN4nu#P_}c@7#P~tt41uZo5%Xp_;vk)6X;S$gqi5- zahkEow&K4AyKfQ||F;y0D~gQ$maAj<`T3d1QQ4OhsYAg}JAcK<0^VNQg1h3xTpTIN zEib{!+2lF|f-af8Y#xpCLw{q<X z*;~6DH&cONFM4a0!Kz!K@`I`}kK!tK`Py%h3!CAGD=1kxo@s4V2;Q~Hu*fS24jA@M z2L(G9uJX|#0}VW;t=nRN7yh?vLJdX@;3tTJ_D6upexHkZy>D1mh=9dbM7oh`C%|?m zHLOxkG0CHm{(1N5WN4#`xfKU8xghlsb}3v7xeT!&Nx3Pq=YBjZ#z^C7!$``(qH?V> z&PC2Z=424j2pU>;I+1gS@7@&1&)Yk|8LVz2L(;sJWy4H1CQ{nT5C;0&&OUExO3%Rl zgXvSfmp+tOm33GdC^5{ZOo_oS%D~%yI(??%)UA)ENIjeY;ReUCpvlw5?%5-Yf>{50 zMs&t#YB9FzxeOcby^sNc_xcQ?^xHI67O()qyV0V9YRp>N$BiW!K;Fw)cDEcVx@OjS zz^?Iq#qw5WZi<*^F3S@DGV_Ig`AT&vsMO14k=|T8E!W;zhZnQF7VLEJHXR~@dD z=nO7p4a_NGJWDHDrU>Wgd(f77>a(JPny)vz_eU4B)SIvjy6`bYoKS9hh$n%RX^@p} znP$-QQ#BvuouZ<->hk#7CYuTcH1D_5QBLYX=4~XT2n#!1)w- zW}bs|YNXE6TvU%;PkFdaxa9`qZ##_pT)TdF!6QkumQ45Cmdq<)mgk0WHR70ziVYM=@KU>6uE0SCHI zEVueTVxya(Eh|q3kVTK=@bY->QyeZ?$CepgLgzz$PN0IRk7(Ksw!c}ay_GA-mcYcm z?=t;{58jKCHEWjqf(Y#Y9ELCEO-!Se#Q9o$O$GeR~g?|+yJyfw{qujJCx~-j&X(W`k?vREM$RRPVr%PUS*9~76 zDr^|TRZhNr>d0jRd%U?BIQh28doT-&~)z7Y&anG=m0L+;^- z?961dTwTj8#KwV(ajq?8*BKooV>ApIKC%WnMwNda40gA+>+WI+l}@%W1yD; zc5!QMRyo;d3bE#_FXvdgEW|mEaV#`#fyzGmb-T3}E>V>iy)y&a|(^5=;@P5;|$O!-)!2@W(70#XP`Irp>GG9t6j4 zo?(ggsHe`1GV!vPGddK9I>!0bT`GV(-iK)3bq(G>d7w6__utcb+b57QD&?)L^TDfX zML75#S}byhu{%3AbA0{dUDjvSJ)~{mpCGT15Y&Y7OLS0mtw-CY4^AfVkdFulb7TAP z$A5*NI>bK3-0+7jSi|+V9M4NnTOcFrb=KXJ7fWn!*G9-um2*~EOcEMlpHfva+^_hF zP?{voNbvlD_2&!ZBTeM&o>bGi>qeYZ+kThKLGK<}B%;D<^=oO)J5qc4?a0%!i2|eN zUD7o$s$yRHfSh_VeLC=R-5$t=S*?wOJqV7^#HLDyd^ag_ST%HR19Q;iIs+33RnC6q zwMNwX$xM44mmzEbj}-V`qHr6h;H_j`jEDD^1zX;lwLvhhHA)_IAz**;rUpD;k z``~WaK7`^-Z}f=iSx-a9ZpU)vm2J)Q!uHgtM~T!P4G~VDdT<8%z@&Z7lcl}h>qU&w|<)?`|vK?AM%d_MZw!@WPV}s;n&!+|yqV9>dN@HlqhOaQeYmYf+uQdm4*f z=@7lw(xk$E8sST6_Z0(>qa&%uhW6gEf}jn5uX}JJK?Q$_h(SX1?va+}x{ty%fCp)% z@CFx%e5tEN@6rdXUE9d|jqA9F!^f=Y7m!a;_A}ZHpR~zB9 z)GY%b&gY*rCiYH8P{#af7YPYVuRI0z$7ZCq;skL&ZIJdl1;-rHr8XKmI3Y%~Q!QG+ zpCD4_s6=m8mYg!JoK%p86V=WXGaq;^*vi)^`)UNln(o`n>EkwO<0PL?eCqwmfa-HU z+0jNniubq)!?rnis#QAz5P8q#dd;N@qG)%+#bY99XSz5b7^P8awtOTj&WWGXZ1x7+^HM9=yqLM9Rp+3w{{{swawoR3}P7TBYFp^ z#j_K_jRzGr&t`|!$g44=a@>)VkCll%4%>2&E`>#fzLpu$1O2a$Ys#Uc= zkq*LJBpq5R!5b>~9b#$>Wx5+vmpG|IeP8@=!@I%9G+Kmm9-MoXwqIE>DoaFOznjX_ zPq<&$2hoDUAg7NE`QcA!7I%O?W-(_Qi%eNpnNNRj9iao9g*xt#eQ`V8G+^HxDc3_O z7n-m^sU1ei+l>R@L#S!F!*h123Aa_kFK{6f(8;uPtgU z2#SA=I_i6{vg2iqjVU|UNOhTL)5=c2cr)nXKN_j;O4q_3_8#Z5G(9}=8!k5VbxoRC zO0y)UsC~v?MM5Hkz(%Y0-x?hTi{c~>tvp0tO|H60?a6d+p1DK|E4S!M7bN&}F6OF0 z^hUr1X|4RR;VaFcee0o5h!(pS7}5+5WYM_YVdNq*ep1=na9Dj2DGD9O5!!Ap0nSMY zZgjAA7|@}{)v1+@=yVx_%8buzk(DmTr50Jp0XakAJUazBUQpH~k*50gmVwXh*N#Y7 zKN;r$5shGxeHre)1lGfMz#?-h_8&`4ptg>TX8j1$F7=R}wOTz3_IJ+CHVRo&q&!W6 z&z@uSL|M;pwq9mqW<^hFWw&=bRR6FT+0sJDESZg$WaPZSJFa$tEe^AyIeg~|(s{HF?ayZu60d}eLM;QZdAR3r zI6JwpW+rQXz|!bo$bdW^B$w|h^`H(;8JxA!Ti{yI_UEV}HP;W#6OkVjT;cjwK%+6S z-@nP5fBdtKQu#@Di!=R~@1`vHR(#<;;%u;Jv~4iNcnUOr()GzP5a{3LSt)5nX^brkj;yG{V2YuK$aP&x*QSbF+xP5baS>L1M1#!eUGCh4@8$?d_t zC!iidH{-U7WF!|MfX2gv=5~&*_TEQ=Z-O(tk}Oeamf8Bc$o5CpOTy1Z6@ zj~vc6kF5a?^s#SCflr!0fQQ_U`2j&bmDbH8(B1ODI6Bg}L#|3GyXC|&L16{VC#tHK zpPJ^nYOKBf@dc2Pbr{yo_Dhb0v-RKVPAK=KDX`^EupO*2ra9%;}gFPv+ zk?D?&^ zXDt|nS$osPy7a-S%_p8&E)zeshjgOQN%EXouCp4Zy%lQ3rmqR~qHLAg2f&l~pJK+n z^hqso&Ml-BW zIqzOl+_l`-*A9Rrt&Jhx4K>&cvOmXc!FcZQovFdzAoqDZ40$Dq9&iiB^^c-QTd*Xj zid_ie;ef0%KCSS;mgG%3`p%6|^4IF;xSB_lt0bXZ=$X$NM5s@Zyt8wM)wM17$j02w zk3G7;F`PKqGP3|1n9{LS*r@zsh#ukZQ}XL2%*ke|LSbBtb|b?~PRkSd2>FiqG&HNf zIl-a5`-I^SYRy7(jQ}*}{B8*+>;6n*p~{74KrvC!6|2)a4$Y)|(Q#u%BO?ArZ%>JQ zuiEq5oCa7+DNtC6N>BFQu|5HuYEt}$d}YPi@&E)ZID^7%Nc2XmVOErn;R zk@ZJuD^!lhjN;xvs)(4|x7y;D!OxBnemi8R@u7klb#sY+{Acq9Glfjb+4GwkGv1%o z&?`3s-U6=5YD9P@znyr~#-nq1AO3r74w;DnMqr_{6OB4 z%yf@SR;)3hMH-mnZ#x34%4D^z(O;f4v$TvR8J=*4F`0umxHueh`hDt1h3#_t@gD6`jIuvhqP$F zmBe;WAn>r)Q5t<9P{^xiG^I|FDg17>9|^MpL6n;`Df+kP0_ztAfRglSB?IXEc{cLF>+$>~cE%3I6p7gVfLYs!Nq?`1| z=Ad`16)F*>au-hDhsTWvm7k`De@0ugKX*Ob66^~Kq1$s0w7%Fp6hZp=5K1pR%n8Xx zwvF4Ovk)g&DALh?WMYDI4KnRhtR~Ddfa2velSx)pO%jaOK#e8*m0CF~9T1-QodWm5 z)WERgc-_DSk00YV{i7}y(voc#CL^1TQ8=r&^IwAu0JL85UkePBz<(_;f+9jK%&l^! zK;Zv10R;dA0fYcV03;|VH~>*F2mpW%fJ6`anBElWa9L?Dvv8=88$@f3_MDA<=@c{G zqWM^uM>DzYOTxi@4MSM>L-A@s5mO+qJ1MR0Ai5gk>t>H{DAnl#&YsWtv&!j0{4;&} z(&FyR4Z;IMbfg2PjZaiv9ok90 z@-5sK^Rjoc5~SG)sgy54vjrwq{h)^Gt!MK9g%&9*q!xv&$882PTlsy4iTgZ<2Gw_F z9JxCEiDx^`IwBFz@Sq|~dQbnm9395PnGKoBm5A})9)BaMPU>@9xY&NZDLl-yygSsE zi_X%zNBf7Izu)HOv%TVl$M3gVxa#M1b*Sua+JTd2Wqv<)yrdKhu%%Nl!UURV$j{&> zve&RH(}$K`ozFCopY}&wR7Hc>fe%k_8(r*%!Oei0JF2V1&L_1uv1w##RtB@Az?EqG zJ`v;aUH*N+6a)LQE1Ga@%M>ymej(YDQ8+Tika*c1zFs#a1$<8_lO*(8AFV2)a6l31Qr({vrMpSNgo_|YF{dlWY#P)4(*_bN7gh-OfIqY5 z#xAgWTv0f>UFI);{lkLgDL(&x+ijwV3wd@uH+*-we&2*y&-(Oe{KTxn%{`xa$TSCE zaU;#Y1PV0daMcytWudoSvq9>PkgcIdNR7uuYDi3qZ)_WXtB9S$G%peoh+YdGH>=g^ zfg2HbGrlNj$YGOLc%KO_qr+MZUo~cIpk+Yt3Ho9+7MgYCk_ltjAlj^JIIHE>vSfYi zykv^A);;l`1GMrwOl6f`GB+0ov%$DAn}ft9b*VjRc9Vat6|@s}-ahDKgyHsV2FKP3 z&^#)8fDMh>+xhIS$Jtmch~`0s{)*|(b^B;Hr(l(U*nOndVx8#M+VD@M))DSfptYuY znK@z`H#~v=SIK>&R7pZ3+JHuHDrV5&q!S(owY?MIHWUnyp}5cZ-1%sfI2>h=Dz^x>p6c zJ1_S9cYb*vJH?7#9#qh=-j^#CshlDGntTAbCc%Dr2=!5Jc!H{eTiAIznbJvFzSw*X z9G;R6h(kVwk2`sOZo`2gLws*G%e#&HSM%TiHvMahRUOA}*nqz_dZ*}W@A7Dqr5EMk zaMAtI0SSauhtCwB-|l@{`Ax$VdGt*JnmPa1!JFB8A#u_oH(PcenqCz{CA|BUS~DG2 zAz`)?*cZnWu9;lEnE&d}n^hHlatr-^EQK|BY&DFfR@$KOmQ0l@aQ6UQpt2*MMO2%) z4mmoq>=c>>?Vm@)kuQU{h=1o}a4=?29_1D6Bp^0CIL3^!04%^fZ0B>Nz zypMTt7qO+-yTx5BI#SPGR1Hs-jfF-?fI3cE4EKvu|L(Ml&!@1$L>!#oa?>xQeBv;5EqB z;?n(Mekz88anL&nvfQP?#!9`C`8t!R%pG0G!C=H6i4zr7aF6kuPJ&*^#d)%XUtskm z1jFmu)nVCf1T$x{MhMF;ga@2hPs0nty@0gn&)CaHnuYL|clK)wrANo0xoI#I z1HNVS`(Vt@?j|&tiYGVm1u?BorbyX?>!RV6+BwFd3eaXCS-E^j-Ee+_3 zdpg85emu);fCe7t{z~Kn+V*m~00tOUIIMq=^$@=Iz_B%4X~c*BN_1O_F*1vMdvc`( zOv9|rB~Nk-!hspp%RfZJm041Y&&*U|;>&@vOC|nvX5+#{2D(aYU?2 zy@3=wuCZfjy^hzB>c{OkZ5daS8nC#8s`<+yc!b3k10+!KHu75^c5=wDk32}4)d$~z#zkRwt8T+cBepd06ON$TW`x)5hIa4+YnMPaAF!cg8t zC(te!ZQ1ARErfTmi+j+3_+4CwQk~o}QQd3xIOM68W!rUsZMh6Ujy8%&f%y{dm=d9G z?t=W9=C%qma8Jn?oKNb+VG(&QBE&=yJzT@!3}S+mHuVvu#WN+H%?l+cR=(3sd;tGs z6*1uQ+5R?xQKL>teK}seZ>%~6rJ#GBpA4Z<=uYU3Lnx3^b=cvQ;QES8EG2jA@A4Mo zOwPQhg=<|+mC+}ui^z1cOWfh+)zWeZfkwnXNd472w_SpLElV`J*ibw5%+2BY94`~XtN+xC z9m@O;5=bW&MPv4KvndW&e4cGf*Zb3^Lq?n$u@imNX~UFau=c6YHpG6%wJkmDY!n9A zrGnplx!3PYp2r{_<&GfPSWPuZ>%VxGU-$cQQJQ~W27^x;xnC*`(CQ`e#ylxXa(tpu z8gxPsIeDM=v2Q3-Rpm4Y?ly$-pJ}sWf+y=aeeZ`*{&q8Pp@}dVul~hzhbm=Wj794x z+Ca6!n|%izwN)*o#fr9?3GQRH01Sn$=lh5m;9`~Slm_7K=B^5=xis2hxv2ESESi_R z4Kog|Y7o|;KX^vz?@q9*YLiE|?sz=jz?ak7My=NqU}PB>YF=iFasDescR=G^$~rtJ z3C88y6%2f;O%t?gjhZFOAsJEkH|s#0nj(uh0)E*_!EVH0G?aGonK;Zv$>}%uMi%rc z{=K{EOiaUqCkqp``nhSboM-m@A`#nXKAHR-xxC|n1G$*)0YtdhwZQr;iK(3b`;ql#?)R_nmvxFX0YD^!g^}T?iCsAvEnfYJ_HBbg%uEHM@l$=}9bRb5*mx0@47q)z85(aT zxoce&-pP5V7%UqDc?sLNv2Y$7v-))-WBaJ>qDpGbt3tkQduTHK;O?06A$Q(bCXN%? zqwsM1XX_zNV6uLQvu!9t@?q^%&*!h81JaKXHyK$ayvWbX40F@yZ2yN0aiO+*;o zA~bZ^^it*vI?Y7=q1#x}r<$0Rf_?m^>JS=_owVh4YKs|)7BZQX>Rx?WY}O>Fz@6E@ z8}>1^F6V+3nF6|^?w(~^B+en-_6*4KpkW^8y301Nt8pz>> zjO|}UA)Tj}Ld7#<`}U0CVJln^`lz!mI}EhBb1@CMK)LseS>DS1X@$l#DC)aj%}vs>*^mib<&abRwV(x5NQhsDP)x1@kd#4n;-Wxh?NH%@%oNi+ zZdxMewNxI$Xwc*54^8$_I+Xe|EnnGjbNM<~oAhPZ!yHkr`~u)9o@;^x3w#+V(W~JF zZaBv!izPzP;>`SqM!T+NB6i5u4r#<&=<=(BXhnu6obyQ zxm&p*ZsVg{+gz%MeAaTcYl|2v@);qo#-@YY=S!M?6p1o=#}3R7_#yFzeCMA5qBrVE zOC)wle(2vjEom{5O=Q4vU>NS@nPQ|#2KK8$I4S2!1#K~~nUC`(X6N-8LlaXHP{-ebjLtPU}X zN2mSb9v+z*FS!p@jZey8wnaonh3~!DfHH6fY#?TuJa8~_ryqf^^OaX9bo5X*A&dHa zJ;LS)DPTeFMl|q;Pi&J)ztWYNUm^{`NxCxX9Marn+U|P~6UThp>y%NF*5Q+Y00I6V zI#|6~U!nXWsNPH#qlx2d>iN@KMSPKYZ9jP9ghKjQN<|a!P-!~8(r5M3o1)sX`a^a> zlYW>(U=N=4EFy1w(~on|eZI`?FwM3y^1hoo2M&n=|3-J6KDOS9Ep{ytS z5=JW(r&pOpJ%^sm>Eh>l+-~bie)i{L4}bRFavo`<0(cipK;TL*)*VZxGUF=K)d zG!Sp|i)rg*veL`U#?~K73vVdZU?nHSL2QM0IF}QiW<_o+!Nk_~a%~SV#jWeYs2+m| z2i1AzgWJn8V{J_QFcCAr}7UYqu{A6sKzRM^J+B;@?e22%ltcqXzs ztE6&`=N0+v7(BROr%GK=)A{8lLUSe@0sdI`Cop15V*+M4)NP$y3idCAZCtt+?!-N^ z?vw**vI^P)6F?o{x=i=gam$*y1h;jNh6D1P?5kH(;GERYI{0<)JB7cUAlDPey*iS#;AS8`!~@qO{3mr;Ql` zPNY4z*lpvSv`%!nk93<-V9X*fq)qishTd?kp&U2AyA z&sY-ekI$s6PpxO~NaCVsFl2zIvz%lkRJ{XgAEBGD0B$f3v&}akzIO13Y5@F5X~3Q` zm;b%qvlZJ@rt|*tXs$};b1&ak6>oRCVtP&5c&tt7HTCFeGL&0;T^ZU=9}OE)JV+D$ z*uGqzW*VvT=F0Tl#dxIoNt+R%YFQqd0zfC}N%Q14|H1rhAhhm+n|%kqQkn9*ncW?o zy=HbcU8bNPNU$>cSVl}K@H=DDfj_3*K2>3_X^FiGqa3r<&wJH zV1@)OGW(rNN2mipRp_0jcfjKk?c5sPCI+zUGZiYN)YLCZkW_$74vR%<9Ni+l4Bjil z4wt92?z}r=`TxQ9`SBiLcZcBzyKgQ>LHW`*%64i+shKzIVE+8cWP2&8 z-ifiF6HneY8>h;zPqYp4d%C6f)(J5z#d2)CYv>r4WbT?R`@T29`YKLWxzYJ!yz*cR zx!@m@W8fFY&UvN79Q4~Yad==q_gMUrnK-E7e-vCsq$^oay!drJul^}QvS;99BO z*RbBMj#njwz8uPe>*QJ;tlW0(7)E#GgeldIP#qt`Bu=<+$KU2ETchC0ait$T^O=L} zry9i}It`E_qO{o?cm*9D2RDm1HuT_oN!9+7-#8#8&5a7a`kw` z0}zoJ=neapT>mm|6vy-HAuVr)Bs15wQ)wGij2_v15DJL;s8cJ;R&tqn!~ihCc1O&q zqb#DM9RoricTQphWi~3zYZ<5gAmn_`QT4t`Rp%t&=EMmAjDyLCV~_|A;EgcVPx0uG zU4yA&JHA>;_tZ1y6i$W)78lF#^X`}8h;r$>1knYus1H1lDHUJ6X6=fGn@1w??<+r| zg<}z~WUk{6d=AU8SNPWE5`eK4fXBNNw<+f1}?sS(KY8DZ~)QaJR?MisRW!?@Q5a79)a+6mH-g5 z9y{FLLAR6FXJ!zZS+MD=BH)y;z+crsPx~)w+CN}dWlrN%vj8|R&AzD+vueb6epyWC z=}}Yz!BGkBAV?^@W-f_n61nWD3We8Z!Wz`<@b5H1(WqwuFw$!ZAo|2<6YB`JG- zI}rqsngE`p>clozCywN5yd?_zw|Ltu5{G(j@~Gif@Kcv`z(5n(6mwndnoENe+hs)G zVQRptYph^eYUnh+X?m)igjl3O_|TrT+|^DMGXMItOFFBTp3?TkOnX)13B(ffC%*8D z+Z6skQ&1K>t_6vB5YHe|);dbdImtTfHH4)&jK-wZn!l<(%R1Fny%#egJI0pg_7=|z z&)}QWA>WJ@i`KR~iqK(Aw2PTLbyRcSYN3Oi>vm}m=2)8FAK#mEIWbm$#v>V;bEiI- zA5(S>2KtQRJ`e8x3==93Ad-^JW3RpB*}W6y*U(Va12x~-1)|rMzP@y#Aim2lU+1`^ z6Or8eJq|-oPuvJ744wFF%z*F8T4yGc_nz?vem_55qXSCV_BUOy8dNu5l4A;m#tv&J zWa6RSMgh#V^dU_dDU51B_dU_VwdJfC@etxz-Z&*Oceb_~J*Ifh8ui<$k-6BU6T5!V)jCWUqs4+`<8VP`;``!$Md>oVs8xM=x3-ev7J)l!D zXeaLQd0@lXqz=V4*vA{(LDjPS;w_;L#M9&Y)&jRS7HrBW zib*uzJ$v{k0rEwgI80~q8`64Bpu3oD;0nOus$l$37Jmp*flMfds*`=5sT^{gFG zA*@K68Ij#6`dWzN2FY`KU&XZFiHgLt#ER9*LB|cIjT>7Uay)#%l^s{5^UnptbSt3@ zE@Hx|@c%a#YLwx!{t*jaBtG|Z38wEC$z={(@VIXeqrr~t+~?%}?;i$%UD7K`ksZY2 zVt-6(=LnDs91(gWTE;4f(J?@Y{IwM_t9=Ua+hxRa z%2%*hoY5w|vR!7sj10Q_@ywXLW{}y+Jrg@>q165~c2R*<2z^uX^db3^G?IkB+FJi; z!@eRm9JWFIVWf|GiMQ%8ZrOIHZbcsxaVb6O9cI75Uo|#&amSc^QH3VJ>s?kof|k!b zFKjPqkeQ3>q?|$h&5f8RUcY7tCZ;jhPX|yppjK?4%N2rkpFUI5MqH`keMV7T33!?z zf7ug~x(p7Q6d}d;i`~*#RE)p|m#Hrk+mkP1c5+S!z zvbJI%)Z7I5{AQVLFA@i@=j1LME7^y&{YExecGBC;IH<?}H)K{+P?veGXJ?>0r0z z;G;rFxye%KX~_Ja-KWtA>Dclhl$H%g{Qi|=?U4os#zcO9w6XGz;<(1)<-@_oNM z{m=+md$ZD9>x9Fv)42>1TW7>Hx%f`hh_knZY!kSPH>b5{lA3OZnSO=K5vp7TN7%l_ zB*>OeC#8Q-*YRX__P|*qsX&T`5F^8~7c2W-&U4{Gv}M+@F3ORO##Volt-OxHk0Eq@ z>NOw+Pdak!DQLbK&%`Rz8LGp6#^a#QY5M0fN)O`u6b6+gKH|2L)EhPYn<{p5i z$B0AQ`r1v2;_F6vj}&xsAZ;l*lE=Xfx`OyYo=z$84uxBjm>5=SO6DBWxSM<;{T|W=#zI8EEsC~)La8I zLU(G0@vq1N1DbgVss!I~tt4}5I%rw$<_QEtO~{K3^_;>!hSyq{_z1OF&z$G8=3KfQ zQO5@M`x8`^v&@%wlaXNNlpM0r499l4jQ$HX2j}TKQP@9&>8D_6i@7P9wN*+(|8uRP zQO{vwn3OZjkVJKixeb>u2+drU%I6Z%6I$h3_72x=Vp~tJ_RWJtvANKCbHJGsAR?O- z<-X>FGxY zv{8=UB%51J3seDH8UKX1*jWZC?G@*mDhe0b=~?<^l3LxY4eW&C4SPDF{I91mP2TSt zMO^v*J&~mK+#w*PkDO9cW4B7slnb9dwE<46b(}m-F%qxsq1{jJ*j*ci_*<$~w6L9o zd&fCDe$IxKctDMgVGKTc^;4$8pA7QHWC^uX{5iOT?H)|)O4U=jlrnuqitu0rSQRF6 zcp|SD1gSMrnno6J1+cnbu?B=soxK>3m89nC?D?Kqp%qalcPOv1u)<5>#Rhm{m?IKS z2rCe2Q^V+?eimieYAWuC8V_S20uI9@^Q9Q&P0rKA;Z1b}@06Ac%lgLRjaM3UeQ z&(`3mcmCNwVtm7h!=P)jUN!6r;_UA7{j-@=u;;LU{8Hz=%%c-U5~S{)WS+c7Zd9i4 zp-ZdEi8F{2t_}ljP^Yf8-}hLm3~XbXZ%WdUxD7wPBSBkx3s}uYi@{a{lG!Rh@zSl4 z+B?=tOGdK!ca==>^ULd|?*#`_E2W!l^DOu$v5Lo0S65W;gc5ei#g+`zp+&8kSNM}% zWl@Y+hCF3q?~boZ=a|Or1iBk~G9$T#ZTHZ!B&np__p0~F1adBKemtP{O(arQqablr zqA{vnS<$2)7e1v~VVz_(zvQIvzsxZVtXcbTk5k0-LOE2s!BW@G3mjyH>uVSg`wj(f zHp`&$pt(ekf+t-7sy4${&r&too7}!K-m`3#YFx!p+^_du7K~bpj|d|G0CxE^Me=h0 zVeiM1=^#TT^UkBKwWyvPxCU8^$!C4-1x~g-ATvX$<$iJ&I^vXeYAiE=rVUaHk&{>b z3bS5BHTI-I*1mQdC{P49nFQAbU=emRejuK)$-%zWV3ZJIh)O^q*;qu!s)qcFw0PVM z1eJn%{HFn3l_B#-0-E;Mk&sJEaW^r~x+LO@xo(_wlokgDvwu|ZqLatOQvCkX|Gp6E zctVbsfo*S{F9fA4MvQZ%0hZ5Y?oYOI{PSc9&b*M7A4s1vK5H{@@2O4(4uk@xa(10qOJSl@e z9PiwIqIEaG1ayaD!e%04R@O$J9!POyZ`+FJ912B)d>!AG#Ga98s;rMqXlj}^C7%T= zpAn@NL5d-lU?H0Gzt!-R%kt2d{Tt|LK??OErmL`Ap;amQJV%^JcqzQyUxu8Ag2edx zGeJ6>2j#4bluHA}Dm6A32;}73e0}SJTCB7T!;T6~Un}W!9iY1k3M!P-g>jSco^o&F zVTP6|GMBi8%0{gTXBa;fC88Dga1;QIWys-?EB%w#9g}w{?JC`+e@>+yCCUp4B3cxa zW0!HuELwaKD6GsSgg!;p^t@}w+`s)SlnFv)8q(Rz-zA5`-51D<8|Y1ThgB77mx~R- zn>_^e&RQ~k5wU^@SYx|NIuM-%GY(oi-Uqvj5hn!T1liw5S%;mM2hmkbZ)4@}Y=t(l zWVh%_DG~;tjDw|;vhdLu&wf)8+!Ng$q%ZV!7v7{ zYdOuJf$?&L>Qq0?o@Zkhtr$f`@L9?=L$&{3Jz`>zElTLk{S27}7L8XPV0N6ZPJ5Jq z`k4~7+TRgWd?W^a@&l%223>_(%HyTOuLQ5zudXpTwrCz|it5HI4mvs!0I`zOWF$Nyu~6`j*zuh4O|x%_bs zCwPOU0-&@I@6_ zd!nbE0*;eG8pq&v)Kdb1E)H7?D*6D+I4KLsZ#irsRdJof#4^r_r%nu@#jp}9ZwLOu oAZ@Hn)EFKafnE}%ZU~bX<0!C=L{q6~tN)x+ZAKa+iP*c5Kyz3eq5uE@ literal 0 HcmV?d00001 diff --git a/apps/admin/webapp_proto/src/assets/noports.jpg b/apps/admin/webapp_proto/src/assets/noports.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78aa3c6629e6477c12dfb66271d2133988f5e78d GIT binary patch literal 10748 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