diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5f3c1e4effb..f733819f5e9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -493,7 +493,9 @@ jobs: --tag cmd-api-server \ --tag "ghcr.io/hyperledger/cactus-cmd-api-server:$(date +"%Y-%m-%dT%H-%M-%S" --utc)-dev-$(git rev-parse --short HEAD)" - - if: ${{ env.RUN_TRIVY_SCAN == 'true' }} + - if: | + ${{ env.RUN_TRIVY_SCAN == 'true' + && (env.DAY_OF_WEEK == '4' || env.DAY_OF_WEEK == '7') }} name: Run Trivy vulnerability scan for cmd-api-server uses: aquasecurity/trivy-action@0.19.0 with: diff --git a/packages/cactus-cmd-api-server/package.json b/packages/cactus-cmd-api-server/package.json index bc904396d85..3b283060b9d 100644 --- a/packages/cactus-cmd-api-server/package.json +++ b/packages/cactus-cmd-api-server/package.json @@ -89,6 +89,7 @@ "fastify": "4.28.1", "fs-extra": "11.2.0", "google-protobuf": "3.21.4", + "http-status-codes": "2.1.3", "jose": "4.15.5", "json-stable-stringify": "1.0.2", "lmify": "0.3.0", @@ -134,7 +135,6 @@ "google-protobuf": "3.21.4", "grpc-tools": "1.12.4", "grpc_tools_node_protoc_ts": "5.3.3", - "http-status-codes": "2.1.4", "protobufjs": "7.4.0", "tsx": "4.16.2" }, diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/.openapi-generator/FILES b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/.openapi-generator/FILES index 8c0f008a05f..3b1e0f7513b 100644 --- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/.openapi-generator/FILES +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/.openapi-generator/FILES @@ -6,6 +6,7 @@ client.go configuration.go go.mod go.sum +model_cmd_api_server_endpoint_error_response.go model_health_check_response.go model_memory_usage.go model_watch_healthcheck_v1.go diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md index 548fe6c56c8..2eff5f1a8e8 100644 --- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/README.md @@ -84,6 +84,7 @@ Class | Method | HTTP request | Description ## Documentation For Models + - [CmdApiServerEndpointErrorResponse](docs/CmdApiServerEndpointErrorResponse.md) - [HealthCheckResponse](docs/HealthCheckResponse.md) - [MemoryUsage](docs/MemoryUsage.md) - [WatchHealthcheckV1](docs/WatchHealthcheckV1.md) @@ -91,7 +92,18 @@ Class | Method | HTTP request | Description ## Documentation For Authorization -Endpoints do not require authorization. + +Authentication schemes defined for the API: +### bearerTokenAuth + +- **Type**: HTTP Bearer token authentication + +Example + +```golang +auth := context.WithValue(context.Background(), sw.ContextAccessToken, "BEARER_TOKEN_STRING") +r, err := client.Service.Operation(auth, args) +``` ## Documentation for Utility Methods diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml index b25ac93cc3c..b3391e5b6c4 100644 --- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api/openapi.yaml @@ -8,6 +8,11 @@ info: version: 2.0.0 servers: - url: / +security: +- bearerTokenAuth: + - read:health + - read:metrics + - read:spec paths: /api/v1/api-server/healthcheck: get: @@ -21,6 +26,21 @@ paths: schema: $ref: '#/components/schemas/HealthCheckResponse' description: OK + "401": + content: + '*/*': + schema: + $ref: '#/components/schemas/CmdApiServerEndpointErrorResponse' + description: Unauthorized - Invalid token + "403": + content: + '*/*': + schema: + $ref: '#/components/schemas/CmdApiServerEndpointErrorResponse' + description: Forbidden - Valid token but missing correct scope + security: + - bearerTokenAuth: + - read:health summary: Can be used to verify liveness of an API server instance x-hyperledger-cacti: http: @@ -37,6 +57,21 @@ paths: schema: $ref: '#/components/schemas/PrometheusExporterMetricsResponse' description: OK + "401": + content: + '*/*': + schema: + $ref: '#/components/schemas/CmdApiServerEndpointErrorResponse' + description: Unauthorized - Invalid token + "403": + content: + '*/*': + schema: + $ref: '#/components/schemas/CmdApiServerEndpointErrorResponse' + description: Forbidden - Valid token but missing correct scope + security: + - bearerTokenAuth: + - read:metrics summary: Get the Prometheus Metrics x-hyperledger-cacti: http: @@ -54,6 +89,21 @@ paths: schema: $ref: '#/components/schemas/GetOpenApiSpecV1EndpointResponse' description: OK + "401": + content: + '*/*': + schema: + $ref: '#/components/schemas/CmdApiServerEndpointErrorResponse' + description: Unauthorized - Invalid token + "403": + content: + '*/*': + schema: + $ref: '#/components/schemas/CmdApiServerEndpointErrorResponse' + description: Forbidden - Valid token but missing correct scope + security: + - bearerTokenAuth: + - read:spec x-hyperledger-cacti: http: verbLowerCase: get @@ -127,3 +177,14 @@ components: GetOpenApiSpecV1EndpointResponse: nullable: false type: string + CmdApiServerEndpointErrorResponse: + properties: + message: + example: | + Forbidden - Valid token but missing correct scope + type: string + securitySchemes: + bearerTokenAuth: + bearerFormat: JSON Web Tokens + scheme: bearer + type: http diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api_default.go b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api_default.go index 27da765c0c8..58c64eaeed9 100644 --- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api_default.go +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/api_default.go @@ -77,7 +77,7 @@ func (a *DefaultApiService) GetHealthCheckV1Execute(r ApiGetHealthCheckV1Request } // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} + localVarHTTPHeaderAccepts := []string{"application/json", "*/*"} // set Accept header localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) @@ -106,6 +106,27 @@ func (a *DefaultApiService) GetHealthCheckV1Execute(r ApiGetHealthCheckV1Request body: localVarBody, error: localVarHTTPResponse.Status, } + if localVarHTTPResponse.StatusCode == 401 { + var v CmdApiServerEndpointErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v CmdApiServerEndpointErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } return localVarReturnValue, localVarHTTPResponse, newErr } @@ -176,7 +197,7 @@ func (a *DefaultApiService) GetOpenApiSpecV1Execute(r ApiGetOpenApiSpecV1Request } // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} + localVarHTTPHeaderAccepts := []string{"application/json", "*/*"} // set Accept header localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) @@ -205,6 +226,27 @@ func (a *DefaultApiService) GetOpenApiSpecV1Execute(r ApiGetOpenApiSpecV1Request body: localVarBody, error: localVarHTTPResponse.Status, } + if localVarHTTPResponse.StatusCode == 401 { + var v CmdApiServerEndpointErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v CmdApiServerEndpointErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } return localVarReturnValue, localVarHTTPResponse, newErr } @@ -273,7 +315,7 @@ func (a *DefaultApiService) GetPrometheusMetricsV1Execute(r ApiGetPrometheusMetr } // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"text/plain"} + localVarHTTPHeaderAccepts := []string{"text/plain", "*/*"} // set Accept header localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) @@ -302,6 +344,27 @@ func (a *DefaultApiService) GetPrometheusMetricsV1Execute(r ApiGetPrometheusMetr body: localVarBody, error: localVarHTTPResponse.Status, } + if localVarHTTPResponse.StatusCode == 401 { + var v CmdApiServerEndpointErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v CmdApiServerEndpointErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } return localVarReturnValue, localVarHTTPResponse, newErr } diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go index a3b0f197443..b9e16de3b6a 100644 --- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/client.go @@ -410,6 +410,11 @@ func (c *APIClient) prepareRequest( // Walk through any authentication. + // AccessToken Authentication + if auth, ok := ctx.Value(ContextAccessToken).(string); ok { + localVarRequest.Header.Add("Authorization", "Bearer "+auth) + } + } for header, value := range c.cfg.DefaultHeader { diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go index c69a1313a52..6242dfec4f9 100644 --- a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/configuration.go @@ -28,6 +28,9 @@ func (c contextKey) String() string { } var ( + // ContextAccessToken takes a string oauth2 access token as authentication for the request. + ContextAccessToken = contextKey("accesstoken") + // ContextServerIndex uses a server configuration from the index. ContextServerIndex = contextKey("serverIndex") diff --git a/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/model_cmd_api_server_endpoint_error_response.go b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/model_cmd_api_server_endpoint_error_response.go new file mode 100644 index 00000000000..d3fa00b9c9a --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/go/generated/openapi/go-client/model_cmd_api_server_endpoint_error_response.go @@ -0,0 +1,126 @@ +/* +Hyperledger Cactus API + +Interact with a Cactus deployment through HTTP. + +API version: 2.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package cactus-cmd-api-server + +import ( + "encoding/json" +) + +// checks if the CmdApiServerEndpointErrorResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CmdApiServerEndpointErrorResponse{} + +// CmdApiServerEndpointErrorResponse struct for CmdApiServerEndpointErrorResponse +type CmdApiServerEndpointErrorResponse struct { + Message *string `json:"message,omitempty"` +} + +// NewCmdApiServerEndpointErrorResponse instantiates a new CmdApiServerEndpointErrorResponse object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCmdApiServerEndpointErrorResponse() *CmdApiServerEndpointErrorResponse { + this := CmdApiServerEndpointErrorResponse{} + return &this +} + +// NewCmdApiServerEndpointErrorResponseWithDefaults instantiates a new CmdApiServerEndpointErrorResponse object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCmdApiServerEndpointErrorResponseWithDefaults() *CmdApiServerEndpointErrorResponse { + this := CmdApiServerEndpointErrorResponse{} + return &this +} + +// GetMessage returns the Message field value if set, zero value otherwise. +func (o *CmdApiServerEndpointErrorResponse) GetMessage() string { + if o == nil || IsNil(o.Message) { + var ret string + return ret + } + return *o.Message +} + +// GetMessageOk returns a tuple with the Message field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CmdApiServerEndpointErrorResponse) GetMessageOk() (*string, bool) { + if o == nil || IsNil(o.Message) { + return nil, false + } + return o.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (o *CmdApiServerEndpointErrorResponse) HasMessage() bool { + if o != nil && !IsNil(o.Message) { + return true + } + + return false +} + +// SetMessage gets a reference to the given string and assigns it to the Message field. +func (o *CmdApiServerEndpointErrorResponse) SetMessage(v string) { + o.Message = &v +} + +func (o CmdApiServerEndpointErrorResponse) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CmdApiServerEndpointErrorResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Message) { + toSerialize["message"] = o.Message + } + return toSerialize, nil +} + +type NullableCmdApiServerEndpointErrorResponse struct { + value *CmdApiServerEndpointErrorResponse + isSet bool +} + +func (v NullableCmdApiServerEndpointErrorResponse) Get() *CmdApiServerEndpointErrorResponse { + return v.value +} + +func (v *NullableCmdApiServerEndpointErrorResponse) Set(val *CmdApiServerEndpointErrorResponse) { + v.value = val + v.isSet = true +} + +func (v NullableCmdApiServerEndpointErrorResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableCmdApiServerEndpointErrorResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCmdApiServerEndpointErrorResponse(val *CmdApiServerEndpointErrorResponse) *NullableCmdApiServerEndpointErrorResponse { + return &NullableCmdApiServerEndpointErrorResponse{value: val, isSet: true} +} + +func (v NullableCmdApiServerEndpointErrorResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCmdApiServerEndpointErrorResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/packages/cactus-cmd-api-server/src/main/json/openapi.json b/packages/cactus-cmd-api-server/src/main/json/openapi.json index a9c63cf9d26..d4ee7c7681b 100644 --- a/packages/cactus-cmd-api-server/src/main/json/openapi.json +++ b/packages/cactus-cmd-api-server/src/main/json/openapi.json @@ -75,9 +75,29 @@ "GetOpenApiSpecV1EndpointResponse": { "type": "string", "nullable": false + }, + "CmdApiServerEndpointErrorResponse": { + "properties": { + "message": { + "type": "string", + "example": "Forbidden - Valid token but missing correct scope\n" + } + } + } + }, + "securitySchemes": { + "bearerTokenAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JSON Web Tokens" } } }, + "security": [ + { + "bearerTokenAuth": ["read:health", "read:metrics", "read:spec"] + } + ], "paths": { "/api/v1/api-server/healthcheck": { "get": { @@ -101,8 +121,33 @@ } } } + }, + "401": { + "description": "Unauthorized - Invalid token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - Valid token but missing correct scope", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } } - } + }, + "security": [ + { + "bearerTokenAuth": ["read:health"] + } + ] } }, "/api/v1/api-server/get-prometheus-exporter-metrics": { @@ -126,8 +171,33 @@ } } } + }, + "401": { + "description": "Unauthorized - Invalid token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - Valid token but missing correct scope", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } } - } + }, + "security": [ + { + "bearerTokenAuth": ["read:metrics"] + } + ] } }, "/api/v1/api-server/get-open-api-spec": { @@ -151,8 +221,33 @@ } } } + }, + "401": { + "description": "Unauthorized - Invalid token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - Valid token but missing correct scope", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } } - } + }, + "security": [ + { + "bearerTokenAuth": ["read:spec"] + } + ] } } } diff --git a/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json b/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json index a9c63cf9d26..d4ee7c7681b 100644 --- a/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json +++ b/packages/cactus-cmd-api-server/src/main/json/openapi.tpl.json @@ -75,9 +75,29 @@ "GetOpenApiSpecV1EndpointResponse": { "type": "string", "nullable": false + }, + "CmdApiServerEndpointErrorResponse": { + "properties": { + "message": { + "type": "string", + "example": "Forbidden - Valid token but missing correct scope\n" + } + } + } + }, + "securitySchemes": { + "bearerTokenAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JSON Web Tokens" } } }, + "security": [ + { + "bearerTokenAuth": ["read:health", "read:metrics", "read:spec"] + } + ], "paths": { "/api/v1/api-server/healthcheck": { "get": { @@ -101,8 +121,33 @@ } } } + }, + "401": { + "description": "Unauthorized - Invalid token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - Valid token but missing correct scope", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } } - } + }, + "security": [ + { + "bearerTokenAuth": ["read:health"] + } + ] } }, "/api/v1/api-server/get-prometheus-exporter-metrics": { @@ -126,8 +171,33 @@ } } } + }, + "401": { + "description": "Unauthorized - Invalid token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - Valid token but missing correct scope", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } } - } + }, + "security": [ + { + "bearerTokenAuth": ["read:metrics"] + } + ] } }, "/api/v1/api-server/get-open-api-spec": { @@ -151,8 +221,33 @@ } } } + }, + "401": { + "description": "Unauthorized - Invalid token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - Valid token but missing correct scope", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CmdApiServerEndpointErrorResponse" + } + } + } } - } + }, + "security": [ + { + "bearerTokenAuth": ["read:spec"] + } + ] } } } diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES index 9354990b616..e1a27d52906 100644 --- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES @@ -21,6 +21,7 @@ src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt src/main/kotlin/org/openapitools/client/infrastructure/URIAdapter.kt src/main/kotlin/org/openapitools/client/infrastructure/UUIDAdapter.kt +src/main/kotlin/org/openapitools/client/models/CmdApiServerEndpointErrorResponse.kt src/main/kotlin/org/openapitools/client/models/HealthCheckResponse.kt src/main/kotlin/org/openapitools/client/models/MemoryUsage.kt src/main/kotlin/org/openapitools/client/models/WatchHealthcheckV1.kt diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md index a9770d9c2ef..dbb033f5b69 100644 --- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md @@ -52,6 +52,7 @@ Class | Method | HTTP request | Description ## Documentation for Models + - [org.openapitools.client.models.CmdApiServerEndpointErrorResponse](docs/CmdApiServerEndpointErrorResponse.md) - [org.openapitools.client.models.HealthCheckResponse](docs/HealthCheckResponse.md) - [org.openapitools.client.models.MemoryUsage](docs/MemoryUsage.md) - [org.openapitools.client.models.WatchHealthcheckV1](docs/WatchHealthcheckV1.md) @@ -60,5 +61,10 @@ Class | Method | HTTP request | Description ## Documentation for Authorization -Endpoints do not require authorization. + +Authentication schemes defined for the API: + +### bearerTokenAuth + +- **Type**: HTTP basic authentication diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt index d05dc1394b8..9464bd4d053 100644 --- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -19,6 +19,7 @@ import java.io.IOException import okhttp3.OkHttpClient import okhttp3.HttpUrl +import org.openapitools.client.models.CmdApiServerEndpointErrorResponse import org.openapitools.client.models.HealthCheckResponse import com.squareup.moshi.Json @@ -108,7 +109,7 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient path = "/api/v1/api-server/healthcheck", query = localVariableQuery, headers = localVariableHeaders, - requiresAuthentication = false, + requiresAuthentication = true, body = localVariableBody ) } @@ -176,7 +177,7 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient path = "/api/v1/api-server/get-open-api-spec", query = localVariableQuery, headers = localVariableHeaders, - requiresAuthentication = false, + requiresAuthentication = true, body = localVariableBody ) } @@ -243,7 +244,7 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient path = "/api/v1/api-server/get-prometheus-exporter-metrics", query = localVariableQuery, headers = localVariableHeaders, - requiresAuthentication = false, + requiresAuthentication = true, body = localVariableBody ) } diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index ea4b7b65935..c83d4d4d575 100644 --- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -143,10 +143,20 @@ open class ApiClient(val baseUrl: String, val client: OkHttpClient = defaultClie } } + protected fun updateAuthParams(requestConfig: RequestConfig) { + if (requestConfig.headers[Authorization].isNullOrEmpty()) { + accessToken?.let { accessToken -> + requestConfig.headers[Authorization] = "Bearer $accessToken" + } + } + } protected inline fun request(requestConfig: RequestConfig): ApiResponse { val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.") + // take authMethod from operation + updateAuthParams(requestConfig) + val url = httpUrl.newBuilder() .addEncodedPathSegments(requestConfig.path.trimStart('/')) .apply { diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/CmdApiServerEndpointErrorResponse.kt b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/CmdApiServerEndpointErrorResponse.kt new file mode 100644 index 00000000000..a1502dda6a1 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/CmdApiServerEndpointErrorResponse.kt @@ -0,0 +1,35 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.models + + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param message + */ + + +data class CmdApiServerEndpointErrorResponse ( + + @Json(name = "message") + val message: kotlin.String? = null + +) + diff --git a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/.openapi-generator/FILES b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/.openapi-generator/FILES index 270cf21ca1a..779704fc76e 100644 --- a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/.openapi-generator/FILES +++ b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/.openapi-generator/FILES @@ -1,4 +1,5 @@ README.md +models/cmd_api_server_endpoint_error_response_pb.proto models/health_check_response_pb.proto models/memory_usage_pb.proto models/watch_healthcheck_v1_pb.proto diff --git a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/models/cmd_api_server_endpoint_error_response_pb.proto b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/models/cmd_api_server_endpoint_error_response_pb.proto new file mode 100644 index 00000000000..cf9db105765 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/models/cmd_api_server_endpoint_error_response_pb.proto @@ -0,0 +1,19 @@ +/* + Hyperledger Cactus API + + Interact with a Cactus deployment through HTTP. + + The version of the OpenAPI document: 2.0.0 + + Generated by OpenAPI Generator: https://openapi-generator.tech +*/ + +syntax = "proto3"; + +package org.hyperledger.cactus.cmd_api_server; + + +message CmdApiServerEndpointErrorResponsePB { + string message = 418054152; + +} diff --git a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto index e1d50cfdede..5297324f112 100644 --- a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto +++ b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto @@ -13,6 +13,7 @@ syntax = "proto3"; package org.hyperledger.cactus.cmd_api_server; import "google/protobuf/empty.proto"; +import "models/cmd_api_server_endpoint_error_response_pb.proto"; import "models/health_check_response_pb.proto"; service DefaultService { diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index 79a7e263419..76b05573857 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -81,6 +81,10 @@ import { GetOpenApiSpecV1Endpoint, IGetOpenApiSpecV1EndpointOptions, } from "./web-services/get-open-api-spec-v1-endpoint"; +import { + GetHealthcheckV1Endpoint, + IGetHealthcheckV1EndpointOptions, +} from "./web-services/get-healthcheck-v1-endpoint"; export interface IApiServerConstructorOptions { readonly pluginManagerOptions?: { pluginsPath: string }; @@ -640,6 +644,15 @@ export class ApiServer { const { logLevel } = this.options.config; const pluginRegistry = await this.getOrInitPluginRegistry(); + { + const opts: IGetHealthcheckV1EndpointOptions = { + process: global.process, + logLevel, + }; + const endpoint = new GetHealthcheckV1Endpoint(opts); + await registerWebServiceEndpoint(app, endpoint); + } + { const oasPath = OAS.paths["/api/v1/api-server/get-open-api-spec"]; @@ -657,23 +670,6 @@ export class ApiServer { await registerWebServiceEndpoint(app, endpoint); } - const healthcheckHandler = (req: Request, res: Response) => { - res.json({ - success: true, - createdAt: new Date(), - memoryUsage: process.memoryUsage(), - }); - }; - - const { "/api/v1/api-server/healthcheck": oasPath } = OAS.paths; - const { http } = oasPath.get["x-hyperledger-cacti"]; - const { path: httpPath, verbLowerCase: httpVerb } = http; - if (!isExpressHttpVerbMethodName(httpVerb)) { - const eMsg = `${fnTag} Invalid HTTP verb "${httpVerb}" in cmd-api-server OpenAPI specification for HTTP path: "${httpPath}"`; - throw new RuntimeError(eMsg); - } - app[httpVerb](httpPath, healthcheckHandler); - this.wsApi.on("connection", (socket: SocketIoSocket) => { const { id } = socket; const transport = socket.conn.transport.name; // in most cases, "polling" diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts index 6610e56b536..67b89bca7fc 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -23,6 +23,19 @@ import type { RequestArgs } from './base'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base'; +/** + * + * @export + * @interface CmdApiServerEndpointErrorResponse + */ +export interface CmdApiServerEndpointErrorResponse { + /** + * + * @type {string} + * @memberof CmdApiServerEndpointErrorResponse + */ + 'message'?: string; +} /** * * @export @@ -128,6 +141,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication bearerTokenAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -157,6 +174,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication bearerTokenAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -187,6 +208,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication bearerTokenAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/models/cmd_api_server_endpoint_error_response_pb.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/models/cmd_api_server_endpoint_error_response_pb.ts new file mode 100644 index 00000000000..5c031ffef49 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/models/cmd_api_server_endpoint_error_response_pb.ts @@ -0,0 +1,75 @@ +/** + * Generated by the protoc-gen-ts. DO NOT EDIT! + * compiler version: 3.19.1 + * source: models/cmd_api_server_endpoint_error_response_pb.proto + * git: https://github.com/thesayyn/protoc-gen-ts */ +import * as pb_1 from "google-protobuf"; +export namespace org.hyperledger.cactus.cmd_api_server { + export class CmdApiServerEndpointErrorResponsePB extends pb_1.Message { + #one_of_decls: number[][] = []; + constructor(data?: any[] | { + message?: string; + }) { + super(); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls); + if (!Array.isArray(data) && typeof data == "object") { + if ("message" in data && data.message != undefined) { + this.message = data.message; + } + } + } + get message() { + return pb_1.Message.getFieldWithDefault(this, 418054152, "") as string; + } + set message(value: string) { + pb_1.Message.setField(this, 418054152, value); + } + static fromObject(data: { + message?: string; + }): CmdApiServerEndpointErrorResponsePB { + const message = new CmdApiServerEndpointErrorResponsePB({}); + if (data.message != null) { + message.message = data.message; + } + return message; + } + toObject() { + const data: { + message?: string; + } = {}; + if (this.message != null) { + data.message = this.message; + } + return data; + } + serialize(): Uint8Array; + serialize(w: pb_1.BinaryWriter): void; + serialize(w?: pb_1.BinaryWriter): Uint8Array | void { + const writer = w || new pb_1.BinaryWriter(); + if (this.message.length) + writer.writeString(418054152, this.message); + if (!w) + return writer.getResultBuffer(); + } + static deserialize(bytes: Uint8Array | pb_1.BinaryReader): CmdApiServerEndpointErrorResponsePB { + const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new CmdApiServerEndpointErrorResponsePB(); + while (reader.nextField()) { + if (reader.isEndGroup()) + break; + switch (reader.getFieldNumber()) { + case 418054152: + message.message = reader.readString(); + break; + default: reader.skipField(); + } + } + return message; + } + serializeBinary(): Uint8Array { + return this.serialize(); + } + static deserializeBinary(bytes: Uint8Array): CmdApiServerEndpointErrorResponsePB { + return CmdApiServerEndpointErrorResponsePB.deserialize(bytes); + } + } +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts index 06d8bf0eb10..f4086197f86 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts @@ -4,7 +4,8 @@ * source: services/default_service.proto * git: https://github.com/thesayyn/protoc-gen-ts */ import * as dependency_1 from "./../google/protobuf/empty"; -import * as dependency_2 from "./../models/health_check_response_pb"; +import * as dependency_2 from "./../models/cmd_api_server_endpoint_error_response_pb"; +import * as dependency_3 from "./../models/health_check_response_pb"; import * as pb_1 from "google-protobuf"; import * as grpc_1 from "@grpc/grpc-js"; export namespace org.hyperledger.cactus.cmd_api_server { @@ -174,8 +175,8 @@ export namespace org.hyperledger.cactus.cmd_api_server { responseStream: false, requestSerialize: (message: dependency_1.google.protobuf.Empty) => Buffer.from(message.serialize()), requestDeserialize: (bytes: Buffer) => dependency_1.google.protobuf.Empty.deserialize(new Uint8Array(bytes)), - responseSerialize: (message: dependency_2.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB) => Buffer.from(message.serialize()), - responseDeserialize: (bytes: Buffer) => dependency_2.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB.deserialize(new Uint8Array(bytes)) + responseSerialize: (message: dependency_3.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB) => Buffer.from(message.serialize()), + responseDeserialize: (bytes: Buffer) => dependency_3.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB.deserialize(new Uint8Array(bytes)) }, GetOpenApiSpecV1: { path: "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetOpenApiSpecV1", @@ -197,7 +198,7 @@ export namespace org.hyperledger.cactus.cmd_api_server { } }; [method: string]: grpc_1.UntypedHandleCall; - abstract GetHealthCheckV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; + abstract GetHealthCheckV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract GetOpenApiSpecV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract GetPrometheusMetricsV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; } @@ -205,7 +206,7 @@ export namespace org.hyperledger.cactus.cmd_api_server { constructor(address: string, credentials: grpc_1.ChannelCredentials, options?: Partial) { super(address, credentials, options); } - GetHealthCheckV1: GrpcUnaryServiceInterface = (message: dependency_1.google.protobuf.Empty, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { + GetHealthCheckV1: GrpcUnaryServiceInterface = (message: dependency_1.google.protobuf.Empty, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { return super.GetHealthCheckV1(message, metadata, options, callback); }; GetOpenApiSpecV1: GrpcUnaryServiceInterface = (message: dependency_1.google.protobuf.Empty, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-healthcheck-v1-endpoint.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-healthcheck-v1-endpoint.ts new file mode 100644 index 00000000000..f1ee542cdaf --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-healthcheck-v1-endpoint.ts @@ -0,0 +1,117 @@ +import { StatusCodes } from "http-status-codes"; +import type { Express, Request, Response } from "express"; + +import { + Checks, + IAsyncProvider, + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { + handleRestEndpointException, + IHandleRestEndpointExceptionOptions, + registerWebServiceEndpoint, +} from "@hyperledger/cactus-core"; + +import OAS from "../../json/openapi.json"; + +export interface IGetHealthcheckV1EndpointOptions { + readonly logLevel?: LogLevelDesc; + readonly process: NodeJS.Process; +} + +export class GetHealthcheckV1Endpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GetHealthcheckV1Endpoint"; + + private readonly log: Logger; + + private readonly process: NodeJS.Process; + + public get className(): string { + return GetHealthcheckV1Endpoint.CLASS_NAME; + } + + constructor(public readonly opts: IGetHealthcheckV1EndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(opts, `${fnTag} arg opts`); + Checks.truthy(opts.process, `${fnTag} arg opts.process`); + + this.process = opts.process; + + const level = this.opts.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getAuthorizationOptionsProvider(): IAsyncProvider { + return { + get: async () => ({ + isProtected: true, + requiredRoles: this.oasPath.get.security[0].bearerTokenAuth, + }), + }; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): (typeof OAS.paths)["/api/v1/api-server/healthcheck"] { + return OAS.paths["/api/v1/api-server/healthcheck"]; + } + + public getPath(): string { + return this.oasPath.get["x-hyperledger-cacti"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.get["x-hyperledger-cacti"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.get.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + async handleRequest(_req: Request, res: Response): Promise { + const fnTag = `${this.className}#handleRequest()`; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + const reqTag = `${verbUpper} ${this.getPath()}`; + this.log.debug(reqTag); + + try { + const memoryUsage = this.process.memoryUsage(); + const createdAt = new Date(); + const body = { + success: true, + createdAt, + memoryUsage, + }; + res.json(body).status(StatusCodes.OK); + } catch (error) { + const { log } = this; + const errorMsg = `${fnTag} request handler fn crashed for: ${reqTag}`; + + const ctx: Readonly = { + errorMsg, + log, + error, + res, + }; + + await handleRestEndpointException(ctx); + } + } +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts index 79b3e6d4b0c..c71993819b0 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts @@ -3,8 +3,15 @@ import { IGetOpenApiSpecV1EndpointBaseOptions, } from "@hyperledger/cactus-core"; -import { Checks, LogLevelDesc } from "@hyperledger/cactus-common"; -import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; +import { + Checks, + IAsyncProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; import OAS from "../../json/openapi.json"; @@ -34,4 +41,13 @@ export class GetOpenApiSpecV1Endpoint const fnTag = `${this.className}#constructor()`; Checks.truthy(options, `${fnTag} arg options`); } + + public getAuthorizationOptionsProvider(): IAsyncProvider { + return { + get: async () => ({ + isProtected: true, + requiredRoles: this.opts.oasPath.get.security[0].bearerTokenAuth, + }), + }; + } } diff --git a/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts b/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts index 2babf8ff130..70bd10444d2 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts @@ -104,7 +104,11 @@ const createTestInfrastructure = async (opts: { const grpcHost = `${addressInfoGrpc.address}:${addressInfoGrpc.port}`; - const jwtPayload = { name: "Peter", location: "Albertirsa" }; + const jwtPayload = { + name: "Peter", + location: "London", + scope: "read:spec", + }; const validJwt = await new SignJWT(jwtPayload) .setProtectedHeader({ alg: "RS256" }) .setIssuer(expressJwtOptions.issuer) diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts index 2e0d8c7e571..555520c3d66 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts @@ -87,7 +87,11 @@ describe(testCase, () => { try { expect(expressJwtOptions).toBeTruthy(); - const jwtPayload = { name: "Peter", location: "London" }; + const jwtPayload = { + name: "Peter", + location: "London", + scope: "read:health", + }; const tokenGood = await new SignJWT(jwtPayload) .setProtectedHeader({ alg: "RS256", @@ -100,7 +104,6 @@ describe(testCase, () => { const startResponse = apiServer.start(); await expect(startResponse).not.toReject; expect(startResponse).toBeTruthy(); - const addressInfoApi = (await startResponse).addressInfoApi; const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http"; const { address, port } = addressInfoApi; diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts index 38b636d7c42..2fb27ed2fe6 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts @@ -84,7 +84,11 @@ describe("cmd-api-server:ApiServer", () => { const { address, port } = addressInfoApi; apiHost = `${protocol}://${address}:${port}`; - const jwtPayload = { name: "Peter", location: "Albertirsa" }; + const jwtPayload = { + name: "Peter", + location: "London", + scope: "read:health", + }; const validJwt = await new SignJWT(jwtPayload) .setProtectedHeader({ alg: "RS256" }) .setIssuer(expressJwtOptions.issuer) diff --git a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts index 5b44fa205a0..70ae5e0a8d8 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts @@ -101,7 +101,11 @@ describe("cmd-api-server:getOpenApiSpecV1Endpoint", () => { grpcHost = `${addressInfoGrpc.address}:${addressInfoGrpc.port}`; - const jwtPayload = { name: "Peter", location: "Albertirsa" }; + const jwtPayload = { + name: "Peter", + location: "London", + scope: "read:spec", + }; const validJwt = await new SignJWT(jwtPayload) .setProtectedHeader({ alg: "RS256" }) .setIssuer(expressJwtOptions.issuer) diff --git a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts new file mode 100644 index 00000000000..ec264c0b268 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-oauth2-scopes.test.ts @@ -0,0 +1,274 @@ +import { + ApiServer, + ApiServerApiClient, + ApiServerApiClientConfiguration, + AuthorizationProtocol, + ConfigService, + IAuthorizationConfig, +} from "../../../main/typescript/public-api"; +import { + IJoseFittingJwtParams, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Constants } from "@hyperledger/cactus-core-api"; +import type { AuthorizeOptions as SocketIoJwtOptions } from "@thream/socketio-jwt"; +import type { Params as ExpressJwtOptions } from "express-jwt"; +import "jest-extended"; +import { SignJWT, exportSPKI, generateKeyPair } from "jose"; +import path from "path"; +import { v4 as uuidv4 } from "uuid"; + +describe("cmd-api-server:getOpenApiSpecV1Endpoint", () => { + const logLevel: LogLevelDesc = "INFO"; + let apiServer: ApiServer; + let apiClient: ApiServerApiClient; + let jwtKeyPair: { publicKey: CryptoKey; privateKey: CryptoKey }; + let expressJwtOptions: ExpressJwtOptions & IJoseFittingJwtParams; + + afterAll(async () => await apiServer.shutdown()); + + beforeAll(async () => { + jwtKeyPair = await generateKeyPair("RS256", { modulusLength: 4096 }); + const jwtPublicKey = await exportSPKI(jwtKeyPair.publicKey); + + expressJwtOptions = { + algorithms: ["RS256"], + secret: jwtPublicKey, + audience: uuidv4(), + issuer: uuidv4(), + }; + + const socketIoJwtOptions: SocketIoJwtOptions = { + secret: jwtPublicKey, + algorithms: ["RS256"], + }; + expect(expressJwtOptions).toBeTruthy(); + + const authorizationConfig: IAuthorizationConfig = { + unprotectedEndpointExemptions: [], + expressJwtOptions, + socketIoJwtOptions, + socketIoPath: Constants.SocketIoConnectionPathV1, + }; + + const pluginsPath = path.join( + __dirname, + "../../../../../../", // walk back up to the project root + ".tmp/test/test-cmd-api-server/get-open-api-spec-v1-endpoint_test/", // the dir path from the root + uuidv4(), // then a random directory to ensure proper isolation + ); + const pluginManagerOptionsJson = JSON.stringify({ pluginsPath }); + + const pluginRegistry = new PluginRegistry({ logLevel }); + + const configService = new ConfigService(); + + const apiSrvOpts = await configService.newExampleConfig(); + apiSrvOpts.logLevel = logLevel; + apiSrvOpts.pluginManagerOptionsJson = pluginManagerOptionsJson; + apiSrvOpts.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN; + apiSrvOpts.authorizationConfigJson = authorizationConfig; + apiSrvOpts.configFile = ""; + apiSrvOpts.apiCorsDomainCsv = "*"; + apiSrvOpts.apiPort = 0; + apiSrvOpts.cockpitPort = 0; + apiSrvOpts.grpcPort = 0; + apiSrvOpts.crpcPort = 0; + apiSrvOpts.apiTlsEnabled = false; + apiSrvOpts.grpcMtlsEnabled = false; + apiSrvOpts.plugins = []; + + const config = await configService.newExampleConfigConvict(apiSrvOpts); + + apiServer = new ApiServer({ + config: config.getProperties(), + pluginRegistry, + }); + + apiServer.initPluginRegistry({ pluginRegistry }); + const startResponsePromise = apiServer.start(); + await expect(startResponsePromise).toResolve(); + const startResponse = await startResponsePromise; + expect(startResponse).toBeTruthy(); + + const { addressInfoApi } = await startResponsePromise; + const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http"; + const { address, port } = addressInfoApi; + const apiHost = `${protocol}://${address}:${port}`; + + const jwtPayload = { name: "Peter", location: "Albertirsa" }; + const validJwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(expressJwtOptions.issuer) + .setAudience(expressJwtOptions.audience) + .sign(jwtKeyPair.privateKey); + expect(validJwt).toBeTruthy(); + + const validBearerToken = `Bearer ${validJwt}`; + expect(validBearerToken).toBeTruthy(); + + apiClient = new ApiServerApiClient( + new ApiServerApiClientConfiguration({ + basePath: apiHost, + baseOptions: { headers: { Authorization: validBearerToken } }, + logLevel, + }), + ); + }); + + it("HTTP - allows request execution with a valid JWT Token", async () => { + const jwtPayload = { scope: "read:spec" }; + const validJwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(expressJwtOptions.issuer) + .setAudience(expressJwtOptions.audience) + .sign(jwtKeyPair.privateKey); + + const validBearerToken = `Bearer ${validJwt}`; + expect(validBearerToken).toBeTruthy(); + + const res3Promise = apiClient.getOpenApiSpecV1({ + headers: { Authorization: validBearerToken }, + }); + + await expect(res3Promise).resolves.toHaveProperty("data.openapi"); + const res3 = await res3Promise; + expect(res3.status).toEqual(200); + expect(res3.data).toBeTruthy(); + }); + + it("HTTP - rejects request with an valid JWT but incorrect scope", async () => { + const jwtPayload = { scope: "red:specs" }; + const validJwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(expressJwtOptions.issuer) + .setAudience(expressJwtOptions.audience) + .sign(jwtKeyPair.privateKey); + + const validBearerToken = `Bearer ${validJwt}`; + expect(validBearerToken).toBeTruthy(); + + await expect( + apiClient.getOpenApiSpecV1({ + headers: { Authorization: validBearerToken }, + }), + ).rejects.toMatchObject({ + response: { + status: 403, + statusText: expect.stringContaining("Forbidden"), + }, + }); + }); + + it("HTTP - rejects request with an invalid JWT", async () => { + const { privateKey: otherPrivateKey } = await generateKeyPair("RS256"); + const invalidJwt = await new SignJWT({ scope: "invalid:scope" }) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer("invalid-issuer") + .setAudience("invalid-audience") + .sign(otherPrivateKey); + + const invalidBearerToken = `Bearer ${invalidJwt}`; + expect(invalidBearerToken).toBeTruthy(); + + await expect( + apiClient.getOpenApiSpecV1({ + headers: { Authorization: invalidBearerToken }, + }), + ).rejects.toMatchObject({ + response: { + status: 401, + data: expect.stringContaining("Unauthorized"), + }, + }); + }); + + it("HTTP - allows health check execution with a valid JWT Token", async () => { + const jwtPayload = { scope: "read:health" }; + const validJwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(expressJwtOptions.issuer) + .setAudience(expressJwtOptions.audience) + .sign(jwtKeyPair.privateKey); + + const validBearerToken = `Bearer ${validJwt}`; + expect(validBearerToken).toBeTruthy(); + + const resPromise = apiClient.getHealthCheckV1({ + headers: { Authorization: validBearerToken }, + }); + + await expect(resPromise).resolves.toHaveProperty("data"); + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.data).toBeTruthy(); + }); + + it("HTTP - rejects health check execution with an invalid JWT", async () => { + const { privateKey: otherPrivateKey } = await generateKeyPair("RS256"); + const invalidJwt = await new SignJWT({ scope: "invalid:scope" }) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer("invalid-issuer") + .setAudience("invalid-audience") + .sign(otherPrivateKey); + + const invalidBearerToken = `Bearer ${invalidJwt}`; + expect(invalidBearerToken).toBeTruthy(); + + await expect( + apiClient.getHealthCheckV1({ + headers: { Authorization: invalidBearerToken }, + }), + ).rejects.toMatchObject({ + response: { + status: 401, + data: expect.stringContaining("Unauthorized"), + }, + }); + }); + + it("HTTP - allows Prometheus metrics execution with a valid JWT Token", async () => { + const jwtPayload = { scope: "read:metrics" }; + const validJwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(expressJwtOptions.issuer) + .setAudience(expressJwtOptions.audience) + .sign(jwtKeyPair.privateKey); + + const validBearerToken = `Bearer ${validJwt}`; + expect(validBearerToken).toBeTruthy(); + + const resPromise = apiClient.getPrometheusMetricsV1({ + headers: { Authorization: validBearerToken }, + }); + + await expect(resPromise).resolves.toHaveProperty("data"); + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.data).toBeTruthy(); + }); + + it("HTTP - rejects Prometheus metrics execution with an invalid JWT", async () => { + const { privateKey: otherPrivateKey } = await generateKeyPair("RS256"); + const invalidJwt = await new SignJWT({ scope: "invalid:scope" }) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer("invalid-issuer") + .setAudience("invalid-audience") + .sign(otherPrivateKey); + + const invalidBearerToken = `Bearer ${invalidJwt}`; + expect(invalidBearerToken).toBeTruthy(); + + await expect( + apiClient.getPrometheusMetricsV1({ + headers: { Authorization: invalidBearerToken }, + }), + ).rejects.toMatchObject({ + response: { + status: 401, + data: expect.stringContaining("Unauthorized"), + }, + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 590decc8637..3e367b4193e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9521,7 +9521,7 @@ __metadata: google-protobuf: "npm:3.21.4" grpc-tools: "npm:1.12.4" grpc_tools_node_protoc_ts: "npm:5.3.3" - http-status-codes: "npm:2.1.4" + http-status-codes: "npm:2.1.3" jose: "npm:4.15.5" json-stable-stringify: "npm:1.0.2" lmify: "npm:0.3.0" @@ -32848,6 +32848,13 @@ __metadata: languageName: node linkType: hard +"http-status-codes@npm:2.1.3": + version: 2.1.3 + resolution: "http-status-codes@npm:2.1.3" + checksum: 10/d5025903b41d88d35c1039e6fa4fe4061cce95119f58672d56ed8837bf91cc10c1b3dc47b73288398b55990e7a2c2147ae32feddc896b6863f020b44ef58ee6e + languageName: node + linkType: hard + "http-status-codes@npm:2.1.4": version: 2.1.4 resolution: "http-status-codes@npm:2.1.4"