From 391781c89257df38eb094dffce7ddcc0152b5ad4 Mon Sep 17 00:00:00 2001 From: Howard Gao Date: Sat, 12 Oct 2024 23:35:51 +0800 Subject: [PATCH] [#16] Implementing security for api-server --- .access.json | 6 + .endpoints.json | 4 + .env | 18 +- .gitignore | 5 + .roles.json | 4 + .test.access.json | 23 + .test.endpoints.json | 57 ++ .test.env | 26 + .test.roles.json | 16 + .test.users.json | 26 + .users.json | 8 + README.md | 64 ++- api.md | 446 ++++++++++++--- package.json | 18 +- src/api/apiutil/artemis_jolokia.ts | 306 +++++----- src/api/controllers/api_impl.ts | 68 ++- src/api/controllers/endpoint_manager.ts | 67 +++ src/api/controllers/security.ts | 311 +++++++++-- src/api/controllers/security_manager.ts | 331 +++++++++++ src/app.ts | 8 +- src/config/openapi.yml | 299 +++++++++- src/utils/logger.ts | 6 +- src/utils/security_util.ts | 186 +++++++ src/utils/server.security.test.ts | 704 ++++++++++++++++++++++++ src/utils/server.test.ts | 17 +- src/utils/server.ts | 24 +- test-api-server.crt | 22 + test-api-server.key | 27 + tsconfig.json | 6 +- yarn.lock | 82 ++- 30 files changed, 2883 insertions(+), 302 deletions(-) create mode 100644 .access.json create mode 100644 .endpoints.json create mode 100644 .roles.json create mode 100644 .test.access.json create mode 100644 .test.endpoints.json create mode 100644 .test.env create mode 100644 .test.roles.json create mode 100644 .test.users.json create mode 100644 .users.json create mode 100644 src/api/controllers/endpoint_manager.ts create mode 100644 src/api/controllers/security_manager.ts create mode 100644 src/utils/security_util.ts create mode 100644 src/utils/server.security.test.ts create mode 100644 test-api-server.crt create mode 100644 test-api-server.key diff --git a/.access.json b/.access.json new file mode 100644 index 0000000..8411124 --- /dev/null +++ b/.access.json @@ -0,0 +1,6 @@ +{ + "endpoints": [ + ], + "admin": { + } +} diff --git a/.endpoints.json b/.endpoints.json new file mode 100644 index 0000000..a5a48bf --- /dev/null +++ b/.endpoints.json @@ -0,0 +1,4 @@ +{ + "endpoints": [ + ] +} diff --git a/.env b/.env index 9137b7a..6816f63 100644 --- a/.env +++ b/.env @@ -6,12 +6,20 @@ PLUGIN_NAME='ActiveMQ Artemis Jolokia api-server' SERVER_CERT=/var/serving-cert/tls.crt SERVER_KEY=/var/serving-cert/tls.key +# logging +LOG_LEVEL='info' +ENABLE_REQUEST_LOG='false' + +# security + # replace the token in production deployment SECRET_ACCESS_TOKEN=1e13d44f998dee277deae621a9012cf300b94c91 -# to trust jolokia certs -NODE_TLS_REJECT_UNAUTHORIZED='0' +API_SERVER_SECURITY_ENABLED=true +API_SERVER_SECURITY_AUTH_TYPE=jwt +USERS_FILE_URL=.users.json +ROLES_FILE_URL=.roles.json +ENDPOINTS_FILE_URL=.endpoints.json +ACCESS_CONTROL_FILE_URL=.access.json -# logging -LOG_LEVEL='info' -ENABLE_REQUEST_LOG='false' +API_SERVER_ADMIN_USER=admin diff --git a/.gitignore b/.gitignore index dd38b46..183b951 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,8 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# vs code config +.vscode + + diff --git a/.roles.json b/.roles.json new file mode 100644 index 0000000..af8aded --- /dev/null +++ b/.roles.json @@ -0,0 +1,4 @@ +{ + "roles": [ + ] +} 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..e850e87 --- /dev/null +++ b/.test.env @@ -0,0 +1,26 @@ + +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 + +API_SERVER_ADMIN_USER=admin 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..b6c8d4f --- /dev/null +++ b/.test.users.json @@ -0,0 +1,26 @@ +{ + "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" + }, + { + "id": "usernoroles", + "hash": "$2a$12$J.CGc062y9YhdvYEqqiqoetDronJwUFKR0f2fGARwgHuasAt/QKa2" + }, + { + "id": "admin", + "hash": "$2a$15$deBI0sO4Cvn2gr/QA5pju.94Klh377Np.mcYteBgYlQuyeIwyK4UK" + } + ] +} diff --git a/.users.json b/.users.json new file mode 100644 index 0000000..195b978 --- /dev/null +++ b/.users.json @@ -0,0 +1,8 @@ +{ + "users": [ + { + "id": "admin", + "hash": "$2a$15$deBI0sO4Cvn2gr/QA5pju.94Klh377Np.mcYteBgYlQuyeIwyK4UK" + } + ] +} diff --git a/README.md b/README.md index 57414ce..33e2814 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,68 @@ jwt tokens. It has a default value in .env for dev purposes. In production you should override it with your own secret. -The jwt-key-gen.sh is a tool to generate a random key and used in Dockerfile. +The jwt-key-gen.sh is a tool to generate a random key and used in Dockerfile. It makes sure when you build the api server image a new random key is used. +## 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. + +--- + +**NOTE** + +The api server can be configured with a `super user` that has full access to the +api server's APIs without authorization. The default super user name is `admin` and +password is `admin`. You can use env var `API_SERVER_ADMIN_USER` to appoint a different +super user. + +## Due to its all powerfulness, it is adviced to configure a `super user` to replace the default one. + +### 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 b57a221..bf5162a 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 70bc60e..5915265 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@types/base-64": "^1.0.2", + "@types/bcryptjs": "^2.4.6", "@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,24 +65,29 @@ "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.2", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "dependencies": { - "cors": "^2.8.5", "base-64": "^1.0.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "4.18.2", "express-openapi-validator": "5.1.2", + "express-pino-logger": "^7.0.0", "express-rate-limit": "^7.2.0", - "yaml": "^2.4.5", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", "node-fetch": "2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pino": "^9.5.0", "swagger-routes-express": "^3.3.2", - "express-pino-logger": "^7.0.0", - "pino": "^9.5.0" + "yaml": "^2.4.5" } } 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..f3a87e7 --- /dev/null +++ b/src/api/controllers/security_manager.ts @@ -0,0 +1,331 @@ +import fs from 'fs'; +import yaml from 'js-yaml'; +import * as bcrypt from 'bcryptjs'; +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 => { + if (!process.env.API_SERVER_SECURITY_AUTH_TYPE) { + return AuthType.Jwt; + } + if (process.env.API_SERVER_SECURITY_AUTH_TYPE === 'jwt') { + return AuthType.Jwt; + } + return AuthType.Unknown; +}; + +export interface SecurityManager { + start(): Promise; + 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 { + isSuperUser(user: User): boolean; + 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>(); + + superUser: string; + + isSuperUser(user: User): boolean { + if (this.superUser) { + return this.superUser === user.id; + } + return false; + } + + 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', + ); + if (process.env.API_SERVER_ADMIN_USER) { + this.superUser = process.env.API_SERVER_ADMIN_USER; + } else { + this.superUser = undefined; + } + 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 => { + if (!this.securityStore.isSuperUser(user)) { + 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); + } + }), + ); + }); + }; +} + +let securityManager: SecurityManager; + +export const InitSecurity = async () => { + if (IsSecurityEnabled()) { + const securityManager = GetSecurityManager(); + await securityManager.start(); + } +}; + +export const GetSecurityManager = (): SecurityManager => { + if (!securityManager) { + const authType = getAuthType(); + if (authType === AuthType.Jwt) { + securityManager = new JwtSecurityManager(); + } else { + throw Error('Auth type not supported ' + authType); + } + } + return securityManager; +}; + +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 488d1d4..960bc93 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: [] 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: @@ -1416,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_util.ts b/src/utils/security_util.ts new file mode 100644 index 0000000..9e9b902 --- /dev/null +++ b/src/utils/security_util.ts @@ -0,0 +1,186 @@ +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', + Unknown = 'unknown', +} + +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..f34b3e1 --- /dev/null +++ b/src/utils/server.security.test.ts @@ -0,0 +1,704 @@ +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; +let mockBroker2: nock.Scope; +let mockBroker3: nock.Scope; +let mockBroker4: nock.Scope; + +const apiUrlBase = 'https://localhost:9444/api/v1'; +const apiUrlPrefix = '/console/jolokia'; +const strictedApiUrlPrefix = '/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 broker2EndpointUrl = 'http://127.0.0.2:8161'; +const broker3EndpointUrl = 'http://127.0.0.3:8161'; +const broker4EndpointUrl = + 'https://artemis-broker-jolokia-0-svc-ing-default.artemiscloud.io:443'; + +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); + mockBroker2 = nock(broker2EndpointUrl); + mockBroker3 = nock(broker3EndpointUrl); + mockBroker4 = nock(broker4EndpointUrl); +}; + +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(5); + expect(users.has('user1')).toBeTruthy(); + expect(users.has('user2')).toBeTruthy(); + expect(users.has('root')).toBeTruthy(); + expect(users.has('usernoroles')).toBeTruthy(); + //super user + expect(users.has('admin')).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: 'amq-broker1', + }, + ]; + + const jolokiaResp1 = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker1"'], + 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(jolokiaResp1)); + + 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="amq-broker1"'], + 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="amq-broker1"', + 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="amq-broker1"' && + 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-broker1"'], + 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); + }); +}); + +describe('test endpoint access with permission denied without roles', () => { + let jwtToken: string; + + beforeAll(async () => { + const result = await doServerLogin('usernoroles', 'password1'); + jwtToken = result.authToken; + expect(result.accessToken).toBeNull(); + expect(jwtToken.length).toBeGreaterThan(0); + }); + + it('test get brokers get denied on all brokers', async () => { + const jolokiaResp1 = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker1"'], + timestamp: 1714703745, + status: 200, + }; + const jolokiaResp2 = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker2"'], + timestamp: 1714703745, + status: 200, + }; + const jolokiaResp3 = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker3"'], + timestamp: 1714703745, + status: 200, + }; + const jolokiaResp4 = { + request: {}, + value: ['org.apache.activemq.artemis:broker="amq-broker4"'], + 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(jolokiaResp1)); + + mockBroker2 + .persist() //use persist when this path will get called more than once. + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaResp2)); + + mockBroker3 + .persist() //use persist when this path will get called more than once. + .get(apiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*') + .reply(200, JSON.stringify(jolokiaResp3)); + + mockBroker4 + .persist() //use persist when this path will get called more than once. + .get( + strictedApiUrlPrefix + '/search/org.apache.activemq.artemis:broker=*', + ) + .reply(200, JSON.stringify(jolokiaResp4)); + + const resp = await doGet( + '/brokers' + '?targetEndpoint=broker1', + null, + jwtToken, + ); + + expect(resp.ok).not.toBeTruthy(); + expect(resp.status).toEqual(401); + + const resp1 = await doGet( + '/brokers' + '?targetEndpoint=broker2', + null, + jwtToken, + ); + + expect(resp1.ok).not.toBeTruthy(); + expect(resp1.status).toEqual(401); + + const resp2 = await doGet( + '/brokers' + '?targetEndpoint=broker3', + null, + jwtToken, + ); + + expect(resp2.ok).not.toBeTruthy(); + expect(resp2.status).toEqual(401); + }); +}); + +describe('test endpoint access with super user', () => { + let jwtToken: string; + + beforeAll(async () => { + const result = await doServerLogin('admin', 'admin'); + jwtToken = result.authToken; + expect(result.accessToken).toBeNull(); + expect(jwtToken.length).toBeGreaterThan(0); + }); + + it('test super user has access on all brokers', 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 resp1 = await doGet( + '/brokers' + '?targetEndpoint=broker1', + null, + jwtToken, + ); + + expect(resp1.ok).toBeTruthy(); + expect(resp1.status).toEqual(200); + + await resp1.json().then((value) => { + expect(value).toEqual([{ name: 'amq-broker1' }]); + }); + + const resp2 = await doGet( + '/brokers' + '?targetEndpoint=broker2', + null, + jwtToken, + ); + + expect(resp2.ok).toBeTruthy(); + expect(resp2.status).toEqual(200); + + await resp2.json().then((value) => { + expect(value).toEqual([{ name: 'amq-broker2' }]); + }); + + const resp3 = await doGet( + '/brokers' + '?targetEndpoint=broker3', + null, + jwtToken, + ); + + expect(resp3.ok).toBeTruthy(); + expect(resp3.status).toEqual(200); + + await resp3.json().then((value) => { + expect(value).toEqual([{ name: 'amq-broker3' }]); + }); + + const resp4 = await doGet( + '/brokers' + '?targetEndpoint=broker4', + null, + jwtToken, + ); + + expect(resp4.ok).toBeTruthy(); + expect(resp4.status).toEqual(200); + + await resp4.json().then((value) => { + expect(value).toEqual([{ name: 'amq-broker4' }]); + }); + }); +}); diff --git a/src/utils/server.test.ts b/src/utils/server.test.ts index 5ef4c0d..e121eb8 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..d74df30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -732,6 +732,11 @@ resolved "https://registry.yarnpkg.com/@types/base-64/-/base-64-1.0.2.tgz#f7bc80d242306f20c57f076d79d1efe2d31032ca" integrity sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw== +"@types/bcryptjs@^2.4.6": + version "2.4.6" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz#2b92e3c2121c66eba3901e64faf8bb922ec291fa" + integrity sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ== + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -844,6 +849,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 +895,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" @@ -1550,6 +1585,11 @@ 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== +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -4303,12 +4343,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== @@ -5255,6 +5295,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 +5395,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" @@ -6957,6 +7024,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" @@ -7162,7 +7238,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==