diff --git a/.env b/.env index 612257e..70ff0aa 100644 --- a/.env +++ b/.env @@ -9,9 +9,13 @@ SERVER_KEY=/var/serving-cert/tls.key # replace the token in production deployment SECRET_ACCESS_TOKEN=1e13d44f998dee277deae621a9012cf300b94c91 -# to trust jolokia certs -NODE_TLS_REJECT_UNAUTHORIZED='0' - # logging LOG_LEVEL='info' ENABLE_REQUEST_LOG='false' + +# security +API_SERVER_SECURITY_ENABLED=false +USERS_FILE_URL=.users.json +ROLES_FILE_URL=.roles.json +ENDPOINTS_FILE_URL=.endpoints.json +ACCESS_CONTROL_FILE_URL=.access.json diff --git a/.gitignore b/.gitignore index dd38b46..3d2da4b 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,13 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# vs code config +.vscode + +# security files +.users.json +.roles.json +.endpoints.json +.access.json + diff --git a/.test.access.json b/.test.access.json new file mode 100644 index 0000000..c3e5d95 --- /dev/null +++ b/.test.access.json @@ -0,0 +1,23 @@ +{ + "endpoints": [ + { + "name": "broker1", + "roles": ["role1", "manager"] + }, + { + "name": "broker2", + "roles": ["role2", "manager"] + }, + { + "name": "broker3", + "roles": ["manager"] + }, + { + "name": "broker4", + "roles": ["manager"] + } + ], + "admin": { + "roles": ["manager"] + } +} diff --git a/.test.endpoints.json b/.test.endpoints.json new file mode 100644 index 0000000..3d94413 --- /dev/null +++ b/.test.endpoints.json @@ -0,0 +1,57 @@ +{ + "endpoints": [ + { + "name": "broker1", + "url": "http://127.0.0.1:8161", + "auth": [ + { + "scheme": "basic", + "data": { + "username": "guest", + "password": "guest" + } + } + ] + }, + { + "name": "broker2", + "url": "http://127.0.0.2:8161", + "auth": [ + { + "scheme": "basic", + "data": { + "username": "guest", + "password": "guest" + } + } + ] + }, + { + "name": "broker3", + "url": "http://127.0.0.3:8161", + "auth": [ + { + "scheme": "basic", + "data": { + "username": "guest", + "password": "guest" + } + } + ] + }, + { + "name": "broker4", + "url": "https://artemis-broker-jolokia-0-svc-ing-default.artemiscloud.io:443", + "jolokiaPrefix": "/jolokia/", + "auth": [ + { + "scheme": "cert", + "data": { + "certpath": "test-api-server.crt", + "keypath": "test-api-server.key" + } + } + ] + } + ] +} diff --git a/.test.env b/.test.env new file mode 100644 index 0000000..2b9e547 --- /dev/null +++ b/.test.env @@ -0,0 +1,24 @@ + +PLUGIN_VERSION=1.0.0 +PLUGIN_NAME='ActiveMQ Artemis Jolokia api-server' + +# dev cert +SERVER_CERT=/var/serving-cert/tls.crt +SERVER_KEY=/var/serving-cert/tls.key + +# replace the token in production deployment +SECRET_ACCESS_TOKEN=1e13d44f998dee277deae621a9012cf300b94c91 + +# to trust jolokia certs +NODE_TLS_REJECT_UNAUTHORIZED='0' + +# logging +LOG_LEVEL='debug' +ENABLE_REQUEST_LOG=false + +# security +API_SERVER_SECURITY_ENABLED=true +USERS_FILE_URL=.test.users.json +ROLES_FILE_URL=.test.roles.json +ENDPOINTS_FILE_URL=.test.endpoints.json +ACCESS_CONTROL_FILE_URL=.test.access.json diff --git a/.test.roles.json b/.test.roles.json new file mode 100644 index 0000000..1d86eb2 --- /dev/null +++ b/.test.roles.json @@ -0,0 +1,16 @@ +{ + "roles": [ + { + "name": "role1", + "uids": ["user1"] + }, + { + "name": "role2", + "uids": ["user1", "user2"] + }, + { + "name": "manager", + "uids": ["root"] + } + ] +} diff --git a/.test.users.json b/.test.users.json new file mode 100644 index 0000000..8b9cebf --- /dev/null +++ b/.test.users.json @@ -0,0 +1,18 @@ +{ + "users": [ + { + "id": "user1", + "email": "user1@example.com", + "hash": "$2a$12$nv9iV5/UNuV4Mdj1Jf8zfuUraqboSRtSQqCmtOc4F7rdwmOb9IzNu" + }, + { + "id": "user2", + "hash": "$2a$12$VHZ9aJ5A87YeFop4xVW.aOMm95ClU.EviyT9o0i8HYLdG6w6ctMfW" + }, + { + "id": "root", + "email": "user3@example.com", + "hash": "$2a$12$VHZ9aJ5A87YeFop4xVW.aOMm95ClU.EviyT9o0i8HYLdG6w6ctMfW" + } + ] +} diff --git a/README.md b/README.md index 59a06a6..1876fd2 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,55 @@ To undeploy, run ```sh ./undeploy.sh ``` + +## Security Model of the API Server + +The API Server provides a security model that provides authentication and authorization of incoming clients. +The security can be enabled/disabled (i.e. via `API_SERVER_SECURITY_ENABLED` env var) + +### Authentication + +Currently the api server support `jwt` token authentication. + +#### The login api + +The login api is defined in openapi.yml + +```yaml +/server/login +``` + +A client logs in to an api server by sending a POST request to the login path. The request body contains login information (i.e. username and password for jwt authentication type) + +Please refer to [api.md](api.md) for details of the log api. + +Currently the security manager uses local file to store user's info. The default users file name is `.users.json` +The users file name can be configured using `USERS_FILE_URL` env var. See `.test.users.json` for sample values. + +### Authorization + +The server uses RBAC (Role Based Access Control) authorization. User/role mappings are stored in a local file. By default the file +name is `.roles.json` and can be configured using `ROLES_FILE_URL` env var. See `.test.roles.json` for sample values. + +The permissions are defined in a local file. By default the file name is `.access.json` and can be configured using +`ACCESS_CONTROL_FILE_URL` env var. See `.test.access.json` for sample values. + +### Endpoints Management + +The server keeps a list of jolokia endpoints for clients to access. The endpoints are loaded from a local file named +`.endpoints.json`. Each top leve entry represents a jolokia endpoint. An entry has a unique name and details to access the jolokia api. See `.test.endpoints.json` for sample values. + +### Accessing a jolokia endpoint + +When an authenticated client sends a request to the api-server, it should present its token in the request header + + 'Authorization: Bearer `token`' + +It also need to give the `targetEndpoint` in the query part of the request if the request is to access an jolokia endpoint. + +For example `/execBrokerOperation?targetEndpoint=broker1`. + +### Direct Proxy + +Direct Proxy means a client can pass a broker's endpoint info to the api-server in order to access it via the api-server. +For example the [self-provisioning plugin](https://github.com/artemiscloud/activemq-artemis-self-provisioning-plugin) uses this api to access the jolokia of a broker's jolokia endpoint. diff --git a/api.md b/api.md index 90712a4..ecfc861 100644 --- a/api.md +++ b/api.md @@ -81,59 +81,181 @@ If necessary update the code that is using the hooks to comply with your changes ## Path Table -| Method | Path | Description | -| ------ | ----------------------------------------------------------------------- | -------------------------------------- | -| POST | [/jolokia/login](#postjolokialogin) | The login api | -| GET | [/brokers](#getbrokers) | retrieve the broker mbean | -| GET | [/brokerDetails](#getbrokerdetails) | broker details | -| GET | [/readBrokerAttributes](#getreadbrokerattributes) | read broker attributes | -| GET | [/readAddressAttributes](#getreadaddressattributes) | read address attributes | -| GET | [/readQueueAttributes](#getreadqueueattributes) | read queue attributes | -| GET | [/readAcceptorAttributes](#getreadacceptorattributes) | read acceptor attributes | -| GET | [/readClusterConnectionAttributes](#getreadclusterconnectionattributes) | read cluster connection attributes | -| POST | [/execClusterConnectionOperation](#postexecclusterconnectionoperation) | execute a cluster connection operation | -| GET | [/checkCredentials](#getcheckcredentials) | Check the validity of the credentials | -| POST | [/execBrokerOperation](#postexecbrokeroperation) | execute a broker operation | -| GET | [/brokerComponents](#getbrokercomponents) | list all mbeans | -| GET | [/addresses](#getaddresses) | retrieve all addresses on broker | -| GET | [/queues](#getqueues) | list queues | -| GET | [/queueDetails](#getqueuedetails) | retrieve queue details | -| GET | [/addressDetails](#getaddressdetails) | retrieve address details | -| GET | [/acceptors](#getacceptors) | list acceptors | -| GET | [/acceptorDetails](#getacceptordetails) | retrieve acceptor details | -| GET | [/clusterConnections](#getclusterconnections) | list cluster connections | -| GET | [/clusterConnectionDetails](#getclusterconnectiondetails) | retrieve cluster connection details | -| GET | [/api-info](#getapi-info) | the api info | +| Method | Path | Description | +| ------ | ----------------------------------------------------------------------- | ---------------------------------------- | +| POST | [/server/login](#postserverlogin) | Api to log in to the api server. | +| POST | [/server/logout](#postserverlogout) | Api to log out | +| POST | [/jolokia/login](#postjolokialogin) | The login api | +| GET | [/server/admin/listEndpoints](#getserveradminlistendpoints) | List endpoints managed by the api-server | +| GET | [/brokers](#getbrokers) | retrieve the broker mbean | +| GET | [/brokerDetails](#getbrokerdetails) | broker details | +| GET | [/readBrokerAttributes](#getreadbrokerattributes) | read broker attributes | +| GET | [/readAddressAttributes](#getreadaddressattributes) | read address attributes | +| GET | [/readQueueAttributes](#getreadqueueattributes) | read queue attributes | +| GET | [/readAcceptorAttributes](#getreadacceptorattributes) | read acceptor attributes | +| GET | [/readClusterConnectionAttributes](#getreadclusterconnectionattributes) | read cluster connection attributes | +| POST | [/execClusterConnectionOperation](#postexecclusterconnectionoperation) | execute a cluster connection operation | +| GET | [/checkCredentials](#getcheckcredentials) | Check the validity of the credentials | +| POST | [/execBrokerOperation](#postexecbrokeroperation) | execute a broker operation | +| GET | [/brokerComponents](#getbrokercomponents) | list all mbeans | +| GET | [/addresses](#getaddresses) | retrieve all addresses on broker | +| GET | [/queues](#getqueues) | list queues | +| GET | [/queueDetails](#getqueuedetails) | retrieve queue details | +| GET | [/addressDetails](#getaddressdetails) | retrieve address details | +| GET | [/acceptors](#getacceptors) | list acceptors | +| GET | [/acceptorDetails](#getacceptordetails) | retrieve acceptor details | +| GET | [/clusterConnections](#getclusterconnections) | list cluster connections | +| GET | [/clusterConnectionDetails](#getclusterconnectiondetails) | retrieve cluster connection details | +| GET | [/api-info](#getapi-info) | the api info | ## Reference Table -| Name | Path | Description | -| ------------------ | ------------------------------------------------------------------------------- | ----------- | -| OperationRef | [#/components/schemas/OperationRef](#componentsschemasoperationref) | | -| OperationArgument | [#/components/schemas/OperationArgument](#componentsschemasoperationargument) | | -| OperationResult | [#/components/schemas/OperationResult](#componentsschemasoperationresult) | | -| DummyResponse | [#/components/schemas/DummyResponse](#componentsschemasdummyresponse) | | -| ApiResponse | [#/components/schemas/ApiResponse](#componentsschemasapiresponse) | | -| LoginResponse | [#/components/schemas/LoginResponse](#componentsschemasloginresponse) | | -| Address | [#/components/schemas/Address](#componentsschemasaddress) | | -| Acceptor | [#/components/schemas/Acceptor](#componentsschemasacceptor) | | -| ClusterConnection | [#/components/schemas/ClusterConnection](#componentsschemasclusterconnection) | | -| Queue | [#/components/schemas/Queue](#componentsschemasqueue) | | -| Broker | [#/components/schemas/Broker](#componentsschemasbroker) | | -| FailureResponse | [#/components/schemas/FailureResponse](#componentsschemasfailureresponse) | | -| JavaTypes | [#/components/schemas/JavaTypes](#componentsschemasjavatypes) | | -| ComponentDetails | [#/components/schemas/ComponentDetails](#componentsschemascomponentdetails) | | -| Signatures | [#/components/schemas/Signatures](#componentsschemassignatures) | | -| Signature | [#/components/schemas/Signature](#componentsschemassignature) | | -| Attr | [#/components/schemas/Attr](#componentsschemasattr) | | -| Argument | [#/components/schemas/Argument](#componentsschemasargument) | | -| ComponentAttribute | [#/components/schemas/ComponentAttribute](#componentsschemascomponentattribute) | | -| ExecResult | [#/components/schemas/ExecResult](#componentsschemasexecresult) | | +| Name | Path | Description | +| -------------------- | ----------------------------------------------------------------------------------- | ----------- | +| OperationRef | [#/components/schemas/OperationRef](#componentsschemasoperationref) | | +| OperationArgument | [#/components/schemas/OperationArgument](#componentsschemasoperationargument) | | +| OperationResult | [#/components/schemas/OperationResult](#componentsschemasoperationresult) | | +| DummyResponse | [#/components/schemas/DummyResponse](#componentsschemasdummyresponse) | | +| ApiResponse | [#/components/schemas/ApiResponse](#componentsschemasapiresponse) | | +| ServerLogoutResponse | [#/components/schemas/ServerLogoutResponse](#componentsschemasserverlogoutresponse) | | +| ServerLoginResponse | [#/components/schemas/ServerLoginResponse](#componentsschemasserverloginresponse) | | +| LoginResponse | [#/components/schemas/LoginResponse](#componentsschemasloginresponse) | | +| Address | [#/components/schemas/Address](#componentsschemasaddress) | | +| Acceptor | [#/components/schemas/Acceptor](#componentsschemasacceptor) | | +| ClusterConnection | [#/components/schemas/ClusterConnection](#componentsschemasclusterconnection) | | +| Queue | [#/components/schemas/Queue](#componentsschemasqueue) | | +| Broker | [#/components/schemas/Broker](#componentsschemasbroker) | | +| Endpoint | [#/components/schemas/Endpoint](#componentsschemasendpoint) | | +| FailureResponse | [#/components/schemas/FailureResponse](#componentsschemasfailureresponse) | | +| JavaTypes | [#/components/schemas/JavaTypes](#componentsschemasjavatypes) | | +| ComponentDetails | [#/components/schemas/ComponentDetails](#componentsschemascomponentdetails) | | +| Signatures | [#/components/schemas/Signatures](#componentsschemassignatures) | | +| Signature | [#/components/schemas/Signature](#componentsschemassignature) | | +| Attr | [#/components/schemas/Attr](#componentsschemasattr) | | +| Argument | [#/components/schemas/Argument](#componentsschemasargument) | | +| ComponentAttribute | [#/components/schemas/ComponentAttribute](#componentsschemascomponentattribute) | | +| ExecResult | [#/components/schemas/ExecResult](#componentsschemasexecresult) | | +| EmptyBody | [#/components/schemas/EmptyBody](#componentsschemasemptybody) | | +| bearerAuth | [#/components/securitySchemes/bearerAuth](#componentssecurityschemesbearerauth) | | ## Path Details --- +### [POST]/server/login + +- Summary + Api to log in to the api server. + +- Description + This api is used to login to the api server. + +- Security + +#### RequestBody + +- application/json + +```ts +{ + userName: string; + password: string; +} +``` + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + message: string; + status: string; + // The jwt token + bearerToken: string; +} +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + +### [POST]/server/logout + +- Summary + Api to log out + +- Description + This api is used to logout the current session. + +#### RequestBody + +- application/json + +```ts +{ +} +``` + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + message: string; + status: string; +} +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + ### [POST]/jolokia/login - Summary @@ -141,11 +263,15 @@ If necessary update the code that is using the hooks to comply with your changes - Description This api is used to login to a jolokia endpoint. It tries to get the broker mbean via the joloia url using the parameters passed in. + If it succeeds, it generates a [jwt token](https://jwt.io/introduction) and returns it back to the client. If it fails it returns a error. + Once authenticated, the client can access the apis defined in this file. With each request the client must include a valid jwt token in a http header named `jolokia-session-id`. The src will validate the token before processing a request is and rejects the request if the token is not valid. +- Security + #### RequestBody - application/json @@ -206,6 +332,53 @@ If necessary update the code that is using the hooks to comply with your changes --- +### [GET]/server/admin/listEndpoints + +- Summary + List endpoints managed by the api-server + +- Description + ** List broker jolokia endpoints ** + The return value is a list of endpoints currently + managed by the api server. + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + name: string + url?: string +}[] +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + ### [GET]/brokers - Summary @@ -216,10 +389,20 @@ If necessary update the code that is using the hooks to comply with your changes The return value is a one-element array that contains the broker's mbean object name. +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string +``` + +```ts +endpoint-name?: string ``` #### Responses @@ -270,10 +453,16 @@ jolokia-session-id: string description of all the operations and attributes of the broker's mbean. It is defined in [ActiveMQServerControl.java](https://github.com/apache/activemq-artemis/blob/2.33.0/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQServerControl.java) +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -335,10 +524,14 @@ jolokia-session-id: string names?: string[] ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -407,10 +600,14 @@ name: string; attrs?: string[] ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -487,10 +684,14 @@ routing-type: string attrs?: string[] ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -559,10 +760,14 @@ name: string; attrs?: string[] ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -631,10 +836,14 @@ name: string; attrs?: string[] ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -699,10 +908,14 @@ jolokia-session-id: string name: string; ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### RequestBody @@ -775,7 +988,7 @@ jolokia-session-id: string #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -827,10 +1040,16 @@ jolokia-session-id: string The return value is a one element json array that contains return values of invoked operation along with the request info. +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### RequestBody @@ -905,10 +1124,16 @@ jolokia-session-id: string It retrieves and returns a list of all mbeans registered directly under the broker managment domain. +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -954,10 +1179,16 @@ string[] **Get all addresses in a broker** It retrieves and returns a list of all address mbeans +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1016,10 +1247,14 @@ jolokia-session-id: string address?: string ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1094,10 +1329,14 @@ name: string; routingType: string; ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1159,10 +1398,14 @@ jolokia-session-id: string name: string; ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1215,10 +1458,16 @@ jolokia-session-id: string **Get all acceptors in a broker** It retrieves and returns a list of all acceptor mbeans +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1279,10 +1528,14 @@ jolokia-session-id: string name: string; ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1335,10 +1588,16 @@ jolokia-session-id: string **Get all cluster connections in a broker** It retrieves and returns a list of all cluster connection mbeans +#### Parameters(Query) + +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1399,10 +1658,14 @@ jolokia-session-id: string name: string; ``` +```ts +targetEndpoint?: string +``` + #### Headers ```ts -jolokia-session-id: string +jolokia-session-id?: string ``` #### Responses @@ -1453,9 +1716,12 @@ jolokia-session-id: string - Description **Show all exposed paths on the api server** + The return value is a json object that contains description of all api paths defined in the api server. +- Security + #### Responses - 200 Success @@ -1465,6 +1731,9 @@ jolokia-session-id: string ```ts { message: { + security: { + enabled?: boolean + } info: { name?: string description?: string @@ -1537,6 +1806,9 @@ jolokia-session-id: string ```ts { message: { + security: { + enabled?: boolean + } info: { name?: string description?: string @@ -1553,6 +1825,26 @@ jolokia-session-id: string } ``` +### #/components/schemas/ServerLogoutResponse + +```ts +{ + message: string; + status: string; +} +``` + +### #/components/schemas/ServerLoginResponse + +```ts +{ + message: string; + status: string; + // The jwt token + bearerToken: string; +} +``` + ### #/components/schemas/LoginResponse ```ts @@ -1621,6 +1913,15 @@ jolokia-session-id: string } ``` +### #/components/schemas/Endpoint + +```ts +{ + name: string + url?: string +} +``` + ### #/components/schemas/FailureResponse ```ts @@ -1749,3 +2050,20 @@ jolokia-session-id: string status: number } ``` + +### #/components/schemas/EmptyBody + +```ts +{ +} +``` + +### #/components/securitySchemes/bearerAuth + +```ts +{ + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" +} +``` diff --git a/package.json b/package.json index f8ffa13..c2de6d7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@types/base-64": "^1.0.2", + "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "27.5.2", @@ -39,6 +40,8 @@ "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.4", "@types/node-fetch": "^2.6.11", + "@types/passport": "^1.0.16", + "@types/passport-jwt": "^4.0.1", "@types/webpack": "5.28.1", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", @@ -62,10 +65,12 @@ "ts-jest": "^29.2.1", "ts-loader": "^9.3.1", "ts-node": "10.9.2", - "typescript": "^4.7.4" + "tsconfig-paths": "^4.2.0", + "typescript": "^4.7.4", + "yaml": "^2.4.5" }, "readme": "README.md", - "_id": "activemq-artemis-jolokia-api-server@0.1.1", + "_id": "activemq-artemis-jolokia-api-server@0.1.1", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "dependencies": { "cors": "^2.8.5", @@ -77,9 +82,13 @@ "yaml": "^2.4.5", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", + "bcrypt": "^5.1.1", "node-fetch": "2", "swagger-routes-express": "^3.3.2", "express-pino-logger": "^7.0.0", + "fs-json-store": "^8.0.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pino": "^9.5.0" } } diff --git a/src/api/apiutil/artemis_jolokia.ts b/src/api/apiutil/artemis_jolokia.ts index ce9c425..a19e7d6 100644 --- a/src/api/apiutil/artemis_jolokia.ts +++ b/src/api/apiutil/artemis_jolokia.ts @@ -1,4 +1,11 @@ -import base64 from 'base-64'; +import { logger } from '../../utils/logger'; +import { + AuthenticationData, + AuthHandler, + AuthOptions, + CreateAuthHandler, + Endpoint, +} from '../../utils/security_util'; import fetch from 'node-fetch'; // search the broker @@ -40,97 +47,128 @@ const queueComponentPattern = 'org.apache.activemq.artemis:address="ADDRESS_NAME",broker="BROKER_NAME",component=addresses,queue="QUEUE_NAME",routing-type="ROUTING_TYPE",subcomponent=queues'; const clusterConnectionComponentPattern = 'org.apache.activemq.artemis:broker="BROKER_NAME",component=cluster-connections,name="CLUSTER_CONNECTION_NAME"'; + +export const BROKER = 'broker'; +export const BROKER_DETAILS = 'broker-details'; +export const BROKER_COMPONENTS = 'broker-components'; +export const ADDRESS = 'address'; +export const QUEUE = 'queue'; +export const ACCEPTOR = 'acceptor'; +export const QUEUE_DETAILS = 'queue-details'; +export const ADDRESS_DETAILS = 'address-details'; +export const ACCEPTOR_DETAILS = 'acceptor-details'; +export const CLUSTER_CONNECTION_DETAILS = 'cluster-connection-details'; +export const CLUSTER_CONNECTION = 'cluster-connection'; + export class ArtemisJolokia { - readonly username: string; - readonly password: string; + readonly name: string; + readonly serverUrl: string; readonly protocol: string; readonly port: string; readonly hostName: string; brokerName: string; - readonly baseUrl: string; - - static readonly BROKER = 'broker'; - static readonly BROKER_DETAILS = 'broker-details'; - static readonly BROKER_COMPONENTS = 'broker-components'; - static readonly ADDRESS = 'address'; - static readonly QUEUE = 'queue'; - static readonly ACCEPTOR = 'acceptor'; - static readonly QUEUE_DETAILS = 'queue-details'; - static readonly ADDRESS_DETAILS = 'address-details'; - static readonly ACCEPTOR_DETAILS = 'acceptor-details'; - static readonly CLUSTER_CONNECTION_DETAILS = 'cluster-connection-details'; - static readonly CLUSTER_CONNECTION = 'cluster-connection'; + baseUrl: string; + authHandlers: Array; componentMap = new Map([ - [ArtemisJolokia.BROKER, brokerSearchPattern], - [ArtemisJolokia.BROKER_COMPONENTS, brokerComponentsSearchPattern], - [ArtemisJolokia.ADDRESS, addressComponentsSearchPattern], - [ArtemisJolokia.QUEUE, queueComponentsSearchPattern], - [ArtemisJolokia.ACCEPTOR, acceptorComponentsSearchPattern], - [ - ArtemisJolokia.CLUSTER_CONNECTION, - clusterConnectionComponentsSearchPattern, - ], + [BROKER, brokerSearchPattern], + [BROKER_COMPONENTS, brokerComponentsSearchPattern], + [ADDRESS, addressComponentsSearchPattern], + [QUEUE, queueComponentsSearchPattern], + [ACCEPTOR, acceptorComponentsSearchPattern], + [CLUSTER_CONNECTION, clusterConnectionComponentsSearchPattern], ]); componentDetailsMap = new Map([ - [ArtemisJolokia.BROKER_DETAILS, brokerDetailsListPattern], - [ArtemisJolokia.QUEUE_DETAILS, queueDetailsListPattern], - [ArtemisJolokia.ADDRESS_DETAILS, addressDetailsListPattern], - [ArtemisJolokia.ACCEPTOR_DETAILS, acceptorDetailsListPattern], - [ - ArtemisJolokia.CLUSTER_CONNECTION_DETAILS, - clusterConnectionDetailsListPattern, - ], + [BROKER_DETAILS, brokerDetailsListPattern], + [QUEUE_DETAILS, queueDetailsListPattern], + [ADDRESS_DETAILS, addressDetailsListPattern], + [ACCEPTOR_DETAILS, acceptorDetailsListPattern], + [CLUSTER_CONNECTION_DETAILS, clusterConnectionDetailsListPattern], ]); componentNameMap = new Map([ - [ArtemisJolokia.BROKER, brokerComponentPattern], - [ArtemisJolokia.ADDRESS, addressComponentPattern], - [ArtemisJolokia.ACCEPTOR, acceptorComponentPattern], - [ArtemisJolokia.QUEUE, queueComponentPattern], - [ArtemisJolokia.CLUSTER_CONNECTION, clusterConnectionComponentPattern], + [BROKER, brokerComponentPattern], + [ADDRESS, addressComponentPattern], + [ACCEPTOR, acceptorComponentPattern], + [QUEUE, queueComponentPattern], + [CLUSTER_CONNECTION, clusterConnectionComponentPattern], ]); - constructor( - username: string, - password: string, - hostName: string, - protocol: string, - port: string, - ) { - this.username = username; - this.password = password; - this.protocol = protocol; - this.port = port; - this.hostName = hostName; + constructor(endpoint: Endpoint) { + const url = new URL(endpoint.url); + + this.name = endpoint.name; + this.protocol = url.protocol.substring(0, url.protocol.length - 1); + this.port = url.port + ? url.port + : ArtemisJolokia.getDefaultPort(this.protocol); + this.hostName = url.hostname; this.brokerName = ''; + this.serverUrl = this.protocol + '://' + this.hostName + ':' + this.port; + this.baseUrl = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/'; + this.serverUrl + ArtemisJolokia.makeJolokiaPrefix(endpoint.jolokiaPrefix); + + this.createAuthHandlers(endpoint.auth); } - getAuthHeaders = (): fetch.Headers => { - const headers = new fetch.Headers(); - headers.set( - 'Authorization', - 'Basic ' + base64.encode(this.username + ':' + this.password), - ); - //this may not needed as we set strict-check to false - headers.set('Origin', 'http://' + this.hostName); - return headers; + createAuthHandlers = (auth: AuthenticationData[]) => { + this.authHandlers = new Array(); + auth.forEach((authData) => { + this.authHandlers.push(CreateAuthHandler(authData)); + }); }; + static makeJolokiaPrefix = (input: string) => { + if (!input) { + return '/console/jolokia/'; + } + if (!input.startsWith('/')) { + input = '/' + input; + } + if (!input.endsWith('/')) { + input = input + '/'; + } + return input; + }; + + static getDefaultPort = (prot: string): string => { + if (prot === 'https') { + return '443'; + } + return '80'; + }; + + validateBroker = async (): Promise => { + const result = await this.getComponents(BROKER); + if (result.length === 1 && result[0].length > 0) { + //org.apache.activemq.artemis:broker="amq-broker" + this.brokerName = result[0].split('=', 2)[1]; + + //remove quotes + this.brokerName = this.brokerName.replace(/"/g, ''); + return true; + } + return false; + }; + + prepareRequest(reqUrl: string): AuthOptions { + const headers = new fetch.Headers(); + headers.set('Origin', this.serverUrl); + const authOpts = { + headers: headers, + }; + this.authHandlers.forEach((handler) => { + handler.handleRequest(reqUrl, authOpts); + }); + return authOpts; + } + getComponents = async ( name: string, params?: Map, ): Promise> => { - const headers = this.getAuthHeaders(); - let searchPattern = this.componentMap.get(name); if (typeof params !== 'undefined') { @@ -143,11 +181,23 @@ export class ArtemisJolokia { const url = this.baseUrl + 'search/' + searchPattern; + const { headers, agent } = this.prepareRequest(url); + const reply = await fetch(url, { method: 'GET', headers: headers, + agent: agent ?? false, }) - .then((response) => response.text()) //check response.ok + .then((response) => { + logger.debug( + { response: response.ok, status: response.statusText }, + 'response from endpoint', + ); + if (response.ok) { + return response.text(); + } + throw response; + }) .then((message) => { const resp: JolokiaResponseType = JSON.parse(message); return resp.value; @@ -157,19 +207,18 @@ export class ArtemisJolokia { }; getBrokerDetails = async (): Promise => { - const headers = this.getAuthHeaders(); - - let searchPattern = this.componentDetailsMap.get( - ArtemisJolokia.BROKER_DETAILS, - ); + let searchPattern = this.componentDetailsMap.get(BROKER_DETAILS); searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); const url = this.baseUrl + 'list/' + searchPattern; + const { headers, agent } = this.prepareRequest(url); + const reply = await fetch(url, { method: 'GET', headers: headers, + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -194,11 +243,7 @@ export class ArtemisJolokia { getAcceptorDetails = async ( params?: Map, ): Promise => { - const headers = this.getAuthHeaders(); - - let searchPattern = this.componentDetailsMap.get( - ArtemisJolokia.ACCEPTOR_DETAILS, - ); + let searchPattern = this.componentDetailsMap.get(ACCEPTOR_DETAILS); if (typeof params !== 'undefined') { for (const [key, value] of params) { @@ -209,9 +254,12 @@ export class ArtemisJolokia { const url = this.baseUrl + 'list/' + searchPattern; + const { headers, agent } = this.prepareRequest(url); + const reply = await fetch(url, { method: 'GET', headers: headers, + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -236,11 +284,7 @@ export class ArtemisJolokia { getAddressDetails = async ( params?: Map, ): Promise => { - const headers = this.getAuthHeaders(); - - let searchPattern = this.componentDetailsMap.get( - ArtemisJolokia.ADDRESS_DETAILS, - ); + let searchPattern = this.componentDetailsMap.get(ADDRESS_DETAILS); if (typeof params !== 'undefined') { for (const [key, value] of params) { @@ -251,9 +295,12 @@ export class ArtemisJolokia { const url = this.baseUrl + 'list/' + searchPattern; + const { headers, agent } = this.prepareRequest(url); + const reply = await fetch(url, { method: 'GET', headers: headers, + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -278,10 +325,8 @@ export class ArtemisJolokia { getClusterConnectionDetails = async ( params?: Map, ): Promise => { - const headers = this.getAuthHeaders(); - let searchPattern = this.componentDetailsMap.get( - ArtemisJolokia.CLUSTER_CONNECTION_DETAILS, + CLUSTER_CONNECTION_DETAILS, ); if (typeof params !== 'undefined') { @@ -293,9 +338,12 @@ export class ArtemisJolokia { const url = this.baseUrl + 'list/' + searchPattern; + const { headers, agent } = this.prepareRequest(url); + const reply = await fetch(url, { method: 'GET', headers: headers, + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -320,17 +368,18 @@ export class ArtemisJolokia { readBrokerAttributes = async ( brokerAttrNames: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForAttributes( - ArtemisJolokia.BROKER, + BROKER, new Map(), brokerAttrNames, ), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -353,7 +402,7 @@ export class ArtemisJolokia { addressName: string, addressAttrNames: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const param = new Map(); @@ -362,11 +411,8 @@ export class ArtemisJolokia { const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, - body: this.getPostBodyForAttributes( - ArtemisJolokia.ADDRESS, - param, - addressAttrNames, - ), + body: this.getPostBodyForAttributes(ADDRESS, param, addressAttrNames), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -389,7 +435,7 @@ export class ArtemisJolokia { clusterConnectionName: string, clusterConnectionAttrNames: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const param = new Map(); @@ -399,10 +445,11 @@ export class ArtemisJolokia { method: 'POST', headers: headers, body: this.getPostBodyForAttributes( - ArtemisJolokia.CLUSTER_CONNECTION, + CLUSTER_CONNECTION, param, clusterConnectionAttrNames, ), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -426,18 +473,19 @@ export class ArtemisJolokia { signature: string, args: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForOperation( - ArtemisJolokia.CLUSTER_CONNECTION, + CLUSTER_CONNECTION, param, signature, args, ), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -460,18 +508,19 @@ export class ArtemisJolokia { signature: string, args: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForOperation( - ArtemisJolokia.BROKER, + BROKER, new Map(), signature, args, ), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -493,11 +542,7 @@ export class ArtemisJolokia { getQueueDetails = async ( params?: Map, ): Promise => { - const headers = this.getAuthHeaders(); - - let searchPattern = this.componentDetailsMap.get( - ArtemisJolokia.QUEUE_DETAILS, - ); + let searchPattern = this.componentDetailsMap.get(QUEUE_DETAILS); if (typeof params !== 'undefined') { for (const [key, value] of params) { @@ -508,9 +553,12 @@ export class ArtemisJolokia { const url = this.baseUrl + 'list/' + searchPattern; + const { headers, agent } = this.prepareRequest(url); + const reply = await fetch(url, { method: 'GET', headers: headers, + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -538,7 +586,7 @@ export class ArtemisJolokia { addressName: string, queueAttrNames: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const param = new Map(); @@ -549,11 +597,8 @@ export class ArtemisJolokia { const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, - body: this.getPostBodyForAttributes( - ArtemisJolokia.QUEUE, - param, - queueAttrNames, - ), + body: this.getPostBodyForAttributes(QUEUE, param, queueAttrNames), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -575,7 +620,7 @@ export class ArtemisJolokia { acceptorName: string, acceptorAttrNames: string[], ): Promise => { - const headers = this.getAuthHeaders(); + const { headers, agent } = this.prepareRequest(this.baseUrl); headers.set('Content-Type', 'application/json'); const param = new Map(); @@ -584,11 +629,8 @@ export class ArtemisJolokia { const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, - body: this.getPostBodyForAttributes( - ArtemisJolokia.ACCEPTOR, - param, - acceptorAttrNames, - ), + body: this.getPostBodyForAttributes(ACCEPTOR, param, acceptorAttrNames), + agent: agent ?? false, }) .then((response) => { if (response.ok) { @@ -606,19 +648,6 @@ export class ArtemisJolokia { return reply; }; - validateUser = async (): Promise => { - const result = await this.getComponents(ArtemisJolokia.BROKER); - if (result.length === 1) { - //org.apache.activemq.artemis:broker="amq-broker" - this.brokerName = result[0].split('=', 2)[1]; - - //remove quotes - this.brokerName = this.brokerName.replace(/"/g, ''); - return true; - } - return false; - }; - getPostBodyForAttributes = ( component: string, params?: Map, @@ -675,6 +704,23 @@ export class ArtemisJolokia { }; } +const validateEndpoint = (endpoint: Endpoint) => { + if (!endpoint.name) { + throw Error('No endpoint name'); + } + if (!endpoint.auth) { + throw Error('No endpoint authentication data'); + } + if (!endpoint.url) { + throw Error('No endpoint url'); + } +}; + +export const CreateArtemisJolokia = (endpoint: Endpoint): ArtemisJolokia => { + validateEndpoint(endpoint); + return new ArtemisJolokia(endpoint); +}; + interface JolokiaPostReadBodyItem { type: string; mbean: string; diff --git a/src/api/controllers/api_impl.ts b/src/api/controllers/api_impl.ts index d2091fa..f125cc0 100644 --- a/src/api/controllers/api_impl.ts +++ b/src/api/controllers/api_impl.ts @@ -1,16 +1,20 @@ import * as express from 'express'; import { - ArtemisJolokia, + ACCEPTOR, + ADDRESS, + BROKER, + BROKER_COMPONENTS, + CLUSTER_CONNECTION, JolokiaExecResponse, JolokiaObjectDetailsType, JolokiaReadResponse, + QUEUE, } from '../apiutil/artemis_jolokia'; import { API_SUMMARY } from '../../utils/server'; +import { GetEndpointManager } from './endpoint_manager'; +import { IsSecurityEnabled } from './security_manager'; import { logger } from '../../utils/logger'; -const BROKER = 'broker'; -const ADDRESS = 'address'; -const QUEUE = 'queue'; const ROUTING_TYPE = 'routing-type'; const parseProps = (rawProps: string): Map => { @@ -23,11 +27,43 @@ const parseProps = (rawProps: string): Map => { return map; }; +export const listEndpoints = ( + _: express.Request, + res: express.Response, +): void => { + try { + GetEndpointManager() + .listEndpoints() + .then((result) => { + res.json( + result.map((entry) => { + return { + name: entry.name, + url: entry.serverUrl, + }; + }), + ); + }) + .catch((err: any) => { + res.status(500).json({ + status: 'error', + message: 'server error ' + JSON.stringify(err), + }); + }); + } catch (err) { + logger.error(err); + res.status(500).json({ + status: 'error', + message: 'server error: ' + JSON.stringify(err), + }); + } +}; + export const getBrokers = (_: express.Request, res: express.Response): void => { try { const jolokia = res.locals.jolokia; - const comps = jolokia.getComponents(ArtemisJolokia.BROKER); + const comps = jolokia.getComponents(BROKER); comps .then((result: any[]) => { @@ -40,11 +76,14 @@ export const getBrokers = (_: express.Request, res: express.Response): void => { }), ); }) - .catch((error: any) => { - logger.error(error); + .catch((err: any) => { + logger.debug(err, 'error getting BROKER comp'); + res.status(500).json({ + status: 'error', + message: 'server error ' + JSON.stringify(err), + }); }); } catch (err) { - logger.error(err); res.status(500).json({ status: 'error', message: 'server error: ' + JSON.stringify(err), @@ -59,7 +98,7 @@ export const getClusterConnections = ( try { const jolokia = res.locals.jolokia; - const comps = jolokia.getComponents(ArtemisJolokia.CLUSTER_CONNECTION); + const comps = jolokia.getComponents(CLUSTER_CONNECTION); comps .then((result: any[]) => { @@ -207,7 +246,7 @@ export const getAcceptors = ( try { const jolokia = res.locals.jolokia; - const comps = jolokia.getComponents(ArtemisJolokia.ACCEPTOR); + const comps = jolokia.getComponents(ACCEPTOR); comps .then((result: any[]) => { @@ -273,7 +312,7 @@ export const getBrokerComponents = ( try { const jolokia = res.locals.jolokia; - const comps = jolokia.getComponents(ArtemisJolokia.BROKER_COMPONENTS); + const comps = jolokia.getComponents(BROKER_COMPONENTS); comps .then((result: any[]) => { @@ -298,7 +337,7 @@ export const getAddresses = ( try { const jolokia = res.locals.jolokia; - const comps = jolokia.getComponents(ArtemisJolokia.ADDRESS); + const comps = jolokia.getComponents(ADDRESS); comps .then((result: any[]) => { res.json( @@ -368,7 +407,7 @@ export const getQueues = ( const param = new Map(); const name = addressName; param.set('ADDRESS_NAME', name); - const comps = jolokia.getComponents(ArtemisJolokia.QUEUE, param); + const comps = jolokia.getComponents(QUEUE, param); comps .then((result: any[]) => { @@ -668,6 +707,9 @@ export const getQueueDetails = ( export const apiInfo = (_: express.Request, res: express.Response): void => { res.json({ + security: { + enabled: IsSecurityEnabled(), + }, message: API_SUMMARY, status: 'successful', }); diff --git a/src/api/controllers/endpoint_manager.ts b/src/api/controllers/endpoint_manager.ts new file mode 100644 index 0000000..691b564 --- /dev/null +++ b/src/api/controllers/endpoint_manager.ts @@ -0,0 +1,67 @@ +import yaml from 'js-yaml'; +import { EndpointList } from '../../utils/security_util'; +import fs from 'fs'; +import { + ArtemisJolokia, + CreateArtemisJolokia, +} from '../apiutil/artemis_jolokia'; +import { logger } from '../../utils/logger'; + +export class EndpointManager { + // endpoint name => endpoint + endpointsMap: Map; + + start = async () => { + this.endpointsMap = EndpointManager.loadEndpoints( + process.env.USERS_FILE_URL + ? process.env.ENDPOINTS_FILE_URL + : '.endpoints.json', + ); + }; + + static loadEndpoints = (fileUrl: string): Map => { + const endpointsMap = new Map(); + if (fs.existsSync(fileUrl)) { + const fileContents = fs.readFileSync(fileUrl, 'utf8'); + const data = yaml.load(fileContents) as EndpointList; + data?.endpoints?.forEach((endpoint) => { + try { + const jolokia = CreateArtemisJolokia(endpoint); + endpointsMap.set(endpoint.name, jolokia); + } catch (err) { + logger.warn( + err, + 'failed to load endpoint (make sure your endpoint config is correct)', + ); + } + }); + } + return endpointsMap; + }; + + listEndpoints = async (): Promise => { + const endpoints = new Array(); + this.endpointsMap.forEach((value) => { + endpoints.push(value); + }); + return endpoints; + }; + + getJolokia = (targetEndpoint: string): ArtemisJolokia => { + const endpoint = this.endpointsMap.get(targetEndpoint); + if (endpoint) { + return endpoint; + } + throw Error('no endpoint found'); + }; +} + +const endpointManager = new EndpointManager(); + +export const InitEndpoints = async () => { + endpointManager.start(); +}; + +export const GetEndpointManager = (): EndpointManager => { + return endpointManager; +}; diff --git a/src/api/controllers/security.ts b/src/api/controllers/security.ts index 3942b97..d544a4f 100644 --- a/src/api/controllers/security.ts +++ b/src/api/controllers/security.ts @@ -1,23 +1,24 @@ import * as express from 'express'; import jwt from 'jsonwebtoken'; -import { ArtemisJolokia } from '../apiutil/artemis_jolokia'; +import { + ArtemisJolokia, + CreateArtemisJolokia, +} from '../apiutil/artemis_jolokia'; +import { GetSecurityManager, IsSecurityEnabled } from './security_manager'; +import { GetEndpointManager } from './endpoint_manager'; +import { + AuthenticationData, + AuthScheme, + Endpoint, + GenerateJWTToken, + GetSecretToken, + PermissionType, + User, +} from '../../utils/security_util'; import { logger } from '../../utils/logger'; const securityStore = new Map(); -const getSecretToken = (): string => { - return process.env.SECRET_ACCESS_TOKEN as string; -}; - -const generateJWTToken = (id: string): string => { - const payload = { - id: id, - }; - return jwt.sign(payload, getSecretToken(), { - expiresIn: 60 * 60 * 1000, - }); -}; - // to by pass CodeQL code scanning warning const validateHostName = (host: string) => { let validHost: string = host; @@ -87,20 +88,28 @@ export const login = (req: express.Request, res: express.Response) => { return; } - const jolokia = new ArtemisJolokia( - userName, - password, - validHost, - validScheme, - validPort, - ); + const authData: AuthenticationData = { + scheme: AuthScheme.Basic, + data: { + username: userName, + password: password, + }, + }; + + const endpoint: Endpoint = { + name: brokerName, + url: validScheme + '://' + validHost + ':' + validPort, + auth: [authData], + }; + + const jolokia = CreateArtemisJolokia(endpoint); try { jolokia - .validateUser() + .validateBroker() .then((result) => { if (result) { - const token = generateJWTToken(brokerName); + const token = GenerateJWTToken(brokerName); securityStore.set(brokerName, jolokia); res.json({ @@ -113,15 +122,91 @@ export const login = (req: express.Request, res: express.Response) => { status: 'failed', message: 'Invalid credential. Please try again.', }); + res.end(); } - res.end(); }) .catch((e) => { - logger.error('got exception while login', e); + logger.error(e, 'got exception while login'); res.status(500).json({ status: 'failed', message: 'Internal error', }); + res.end(); + }); + } catch (err) { + res.status(500).json({ + status: 'error', + message: 'Internal Server Error', + }); + res.end(); + } +}; + +export const serverLogin = (req: express.Request, res: express.Response) => { + try { + if (!IsSecurityEnabled()) { + res + .status(200) + .json({ + status: 'succeed', + message: 'security disabled', + }) + .end(); + return; + } + const securityManager = GetSecurityManager(); + + securityManager + .login(req.body) + .then((token) => { + res.json({ + status: 'success', + message: 'You have successfully logged in the api server.', + bearerToken: token, + }); + }) + .catch((err) => { + res.status(401).json({ + status: 'failed', + message: 'Invalid credential. Please try again.', + }); + res.end(); + }); + } catch (err) { + res.status(500).json({ + status: 'error', + message: 'Internal Server Error', + }); + res.end(); + } +}; + +export const serverLogout = (req: express.Request, res: express.Response) => { + try { + if (!IsSecurityEnabled()) { + res + .status(200) + .json({ + status: 'succeed', + message: 'security disabled', + }) + .end(); + return; + } + GetSecurityManager() + .logOut(req.user as User) + .then(() => { + res.status(200).json({ + status: 'success', + message: 'User logs out', + }); + }) + .catch((err) => { + res.status(500).json({ + status: 'failed', + message: `User failed log out with err ${err}`, + }); + res.end(); }); } catch (err) { res.status(500).json({ @@ -136,10 +221,127 @@ const ignoreAuth = (path: string): boolean => { return ( path === '/api/v1/jolokia/login' || path === '/api/v1/api-info' || + path === '/api/v1/server/login' || !path.startsWith('/api/v1/') ); }; +export const PreOperation = async ( + req: express.Request, + res: express.Response, + next: any, +) => { + const targetEndpoint = req.query.targetEndpoint; + if (targetEndpoint) { + try { + const jolokia = GetEndpointManager().getJolokia(targetEndpoint as string); + jolokia + .validateBroker() + .then((result) => { + if (result) { + res.locals.jolokia = jolokia; + next(); + } else { + res.status(500).json({ + status: 'failed', + message: 'failed to access jolokia endpoint', + }); + res.end(); + } + }) + .catch((err) => { + logger.debug(err, 'failed access jolokia endpoint'); + res.status(500).json({ + status: 'failed', + message: 'failed to access jolokia endpoint', + }); + res.end(); + }); + } catch (err) { + logger.debug(err, 'failed to access endpoint'); + res.status(500).json({ + status: 'failed', + message: 'no available endpoint', + }); + res.end(); + } + } else { + next(); + } +}; + +export const CheckPermissions = async ( + req: express.Request, + res: express.Response, + next: any, +) => { + try { + if (ignoreAuth(req.path)) { + next(); + } else { + if (res.locals.jolokia) { + GetSecurityManager() + .checkPermissions( + req.user as User, + PermissionType.Endpoints, + res.locals.jolokia.name, + ) + .then(() => { + next(); + }) + .catch((err) => { + logger.debug(err, 'permission denied'); + res.status(401).json({ + status: 'failed', + message: 'User has no permission to access the endpoint', + }); + res.end(); + }); + } else if (isAdminOp(req.path)) { + GetSecurityManager() + .checkPermissions(req.user as User, PermissionType.Admin) + .then(() => { + next(); + }) + .catch((err) => { + res.status(401).json({ + status: 'failed', + message: 'User has no permission to access the endpoint', + }); + res.end(); + }); + } else { + next(); + } + } + } catch (err) { + res.status(500).json({ + status: 'error', + message: 'Internal Server Error', + }); + } +}; + +export const VerifyAuth = async ( + req: express.Request, + res: express.Response, + next: any, +) => { + try { + if (ignoreAuth(req.path)) { + next(); + } else { + GetSecurityManager().validateRequest(req, res, next); + } + } catch (err) { + res.status(500).json({ + status: 'error', + message: 'Internal Server Error', + }); + res.end(); + } +}; + export const VerifyLogin = async ( req: express.Request, res: express.Response, @@ -149,35 +351,44 @@ export const VerifyLogin = async ( if (ignoreAuth(req.path)) { next(); } else { - const authHeader = req.headers['jolokia-session-id'] as string; + if (!(res.locals.jolokia || req.path.startsWith('/api/v1/server/'))) { + const authHeader = req.headers['jolokia-session-id'] as string; - if (!authHeader) { - res.sendStatus(401); - } else { - jwt.verify( - authHeader, - getSecretToken(), - async (err: any, decoded: any) => { - if (err) { - res.status(401).json({ - status: 'failed', - message: 'This session has expired. Please login again', - }); - } else { - const brokerKey = decoded['id']; - const jolokia = securityStore.get(brokerKey); - if (jolokia) { - res.locals.jolokia = jolokia; - next(); - } else { + if (!authHeader) { + res.status(401).json({ + status: 'failed', + message: 'unauthenticated', + }); + res.end(); + } else { + jwt.verify( + authHeader, + GetSecretToken(), + async (err: any, decoded: any) => { + if (err) { + logger.error('verify failed', err); res.status(401).json({ status: 'failed', message: 'This session has expired. Please login again', }); + } else { + const brokerKey = decoded['id']; + const jolokia = securityStore.get(brokerKey); + if (jolokia) { + res.locals.jolokia = jolokia; + next(); + } else { + res.status(401).json({ + status: 'failed', + message: 'This session has expired. Please login again', + }); + } } - } - }, - ); + }, + ); + } + } else { + next(); } } } catch (err) { @@ -187,3 +398,7 @@ export const VerifyLogin = async ( }); } }; + +const isAdminOp = (path: string): boolean => { + return path.startsWith('/api/v1/server/admin/'); +}; diff --git a/src/api/controllers/security_manager.ts b/src/api/controllers/security_manager.ts new file mode 100644 index 0000000..247d731 --- /dev/null +++ b/src/api/controllers/security_manager.ts @@ -0,0 +1,304 @@ +import fs from 'fs'; +import yaml from 'js-yaml'; +import * as bcrypt from 'bcrypt'; +import { + AuthType, + GenerateJWTToken, + GetSecretToken, + Permissions, + PermissionType, + Role, + RoleList, + User, + UserList, +} from '../../utils/security_util'; +import passport from 'passport'; +import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt'; +import { Request, Response } from 'express'; +import { ParamsDictionary } from 'express-serve-static-core'; +import { ParsedQs } from 'qs'; + +const getAuthType = (): AuthType => { + return AuthType.Jwt; +}; + +export interface SecurityManager { + getSecurityStore(): SecurityStore; + checkPermissions(user: User, type: PermissionType, data?: any): Promise; + login(credential: any): Promise; + logOut(user: User): Promise; + validateRequest( + req: Request>, + res: Response>, + next: any, + ): void; +} + +interface SecurityStore { + getAllUsers(): Map; + getAllRoles(): Map; + start(): Promise; + checkPermissionOnEndpoint(user: User, targetEndpoint: string): void; + checkPermissionOnAdmin(user: User): void; + findUser(userName: any): Promise; + authenticate(userName: string, password: string): User | null; +} + +class LocalSecurityStore implements SecurityStore { + // userName => User + usersMap: Map; + // roleName => Role + rolesMap: Map; + // Premissions + permissions: Permissions; + // user -> allowed endpoints + userAccessTable = new Map>(); + // user -> roles + userRolesTable = new Map>(); + + getAllUsers(): Map { + return this.usersMap; + } + + getAllRoles(): Map { + return this.rolesMap; + } + + start = async () => { + this.usersMap = LocalSecurityStore.loadUsers( + process.env.USERS_FILE_URL ? process.env.USERS_FILE_URL : '.users.json', + ); + this.rolesMap = LocalSecurityStore.loadRoles( + process.env.USERS_FILE_URL ? process.env.ROLES_FILE_URL : '.roles.json', + ); + this.permissions = LocalSecurityStore.loadPermissions( + process.env.USERS_FILE_URL + ? process.env.ACCESS_CONTROL_FILE_URL + : '.access.json', + ); + + this.buildUserRoleAccessTable(); + }; + + buildUserRoleAccessTable = () => { + // first build role -> allowed endpoints + const roleAccessTable = new Map>(); + + this.permissions.endpoints?.forEach((ep) => { + ep.roles.forEach((r) => { + if (roleAccessTable.has(r)) { + roleAccessTable.get(r).add(ep.name); + } else { + const endpointSet = new Set(); + endpointSet.add(ep.name); + roleAccessTable.set(r, endpointSet); + } + }); + }); + + this.rolesMap.forEach((role) => { + role.uids.forEach((uname) => { + let userEndpoints = this.userAccessTable.get(uname); + let userRoles = this.userRolesTable.get(uname); + if (!userEndpoints) { + userEndpoints = new Set(); + this.userAccessTable.set(uname, userEndpoints); + } + + roleAccessTable.get(role.name).forEach((endpoint) => { + userEndpoints.add(endpoint); + }); + if (!userRoles) { + userRoles = new Set(); + this.userRolesTable.set(uname, userRoles); + } + userRoles.add(role.name); + }); + }); + }; + + checkPermissionOnEndpoint(user: User, targetEndpoint: string): void { + if (!targetEndpoint) { + throw Error('no target endpoint specified'); + } + const endpoints = this.userAccessTable.get(user.id); + if (endpoints) { + if (!endpoints.has(targetEndpoint)) { + throw Error('no permission'); + } + } else { + throw Error('no permission'); + } + } + + checkPermissionOnAdmin(user: User): void { + const roles = this.userRolesTable.get(user.id); + const isAdmin = this.permissions.admin.roles.some((r) => { + if (roles.has(r)) { + return true; + } + }); + if (!isAdmin) { + throw Error('no permission'); + } + } + + static loadUsers = (fileUrl: string): Map => { + const usersMap = new Map(); + if (fs.existsSync(fileUrl)) { + const fileContents = fs.readFileSync(fileUrl, 'utf8'); + const data = yaml.load(fileContents) as UserList; + data?.users?.forEach((user) => { + usersMap.set(user.id, user); + }); + } + return usersMap; + }; + + static loadRoles = (fileUrl: string): Map => { + const rolesMap = new Map(); + if (fs.existsSync(fileUrl)) { + const fileContents = fs.readFileSync(fileUrl, 'utf8'); + const data = yaml.load(fileContents) as RoleList; + data?.roles?.forEach((role) => { + rolesMap.set(role.name, role); + }); + } + return rolesMap; + }; + + static loadPermissions = (fileUrl: string): Permissions => { + if (fs.existsSync(fileUrl)) { + const fileContents = fs.readFileSync(fileUrl, 'utf8'); + const permissions = yaml.load(fileContents) as Permissions; + if (permissions) { + return permissions; + } + } + return { endpoints: [], admin: { roles: [] } }; + }; + + findUser = async (userName: string): Promise => { + if (this.usersMap.has(userName)) { + return this.usersMap.get(userName); + } + throw Error(`No such user ${userName}`); + }; + + authenticate = (userName: string, password: string): User | null => { + let authUser = null; + if (this.usersMap.has(userName)) { + const user = this.usersMap.get(userName); + if (bcrypt.compareSync(password, user.hash)) { + authUser = user; + } + } + return authUser; + }; +} + +class JwtSecurityManager implements SecurityManager { + readonly securityStore: SecurityStore = new LocalSecurityStore(); + readonly authenticatedUsers: Set = new Set(); + + getSecurityStore(): SecurityStore { + return this.securityStore; + } + + logOut = async (user: User) => { + this.authenticatedUsers.delete(user.id); + }; + + addActiveUser = (activeUser: User) => { + this.authenticatedUsers.add(activeUser.id); + }; + + login = async (credential: any): Promise => { + const { userName, password } = credential; + const user = this.securityStore.authenticate(userName, password); + if (user) { + const token = GenerateJWTToken(userName); + this.addActiveUser(user); + return token; + } + throw Error('wrong credentials'); + }; + + validateRequest = ( + req: Request>, + res: Response>, + next: any, + ): void => { + passport.authenticate(AuthType.Jwt, { session: false })(req, res, next); + }; + + checkPermissions = async ( + user: User, + type: PermissionType, + data?: any, + ): Promise => { + switch (type) { + case PermissionType.Endpoints: { + this.securityStore.checkPermissionOnEndpoint(user, data); + break; + } + case PermissionType.Admin: { + this.securityStore.checkPermissionOnAdmin(user); + break; + } + default: + throw Error('invalid type ' + type); + } + }; + + start = async () => { + this.securityStore.start().then(() => { + const opts = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: GetSecretToken(), + ignoreExpiration: false, + }; + + passport.use( + new JwtStrategy(opts, (jwt_payload, done) => { + const userName = jwt_payload.id; + if (userName) { + //find the user + const user = this.securityStore.findUser(userName).then((user) => { + if (user) { + return done(null, user); + } else { + return done(null, false); + } + }); + } else { + return done(null, false); + } + }), + ); + }); + }; +} + +export const jwtSecurityManager = new JwtSecurityManager(); + +export const InitSecurity = async () => { + if (IsSecurityEnabled()) { + const authType = getAuthType(); + if (authType === AuthType.Jwt) { + await jwtSecurityManager.start(); + } + } +}; + +export const GetSecurityManager = (): SecurityManager => { + const authType = getAuthType(); + if (authType === AuthType.Jwt) { + return jwtSecurityManager; + } + throw Error('Auth type not supported ' + authType); +}; + +export const IsSecurityEnabled = (): boolean => { + return process.env.API_SERVER_SECURITY_ENABLED !== 'false'; +}; diff --git a/src/app.ts b/src/app.ts index ed2dd50..4294f68 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,10 +3,12 @@ import https from 'https'; import fs from 'fs'; import path from 'path'; import dotenv from 'dotenv'; -import { logger } from './utils/logger'; +import { InitLoggers, logger } from './utils/logger'; dotenv.config(); +InitLoggers(); + logger.info( `Starting plugin ${process.env.PLUGIN_NAME} ${process.env.PLUGIN_VERSION}`, ); @@ -45,8 +47,12 @@ createServer(isReqLogEnabled === 'true') const secureServer = https.createServer(options, server); secureServer.listen(9443, () => { logger.info('Listening on https://0.0.0.0:9443'); + const securityEnabled = + process.env.API_SERVER_SECURITY_ENABLED !== 'false'; + logger.info('security is ' + (securityEnabled ? 'enabled' : 'disabled.')); }); }) .catch((err) => { logger.error(`Error: ${err}`); + process.exit(1); }); diff --git a/src/config/openapi.yml b/src/config/openapi.yml index 9e4807d..b6da434 100644 --- a/src/config/openapi.yml +++ b/src/config/openapi.yml @@ -93,8 +93,92 @@ tags: description: jolokia API - name: development description: for development purposes + - name: admin + description: for management operations + +security: + - bearerAuth: [] # use the same name as above paths: + /server/login: + post: + summary: Api to log in to the api server. + description: > + This api is used to login to the api server. + tags: + - security + operationId: serverLogin + security: [] # no security for login + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + userName: + type: string + password: + type: string + required: + - userName + - password + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ServerLoginResponse' + 401: + description: Invalid credentials + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + 500: + description: Internal server error + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + /server/logout: + post: + summary: Api to log out + description: > + This api is used to logout the current session. + tags: + - security + operationId: serverLogout + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyBody' + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ServerLogoutResponse' + 401: + description: Invalid credentials + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + 500: + description: Internal server error + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' /jolokia/login: post: summary: The login api @@ -112,6 +196,7 @@ paths: named `jolokia-session-id`. The src will validate the token before processing a request is and rejects the request if the token is not valid. + security: [] # no security for login tags: - security operationId: login @@ -168,6 +253,39 @@ paths: schema: type: object $ref: '#/components/schemas/FailureResponse' + /server/admin/listEndpoints: + get: + summary: List endpoints managed by the api-server + description: > + ** List broker jolokia endpoints ** + The return value is a list of endpoints currently + managed by the api server. + tags: + - admin + operationId: listEndpoints + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Endpoint' + 401: + description: Invalid credentials + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + 500: + description: Internal server error + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' /brokers: get: summary: retrieve the broker mbean @@ -183,7 +301,17 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: header + name: endpoint-name + schema: + type: string + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -223,7 +351,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -263,7 +396,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: names description: attribute names separated by commas. If not speified read all attributes. required: false @@ -274,6 +407,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -315,7 +453,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the address name schema: @@ -332,6 +470,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -373,7 +516,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the queue name schema: @@ -402,6 +545,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -443,7 +591,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the queue name schema: @@ -460,6 +608,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -501,7 +654,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the cluster connection name schema: @@ -518,6 +671,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -559,12 +717,17 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - in: query name: name schema: type: string required: true + - in: query + name: targetEndpoint + schema: + type: string + required: false requestBody: required: true content: @@ -594,7 +757,6 @@ paths: schema: type: object $ref: '#/components/schemas/FailureResponse' - /checkCredentials: get: summary: Check the validity of the credentials @@ -606,7 +768,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false responses: 200: description: Success @@ -647,7 +809,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false requestBody: required: true content: @@ -693,7 +860,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -732,7 +904,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -771,13 +948,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: address required: false in: query schema: type: string description: If given only list the queues on this address + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -818,7 +1000,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: addressName required: false in: query @@ -837,6 +1019,11 @@ paths: schema: type: string description: the routing type of the queue (anycast or multicast) + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -875,13 +1062,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name required: true in: query schema: type: string description: the address name + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -918,7 +1110,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -959,13 +1156,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name required: true in: query schema: type: string description: the acceptor name + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -1002,7 +1204,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -1043,13 +1250,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name required: true in: query schema: type: string description: the cluster connection name + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -1082,6 +1294,7 @@ paths: tags: - development operationId: apiInfo + security: [] #no security for api-info responses: 200: description: Success @@ -1171,6 +1384,11 @@ components: message: type: object properties: + security: + type: object + properties: + enabled: + type: boolean info: type: object properties: @@ -1197,6 +1415,30 @@ components: jolokia-session-id: type: string description: The jwt token + ServerLogoutResponse: + type: object + required: + - status + - message + properties: + message: + type: string + status: + type: string + ServerLoginResponse: + type: object + required: + - status + - message + - bearerToken + properties: + message: + type: string + status: + type: string + bearerToken: + type: string + description: The jwt token LoginResponse: type: object required: @@ -1263,6 +1505,15 @@ components: properties: name: type: string + Endpoint: + type: object + required: + - name + properties: + name: + type: string + url: + type: string FailureResponse: type: object required: @@ -1394,7 +1645,6 @@ components: type: object required: - mbean - - arguments - type - operation properties: @@ -1417,3 +1667,11 @@ components: type: number status: type: number + EmptyBody: + type: object + nullable: true + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 50f852b..bd9359d 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -7,7 +7,7 @@ export const logger = pino({ level: (label) => { return { level: label.toUpperCase() }; }, - bindings: (bindings) => { + bindings: () => { return {}; }, }, @@ -19,3 +19,7 @@ export const logRequest = (enabled: boolean) => level: 'info', enabled, }); + +export const InitLoggers = () => { + logger.level = process.env.LOG_LEVEL || 'info'; +}; diff --git a/src/utils/security_store.ts b/src/utils/security_store.ts new file mode 100644 index 0000000..b00df07 --- /dev/null +++ b/src/utils/security_store.ts @@ -0,0 +1,97 @@ +import { Store } from 'fs-json-store'; +import { User } from './security_util'; +import { error } from 'console'; +import bcrypt from 'bcrypt'; + +export interface StoreApi { + getUserById(id: string): Promise; + getUser(userName: string): Promise; +} + +export class JsonFileStore implements StoreApi { + store: Store; + + constructor(jsonFile: string) { + this.store = new Store({ file: jsonFile }); + } + + async getUserById(id: string): Promise { + const users = (await this.store.read()) as User[]; + users.forEach((u) => { + if (u.id === id) { + return u; + } + }); + throw error('User undefined'); + } + + async getUser(userName: string): Promise { + const users = (await this.store.read()) as User[]; + users.forEach((u) => { + if (u.email === userName) { + return u; + } + }); + throw error('User undefined'); + } +} + +export class AbstractStore { + store: StoreApi; + constructor(store: StoreApi) { + this.store = store; + } +} + +export class UserStore extends AbstractStore { + async getUserById(id: string) { + return await this.store.getUserById(id); + } + + async authenticate(userName: string, password: string): Promise { + const user = await this.store.getUser(userName); + if (!bcrypt.compareSync(password, user.hash)) { + throw error('Invalid credential'); + } + return user; + } +} + +export class RoleStore extends AbstractStore {} + +export class EndpointStore extends AbstractStore {} + +export class AccessControlStore extends AbstractStore {} + +export const GetUserStore = (): UserStore => { + const userFile = process.env.USERS_FILE_URL + ? process.env.USERS_FILE_URL + : '.users.json'; + return new UserStore(new JsonFileStore(userFile)); +}; + +export const GetRoleStore = (): RoleStore => { + const roleFile = process.env.ROLES_FILE_URL + ? process.env.ROLES_FILE_URL + : '.roles.json'; + return new RoleStore(new JsonFileStore(roleFile)); +}; + +export const GetEndpointStore = (): EndpointStore => { + const endpointFile = process.env.ENDPOINTS_FILE_URL + ? process.env.ENDPOINTS_FILE_URL + : '.endpoints.json'; + return new EndpointStore(new JsonFileStore(endpointFile)); +}; + +export const GetAccessControlStore = (): AccessControlStore => { + const aclFile = process.env.ACCESS_CONTROL_FILE_URL + ? process.env.ACCESS_CONTROL_FILE_URL + : '.access.json'; + return new AccessControlStore(new JsonFileStore(aclFile)); +}; + +export const userStore = GetUserStore(); +export const roleStore = GetRoleStore(); +export const endpointStore = GetEndpointStore(); +export const accessControlStore = GetAccessControlStore(); diff --git a/src/utils/security_util.ts b/src/utils/security_util.ts new file mode 100644 index 0000000..553ff23 --- /dev/null +++ b/src/utils/security_util.ts @@ -0,0 +1,185 @@ +import base64 from 'base-64'; +import jwt from 'jsonwebtoken'; +import { Headers } from 'node-fetch'; +import https from 'https'; +import fs from 'fs'; +import http from 'http'; +import { logger } from './logger'; + +export const GetSecretToken = (): string => { + return process.env.SECRET_ACCESS_TOKEN as string; +}; + +export const GenerateJWTToken = (id: string): string => { + const payload = { + id: id, + }; + return jwt.sign(payload, GetSecretToken(), { + expiresIn: 60 * 60 * 1000, + }); +}; + +export enum AuthType { + Jwt = 'jwt', +} + +export interface User { + id: string; + email?: string; + hash: string; +} + +export interface UserList { + users: User[]; +} + +export interface Role { + name: string; + uids: string[]; +} + +export interface RoleList { + roles: Role[]; +} + +export enum PermissionType { + Endpoints = 'endpoints', + Admin = 'admin', +} + +export enum AuthScheme { + //user name and password + Basic = 'basic', + //client cert in mtls + Cert = 'cert', +} +export interface EndpointsPermission { + name: string; + roles: string[]; +} + +export interface AdminPermission { + roles: string[]; +} +export interface Permissions { + endpoints: EndpointsPermission[]; + admin: AdminPermission; +} + +export interface AuthenticationData { + readonly scheme: AuthScheme; + readonly data: any; +} + +export interface BasicAuthData { + readonly username: string; + readonly password: string; +} + +export interface CertAuthData { + readonly certpath: string; + readonly keypath: string; +} + +export interface Endpoint { + readonly name: string; + readonly url: string; + readonly jolokiaPrefix?: string; + readonly auth: AuthenticationData[]; +} + +export interface EndpointList { + endpoints: Endpoint[]; +} + +export interface AuthOptions { + agent?: http.Agent; + headers: Headers; +} + +export abstract class AuthHandler { + abstract handleRequest(reqUrl: string, authOpts: AuthOptions): void; + isHttps = (url: string): boolean => { + return url.startsWith('https://'); + }; +} + +class BasicAuthHandler extends AuthHandler { + readonly basicAuth: BasicAuthData; + + constructor(cred: BasicAuthData) { + super(); + this.basicAuth = cred; + } + + handleRequest = (reqUrl: string, authOpts: AuthOptions): void => { + if (this.isHttps(reqUrl)) { + authOpts.agent = new https.Agent({ + // Disables certificate validation, can we use this instead of setting NODE_TLS_REJECT_UNAUTHORIZED='0'? + rejectUnauthorized: false, + }); + } + authOpts.headers.set( + 'Authorization', + 'Basic ' + + base64.encode(this.basicAuth.username + ':' + this.basicAuth.password), + ); + }; +} + +class CertAuthHandler extends AuthHandler { + readonly certAuth: CertAuthData; + + constructor(cred: CertAuthData) { + super(); + this.validateFiles(cred); + this.certAuth = cred; + } + + validateFiles = (cred: CertAuthData) => { + if (!fs.existsSync(cred.certpath)) { + throw Error('cert file not exist'); + } + if (!fs.existsSync(cred.keypath)) { + throw Error('key file not exist'); + } + }; + + getCert = () => { + return fs.readFileSync(this.certAuth.certpath); + }; + + getKey = () => { + return fs.readFileSync(this.certAuth.keypath); + }; + + handleRequest = (reqUrl: string, authOpts: AuthOptions): void => { + logger.warn( + 'The certificate authentication is experimental and may not work properly', + ); + if (!this.isHttps(reqUrl)) { + throw Error('auth only works with https'); + } + authOpts.agent = new https.Agent({ + // Disables certificate validation, can we use this instead of setting NODE_TLS_REJECT_UNAUTHORIZED='0'? + rejectUnauthorized: false, + // ca: trusted ca bundle + cert: this.getCert(), + key: this.getKey(), + }); + }; +} + +export const CreateAuthHandler = (data: AuthenticationData): AuthHandler => { + switch (data.scheme) { + case AuthScheme.Basic: { + return new BasicAuthHandler(data.data); + } + case AuthScheme.Cert: { + return new CertAuthHandler(data.data); + } + default: { + throw Error('auth scheme not supported: ' + data.scheme); + } + } +}; diff --git a/src/utils/server.security.test.ts b/src/utils/server.security.test.ts new file mode 100644 index 0000000..dfe9f2a --- /dev/null +++ b/src/utils/server.security.test.ts @@ -0,0 +1,522 @@ +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import createServer from './server'; +import nock from 'nock'; +import fetch from 'node-fetch'; +import dotenv from 'dotenv'; +import { InitLoggers, logger } from './logger'; +import { + GetSecurityManager, + IsSecurityEnabled, +} from '../api/controllers/security_manager'; +import { GetEndpointManager } from '../api/controllers/endpoint_manager'; + +dotenv.config({ path: '.test.env' }); + +let testServer: https.Server; +let mockJolokia: nock.Scope; + +let mockBroker1: nock.Scope; + +const apiUrlBase = 'https://localhost:9444/api/v1'; +const apiUrlPrefix = '/console/jolokia'; +const loginUrl = apiUrlBase + '/jolokia/login'; +const serverLoginUrl = apiUrlBase + '/server/login'; +const jolokiaProtocol = 'https'; +const jolokiaHost = 'broker-0-jolokia.test.com'; +const jolokiaPort = '8161'; +const jolokiaSessionKey = 'jolokia-session-id'; + +// see .test.endpoints.json +const broker1EndpointUrl = 'http://127.0.0.1:8161'; + +const startApiServer = async (): Promise => { + process.env.API_SERVER_SECURITY_ENABLED = 'true'; + + const enableRequestLog = process.env.ENABLE_REQUEST_LOG === 'true'; + InitLoggers(); + + const result = await createServer(enableRequestLog) + .then((server) => { + const options = { + key: fs.readFileSync(path.join(__dirname, '../config/domain.key')), + cert: fs.readFileSync(path.join(__dirname, '../config/domain.crt')), + }; + testServer = https.createServer(options, server); + testServer.listen(9444, () => { + logger.info('Listening on https://0.0.0.0:9444'); + logger.info( + 'Security is ' + (IsSecurityEnabled() ? 'enabled' : 'disabled'), + ); + }); + return true; + }) + .catch((err) => { + console.log('error starting server', err); + return false; + }); + return result; +}; + +const stopApiServer = () => { + testServer.close(); +}; + +const startMockJolokia = () => { + mockJolokia = nock(jolokiaProtocol + '://' + jolokiaHost + ':' + jolokiaPort); + mockBroker1 = nock(broker1EndpointUrl); +}; + +const stopMockJolokia = () => { + nock.cleanAll(); +}; + +beforeAll(async () => { + const result = await startApiServer(); + expect(result).toBe(true); + expect(testServer).toBeDefined(); + startMockJolokia(); +}); + +afterAll(() => { + stopApiServer(); + stopMockJolokia(); +}); + +const doGet = async ( + url: string, + token: string | null, + authToken: string, +): Promise => { + const fullUrl = apiUrlBase + url; + const encodedUrl = fullUrl.replace(/,/g, '%2C'); + + if (token) { + const response = await fetch(encodedUrl, { + method: 'GET', + headers: { + [jolokiaSessionKey]: token, + Authorization: 'Bearer ' + authToken, + }, + }); + return response; + } + + const response = await fetch(encodedUrl, { + method: 'GET', + headers: { + Authorization: 'Bearer ' + authToken, + }, + }); + return response; +}; + +const doPost = async ( + url: string, + postBody: fetch.BodyInit, + token: string | null, + authToken: string, +): Promise => { + const fullUrl = apiUrlBase + url; + const encodedUrl = fullUrl.replace(/,/g, '%2C'); + + if (token) { + const reply = await fetch(encodedUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + [jolokiaSessionKey]: token, + Authorization: 'Bearer ' + authToken, + }, + body: postBody, + }); + + return reply; + } + const reply = await fetch(encodedUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + authToken, + }, + body: postBody, + }); + + return reply; +}; + +type LoginOptions = { + [key: string]: string; +}; + +type LoginResult = { + resp: fetch.Response; + accessToken: string | null; + authToken: string; +}; + +const doServerLogin = async ( + user: string, + pass: string, +): Promise => { + const details: LoginOptions = { + userName: user, + password: pass, + }; + + const formBody: string[] = []; + for (const property in details) { + const encodedKey = encodeURIComponent(property); + const encodedValue = encodeURIComponent(details[property]); + formBody.push(encodedKey + '=' + encodedValue); + } + const formData = formBody.join('&'); + + const response = await fetch(serverLoginUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData, + }); + + const obj = await response.json(); + + const bearerToken = obj.bearerToken; + + return { + resp: response, + accessToken: null, + authToken: bearerToken as string, + }; +}; + +const doJolokiaLoginWithAuth = async ( + user: string, + pass: string, +): Promise => { + return doServerLogin(user, pass).then(async (result) => { + if (!result.resp.ok) { + throw Error('failed server login'); + } + + const jolokiaResp = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker"'], + timestamp: 1714703745, + status: 200, + }; + mockJolokia + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaResp)); + + const details: LoginOptions = { + brokerName: 'ex-aao-0', + userName: 'admin', + password: 'admin', + jolokiaHost: jolokiaHost, + port: jolokiaPort, + scheme: jolokiaProtocol, + }; + + const formBody: string[] = []; + for (const property in details) { + const encodedKey = encodeURIComponent(property); + const encodedValue = encodeURIComponent(details[property]); + formBody.push(encodedKey + '=' + encodedValue); + } + const formData = formBody.join('&'); + + const res1 = await fetch(loginUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Bearer ' + result.authToken, + }, + body: formData, + }); + + const data = await res1.json(); + + return { + resp: res1, + accessToken: data[jolokiaSessionKey] as string, + authToken: result.authToken, + }; + }); +}; + +describe('test api server login with jolokia login', () => { + it('test login functionality', async () => { + const result = await doJolokiaLoginWithAuth('user1', 'password'); + + expect(result.resp.ok).toBeTruthy(); + + expect(result?.accessToken?.length).toBeGreaterThan(0); + expect(result.authToken.length).toBeGreaterThan(0); + }); + + it('test jolokia login failure', async () => { + const jolokiaResp = { + request: {}, + value: [''], + error: 'forbidden access', + timestamp: 1714703745, + status: 403, + }; + mockJolokia + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(403, JSON.stringify(jolokiaResp)); + + const result = await doJolokiaLoginWithAuth('user1', 'password'); + + expect(result.resp.ok).toBeFalsy(); + }); + + it('test server login failure wrong user or password', async () => { + const result = await doServerLogin('nouser', 'password'); + expect(result.resp.ok).toBeFalsy(); + + const result1 = await doServerLogin('nouser', 'nopassword'); + expect(result1.resp.ok).toBeFalsy(); + + const result2 = await doServerLogin('user1', 'password2'); + expect(result2.resp.ok).toBeFalsy(); + }); +}); + +describe('test direct proxy access', () => { + let accessToken: string; + let jwtToken: string; + + beforeAll(async () => { + const result = await doJolokiaLoginWithAuth('user1', 'password'); + jwtToken = result.authToken; + expect(result?.accessToken?.length).toBeGreaterThan(0); + accessToken = result.accessToken as string; + expect(jwtToken.length).toBeGreaterThan(0); + }); + + it('test get brokers', async () => { + const result = [ + { + name: 'amq-broker', + }, + ]; + const jolokiaResp = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker"'], + timestamp: 1714703745, + status: 200, + }; + mockJolokia + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doGet('/brokers', accessToken, jwtToken); + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(value.length).toEqual(1); + expect(value[0]).toEqual(result[0]); + }); +}); + +describe('test endpoints loading', () => { + let jwtToken: string; + + beforeAll(async () => { + const result = await doServerLogin('root', 'password'); + jwtToken = result.authToken; + expect(result.accessToken).toBeNull(); + expect(jwtToken.length).toBeGreaterThan(0); + }); + + it('check endpoints are loaded', () => { + const endpointManager = GetEndpointManager(); + expect(endpointManager.endpointsMap.size).toEqual(4); + + const jolokia1 = endpointManager.endpointsMap.get('broker1'); + expect(jolokia1).not.toBeUndefined(); + expect(jolokia1?.baseUrl).toEqual('http://127.0.0.1:8161/console/jolokia/'); + + const jolokia2 = endpointManager.endpointsMap.get('broker2'); + expect(jolokia2).not.toBeUndefined(); + expect(jolokia2?.baseUrl).toEqual('http://127.0.0.2:8161/console/jolokia/'); + + const jolokia3 = endpointManager.endpointsMap.get('broker3'); + expect(jolokia3).not.toBeUndefined(); + expect(jolokia3?.baseUrl).toEqual('http://127.0.0.3:8161/console/jolokia/'); + + const jolokia4 = endpointManager.endpointsMap.get('broker4'); + expect(jolokia4).not.toBeUndefined(); + expect(jolokia4?.baseUrl).toEqual( + 'https://artemis-broker-jolokia-0-svc-ing-default.artemiscloud.io:443/jolokia/', + ); + }); +}); + +describe('check security manager', () => { + const securityManager = GetSecurityManager(); + const securityStore = securityManager.getSecurityStore(); + + it('check user role mapping', () => { + const users = securityStore.getAllUsers(); + expect(users.size).toEqual(3); + expect(users.has('user1')).toBeTruthy(); + expect(users.has('user2')).toBeTruthy(); + expect(users.has('root')).toBeTruthy(); + + const roles = securityStore.getAllRoles(); + expect(roles.size).toEqual(3); + + const role1 = roles.get('role1'); + expect(role1?.uids.length).toEqual(1); + expect(role1?.uids[0]).toEqual('user1'); + + const role2 = roles.get('role2'); + expect(role2?.uids.length).toEqual(2); + expect(role2?.uids[0]).toEqual('user1'); + expect(role2?.uids[1]).toEqual('user2'); + + const role3 = roles.get('manager'); + expect(role3?.uids.length).toEqual(1); + expect(role3?.uids[0]).toEqual('root'); + }); +}); + +describe('test endpoint access with successful auth', () => { + let jwtToken: string; + + beforeAll(async () => { + const result = await doServerLogin('user1', 'password'); + jwtToken = result.authToken; + expect(result.accessToken).toBeNull(); + expect(jwtToken.length).toBeGreaterThan(0); + }); + + it('test get brokers', async () => { + const result = [ + { + name: '127.0.0.1', + }, + ]; + const jolokiaResp = { + request: {}, + value: ['org.apache.activemq.artemis:broker="127.0.0.1"'], + timestamp: 1714703745, + status: 200, + }; + + //use persist when this path will get called more than once. + mockBroker1 + .persist() + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doGet('/brokers?targetEndpoint=broker1', null, jwtToken); + + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(value.length).toEqual(1); + expect(value[0]).toEqual(result[0]); + }); + + it('test execBrokerOperation', async () => { + const jolokiaGetResp = { + request: {}, + value: ['org.apache.activemq.artemis:broker="127.0.0.1"'], + timestamp: 1714703745, + status: 200, + }; + + //use persist when this path will get called more than once. + mockBroker1 + .persist() + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaGetResp)); + + const jolokiaResp = [ + { + request: { + mbean: 'org.apache.activemq.artemis:broker="127.0.0.1"', + arguments: [','], + type: 'exec', + operation: 'listAddresses(java.lang.String)', + }, + value: + '$.artemis.internal.sf.my-cluster.5c0e3e93-1837-11ef-aa70-0a580ad9005f,activemq.notifications,DLQ,ExpiryQueue', + timestamp: 1716385483, + status: 200, + }, + ]; + + mockBroker1 + .post(apiUrlPrefix + '/', (body) => { + if ( + body.length === 1 && + body[0].type === 'exec' && + body[0].mbean === 'org.apache.activemq.artemis:broker="127.0.0.1"' && + body[0].operation === 'listAddresses(java.lang.String)' && + body[0].arguments[0] === ',' + ) { + return true; + } + return false; + }) + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doPost( + '/execBrokerOperation?targetEndpoint=broker1', + JSON.stringify({ + signature: { + name: 'listAddresses', + args: [{ type: 'java.lang.String', value: ',' }], + }, + }), + null, + jwtToken, + ); + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(JSON.stringify(value)).toEqual(JSON.stringify(jolokiaResp)); + }); +}); + +describe('test endpoint access with permission denied', () => { + let jwtToken: string; + + beforeAll(async () => { + const result = await doServerLogin('user2', 'password'); + jwtToken = result.authToken; + expect(result.accessToken).toBeNull(); + expect(jwtToken.length).toBeGreaterThan(0); + }); + + it('test get brokers get denied on broker1', async () => { + const jolokiaResp = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker"'], + timestamp: 1714703745, + status: 200, + }; + + //use persist when this path will get called more than once. + mockBroker1 + .persist() //use persist when this path will get called more than once. + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doGet( + '/brokers' + '?targetEndpoint=broker1', + null, + jwtToken, + ); + + expect(resp.ok).not.toBeTruthy(); + expect(resp.status).toEqual(401); + }); +}); diff --git a/src/utils/server.test.ts b/src/utils/server.test.ts index e78dc7b..c7d3f18 100644 --- a/src/utils/server.test.ts +++ b/src/utils/server.test.ts @@ -5,8 +5,10 @@ import createServer from './server'; import nock from 'nock'; import fetch from 'node-fetch'; import dotenv from 'dotenv'; +import { logger } from './logger'; +import { IsSecurityEnabled } from '../api/controllers/security_manager'; -dotenv.config(); +dotenv.config({ path: '.test.env' }); let testServer: https.Server; let mockJolokia: nock.Scope; @@ -29,12 +31,15 @@ const startApiServer = async (): Promise => { }; testServer = https.createServer(options, server); testServer.listen(9443, () => { - console.info('Listening on https://0.0.0.0:9443'); + logger.info('Listening on https://0.0.0.0:9443'); + logger.info( + 'Security is ' + (IsSecurityEnabled() ? 'enabled' : 'disabled'), + ); }); return true; }) .catch((err) => { - console.log('error starting server', err); + logger.info('error starting server', err); return false; }); return result; @@ -65,6 +70,9 @@ afterAll(() => { }); const doGet = async (url: string, token: string): Promise => { + if (!token) { + throw Error('token undefined ' + token); + } const fullUrl = apiUrlBase + url; const encodedUrl = fullUrl.replace(/,/g, '%2C'); const response = await fetch(encodedUrl, { @@ -146,7 +154,7 @@ describe('test api server login', () => { expect(response.ok).toBeTruthy(); const data = await response.json(); - expect(data['jolokia-session-id']).toBeDefined(); + expect(data['jolokia-session-id'].length).toBeGreaterThan(0); }); it('test login failure', async () => { @@ -174,6 +182,7 @@ describe('test api server apis', () => { const response = await doLogin(); const data = await response.json(); authToken = data['jolokia-session-id']; + expect(authToken.length).toBeGreaterThan(0); }); it('test get brokers', async () => { diff --git a/src/utils/server.ts b/src/utils/server.ts index 14cd8a5..cbbb674 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -3,15 +3,19 @@ import * as OpenApiValidator from 'express-openapi-validator'; import { Express } from 'express-serve-static-core'; import { Summary, connector, summarise } from 'swagger-routes-express'; import { rateLimit } from 'express-rate-limit'; -//import YAML from 'yamljs'; import * as YAML from 'js-yaml'; import * as fs from 'fs'; import path from 'path'; import cors from 'cors'; - import * as api from '../api/controllers'; import { logger, logRequest } from './logger'; +import { + InitSecurity, + IsSecurityEnabled, +} from '../api/controllers/security_manager'; +import { InitEndpoints } from '../api/controllers/endpoint_manager'; + export let API_SUMMARY: Summary; const createServer = async (enableLogRequest: boolean): Promise => { @@ -23,6 +27,10 @@ const createServer = async (enableLogRequest: boolean): Promise => { logger.debug(API_SUMMARY); + await InitSecurity(); + + await InitEndpoints(); + const server = express(); // here we can intialize body/cookies parsers, connect logger, for example morgan @@ -42,7 +50,8 @@ const createServer = async (enableLogRequest: boolean): Promise => { apiSpec: yamlSpecFile, validateRequests: true, validateResponses: true, - ignorePaths: /jolokia\/login/, + validateSecurity: IsSecurityEnabled(), + ignorePaths: /jolokia|server\/login/, }; server.use(express.json()); @@ -50,6 +59,7 @@ const createServer = async (enableLogRequest: boolean): Promise => { server.use(express.urlencoded({ extended: false })); server.use(cors()); server.use(OpenApiValidator.middleware(validatorOptions)); + server.use((req, res, next) => { if (process.env.NODE_ENV === 'production') { logger.debug( @@ -72,6 +82,14 @@ const createServer = async (enableLogRequest: boolean): Promise => { next(); } }); + + server.use(api.PreOperation); + + if (IsSecurityEnabled()) { + server.use(api.VerifyAuth); + server.use(api.CheckPermissions); + } + server.use(api.VerifyLogin); const connect = connector(api, apiDefinition, { diff --git a/test-api-server.crt b/test-api-server.crt new file mode 100644 index 0000000..7e52a07 --- /dev/null +++ b/test-api-server.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIRAMosGPDScmrk0qWljXQFvsQwDQYJKoZIhvcNAQELBQAw +ODEcMBoGA1UEChMTd3d3LmFydGVtaXNjbG91ZC5pbzEYMBYGA1UEAxMPYXJ0ZW1p +cy5yb290LmNhMB4XDTI0MTAyOTA3NDYxN1oXDTI1MDEyNzA3NDYxN1owQjEcMBoG +A1UEChMTd3d3LmFydGVtaXNjbG91ZC5pbzEiMCAGA1UEAxMZYWN0aXZlbXEtYXJ0 +ZW1pcy1vcGVyYXRvcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANFn +zi5B8rHKP/KnhkYtI+k72zqLrJtmjDJQaaMOS3GNO5m2Iqx9jKsdWXCCyH+RgcU6 +5BhXZaPrAMCwMV89KJhk+ZCO1feyEac0i/FtOogkCKDS91vXUcwTEyodZbamFjcm +qcPGCGx8/r8slSsurgn3dqgJW15wOvS9qOSUcRybWbTlfjXegEw4R72odqIYoifg +UaxSYnP6NakuCZbz6VnkEhA8PGJnDCyKg7dVXZeLB16jY0rEOAiBhPrB1b60Ew7R +7fK9f2SqCeSJN8dL2rm2FUv5SWIQL1iJ11FJvRuXslgjO71on3KiUDEQBpgRnEzw +ppFmbK4sF27DTs6jWZkCAwEAAaOBqTCBpjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0T +AQH/BAIwADAfBgNVHSMEGDAWgBTlYaDWxlM59DwScePMzs7KeRP3yTBlBgNVHREE +XjBcghNhcnRlbWlzLWJyb2tlci1zcy0wgkVhcnRlbWlzLWJyb2tlci1zcy0wLmFy +dGVtaXMtYnJva2VyLWhkbHMtc3ZjLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWww +DQYJKoZIhvcNAQELBQADggEBAKGz/I2JBPOdulBoDrFDYgHV1Eud1VFsqwSVepF/ +FY2mdlHqI9rM+hkYrghPk0VTo5Htyx7EdOIuZPyBqKTCoFs1Uqe5tShSn8lOZaS1 +DPcPkfJSSYF30/HTNNCikpfZUjPFKD5Lg93T5QmaYmpuWlaH1nsTL4TSlNt47K6D +eajXHreg33H95QqlwB7ZLE3ffNR4EHUNV79MIf/zlbxJONcHgqZtRLUshIUtqPlx +zZ7G0T9WbJL0ORwOQn1epvME/grqS5hDh/GJBfrGvnvpwH4pOEJkJv9q49UBlLXf +u1OR6UhgqgPXa6hh6VkXx3VnZRK0G/dwLKv0sT5gCnQ1GTU= +-----END CERTIFICATE----- diff --git a/test-api-server.key b/test-api-server.key new file mode 100644 index 0000000..a3b7b80 --- /dev/null +++ b/test-api-server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0WfOLkHysco/8qeGRi0j6TvbOousm2aMMlBpow5LcY07mbYi +rH2Mqx1ZcILIf5GBxTrkGFdlo+sAwLAxXz0omGT5kI7V97IRpzSL8W06iCQIoNL3 +W9dRzBMTKh1ltqYWNyapw8YIbHz+vyyVKy6uCfd2qAlbXnA69L2o5JRxHJtZtOV+ +Nd6ATDhHvah2ohiiJ+BRrFJic/o1qS4JlvPpWeQSEDw8YmcMLIqDt1Vdl4sHXqNj +SsQ4CIGE+sHVvrQTDtHt8r1/ZKoJ5Ik3x0vaubYVS/lJYhAvWInXUUm9G5eyWCM7 +vWifcqJQMRAGmBGcTPCmkWZsriwXbsNOzqNZmQIDAQABAoIBABjyxR29vaxw7C18 +yAKUXjLrbrMK8QWSsiFMc0l56oMc0Hz/tiHW02uPk5hT/I82Rr+4xHQh9XoSBYTv +ePJf1vZREWqnmdZo4LGLESEyYkbWBDEk8VN/0778hsv9tKCOKRdpA9DPRzGlsrQU +G7GJXjLRyNE8TCZ0OJHwBq81AEToBhfvsaNwazP/NEB7oYoM9jEHh5Ahxy/Mh7/N +EmH7SXgIMWfoylp/kyKNuNKA5hxjCVL/0QIXc15bILo0Fn+778EexfPi75Tac0Pz +JQQ07pLUuS5a5fgpEMYEK4iaV3MU8aXmwRE6gIfOphPrk8KcKgTdnmpx556HNttv +vLvLaAECgYEA7XsfRGI5HxINxJwAV9t0Jn7J8fxVQipgnN4C2f/zGtjIZ6OWnHBu +M/OBgr1Z4S/ItIzBSOMNK0EdS+dJGasQckL/5G2vdl9yWp6O9JGCHgZMfmP+GhDW +QptgEHRXjPRPzWuBV/JIAyFcdoRF7rZS3gKxMtMEtO+/HdjXaaXHhfUCgYEA4bw1 +IATj2bz47ih68Gz8mzOnZ04b1kTBvijpil//Yc4rGA910BmD8Ei+qeMbNBnR37yZ +lAwotPtgDmjSaqPSVVoIXNemDXx+w0KesJvBxvAoJX5nohotCquwi7CwFohywWvV +EjhjLQdVIfvywm7cwWdf3n7lUnvXSkkddp4ZmpUCgYBp5JfJn17HKv62p7VDd9iv +/aNA4vqFeW4BJMHywT1+wCGEjR5wfXW2dqNOT+6PCgad85GQVaYenndYzDX9WxkH +SjbefcZaqy7Ll545EdUKXFapmR7KMq3Hn47TZ31OnfYjrAdN1vwjYTHgqxSf3+7N +jjfDaPLVV35J6dIMCt8QLQKBgQCxGbDwWwXMOWdvqhCx+j/BIChxcyWB2NXL9Fst +th0txcunh9Gdn7cU2G3F6ajZGny/NT+kmFmDjEiTZYfYJIkLb6Rp+sKLiCYH2YeY +9cp04swMhnyWAEVgPs02+ztboleuCoTTU6vzkvImxH10L/hAQHNFo3cVXJXO8UgN +XQKndQKBgQDK2bvBlXqjSgza6/7tVlb1XRrteyK2q4b6ucH6NjIJ8MAp2tGrgt05 +8BMZztbQu9dnXWEqgblT/GqVy6TGHGiudufjKlAZw4txBHwIvn0tHujnAiz4/YNh +Qs1bkW0CoX1JkJRpdT+P+T27C55rEuJR6jCh4Mwu3n6G/FNAsjOQTA== +-----END RSA PRIVATE KEY----- diff --git a/tsconfig.json b/tsconfig.json index 77ea373..292a544 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "baseUrl": ".", "target": "es2020", - "lib": [ - "es2020" - ], + "lib": ["es2020"], "module": "CommonJS", "moduleResolution": "node", "outDir": "./dist", @@ -16,6 +14,6 @@ "@app/*": ["src/*"] } }, - "include": ["./src/*", "./env"], + "include": ["./src/*"], "exclude": ["node_modules", "dist"] } diff --git a/yarn.lock b/yarn.lock index d228142..2eabcd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -629,6 +629,21 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -732,6 +747,13 @@ resolved "https://registry.yarnpkg.com/@types/base-64/-/base-64-1.0.2.tgz#f7bc80d242306f20c57f076d79d1efe2d31032ca" integrity sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw== +"@types/bcrypt@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.2.tgz#22fddc11945ea4fbc3655b3e8b8847cc9f811477" + integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -844,6 +866,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/jsonwebtoken@*": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz#e49b96c2b29356ed462e9708fc73b833014727d2" + integrity sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg== + dependencies: + "@types/node" "*" + "@types/jsonwebtoken@^9.0.6": version "9.0.6" resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" @@ -883,6 +912,29 @@ dependencies: undici-types "~5.26.4" +"@types/passport-jwt@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-4.0.1.tgz#080fbe934fb9f6954fb88ec4cdf4bb2cc7c4d435" + integrity sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ== + dependencies: + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.38" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3" + integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*", "@types/passport@^1.0.16": + version "1.0.16" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.16.tgz#5a2918b180a16924c4d75c31254c31cdca5ce6cf" + integrity sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A== + dependencies: + "@types/express" "*" + "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -1183,6 +1235,11 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -1366,6 +1423,19 @@ append-field@^1.0.0: resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -1550,6 +1620,14 @@ base-64@^1.0.0: resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== +bcrypt@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -1805,6 +1883,11 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chromatic@6.10.1: version "6.10.1" resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-6.10.1.tgz#5a52e7d616dfd35b6857d724d503cb7baa842922" @@ -1936,11 +2019,24 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colorette@^2.0.19: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +combine-errors@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/combine-errors/-/combine-errors-3.0.3.tgz#f4df6740083e5703a3181110c2b10551f003da86" + integrity sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q== + dependencies: + custom-error-instance "2.1.1" + lodash.uniqby "4.5.0" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1990,6 +2086,11 @@ configstore@^3.0.0: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -2116,6 +2217,11 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +custom-error-instance@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a" + integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -2198,6 +2304,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -2213,6 +2324,11 @@ detect-indent@^7.0.1: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -3000,6 +3116,33 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-json-store@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/fs-json-store/-/fs-json-store-8.0.1.tgz#b3c0b4331e5a83353a38c981dbdb11a5a1c558c9" + integrity sha512-S+aQIltxLYl4edoA8c39eJU+sjSVg7vVpLsyEbg4KYiclF/f0REQsjBLOnn0CWszL+EIiriNxoYDKU5Xl2zGmA== + dependencies: + combine-errors "^3.0.3" + fs-no-eperm-anymore "^5.0.0" + imurmurhash "^0.1.4" + kind-of "^6.0.3" + proper-lockfile "^4.1.2" + signal-exit "^3.0.4" + tslib "^2.3.1" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-no-eperm-anymore@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-no-eperm-anymore/-/fs-no-eperm-anymore-5.0.0.tgz#8ae7881c322c8b4520c6518a2d6796bafbba1463" + integrity sha512-ZUAG08SqwBh3M7oslddChWzhGxeKTLN9I+xM0FQJXivPjaKJeIOnvUCOIryRt5OYlABDNHqXPHS8zj11/8UBnw== + dependencies: + tslib "^2.3.1" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3015,6 +3158,21 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -3274,6 +3432,11 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -4303,12 +4466,12 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonwebtoken@^9.0.2: +jsonwebtoken@^9.0.0, jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -4355,7 +4518,7 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.1.5" -kind-of@^6.0.0: +kind-of@^6.0.0, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -4481,6 +4644,43 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash._baseiteratee@~4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102" + integrity sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ== + dependencies: + lodash._stringtopath "~4.8.0" + +lodash._basetostring@~4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz#9327c9dc5158866b7fa4b9d42f4638e5766dd9df" + integrity sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw== + +lodash._baseuniq@~4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" + integrity sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A== + dependencies: + lodash._createset "~4.0.0" + lodash._root "~3.0.0" + +lodash._createset@~4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" + integrity sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA== + +lodash._root@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + integrity sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ== + +lodash._stringtopath@~4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz#941bcf0e64266e5fc1d66fed0a6959544c576824" + integrity sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ== + dependencies: + lodash._basetostring "~4.12.0" + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -4541,6 +4741,14 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== +lodash.uniqby@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz#a3a17bbf62eeb6240f491846e97c1c4e2a5e1e21" + integrity sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ== + dependencies: + lodash._baseiteratee "~4.7.0" + lodash._baseuniq "~4.6.0" + lodash.zipobject@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz#b399f5aba8ff62a746f6979bf20b214f964dbef8" @@ -4608,6 +4816,13 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -4806,6 +5021,26 @@ minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -4813,6 +5048,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.4: dependencies: minimist "^1.2.6" +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mri@^1.1.5: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -4886,6 +5126,11 @@ nock@^13.5.4: json-stringify-safe "^5.0.1" propagate "^2.0.0" +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + node-fetch-h2@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz#c6188325f9bd3d834020bf0f2d6dc17ced2241ac" @@ -4893,7 +5138,7 @@ node-fetch-h2@^2.3.0: dependencies: http2-client "^1.2.5" -node-fetch@2, node-fetch@^2.6.1: +node-fetch@2, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -4917,6 +5162,13 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -4970,6 +5222,16 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -5255,6 +5517,28 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -5333,6 +5617,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" @@ -5660,6 +5949,15 @@ propagate@^2.0.0: resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -5816,7 +6114,7 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1: +readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -6021,6 +6319,11 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -6117,7 +6420,7 @@ semver-diff@^2.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -6127,6 +6430,11 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +semver@^7.3.5: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -6163,6 +6471,11 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -6258,7 +6571,7 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.4, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -6453,15 +6766,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6470,6 +6775,14 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string-width@^5.0.0: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -6739,6 +7052,18 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" @@ -6957,6 +7282,15 @@ ts-node@10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -6967,6 +7301,11 @@ tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.3.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -7162,7 +7501,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1: +utils-merge@1.0.1, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== @@ -7343,6 +7682,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + widest-line@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" @@ -7449,6 +7795,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@*, yaml@^2.1.3, yaml@^2.4.5: version "2.4.5" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"