diff --git a/cmd/provision/main.go b/cmd/provision/main.go index 6cc998f2e..e94c127dd 100644 --- a/cmd/provision/main.go +++ b/cmd/provision/main.go @@ -17,11 +17,15 @@ import ( mgsdk "github.com/absmach/magistrala/pkg/sdk" "github.com/absmach/magistrala/provision" httpapi "github.com/absmach/magistrala/provision/api" + "github.com/absmach/magistrala/provision/middleware" "github.com/absmach/supermq" "github.com/absmach/supermq/channels" "github.com/absmach/supermq/clients" smqlog "github.com/absmach/supermq/logger" + smqauthn "github.com/absmach/supermq/pkg/authn" + authnsvc "github.com/absmach/supermq/pkg/authn/authsvc" "github.com/absmach/supermq/pkg/errors" + "github.com/absmach/supermq/pkg/grpcclient" "github.com/absmach/supermq/pkg/server" httpserver "github.com/absmach/supermq/pkg/server/http" "github.com/absmach/supermq/pkg/uuid" @@ -30,8 +34,9 @@ import ( ) const ( - svcName = "provision" - contentType = "application/json" + svcName = "provision" + contentType = "application/json" + envPrefixAuth = "SMQ_AUTH_GRPC_" ) var ( @@ -65,6 +70,24 @@ func main() { } } + grpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + + return + } + authn, authnClient, err := authnsvc.NewAuthentication(ctx, grpcCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + + return + } + defer authnClient.Close() + logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure()) + am := smqauthn.NewAuthNMiddleware(authn) + if cfgFromFile, err := loadConfigFromFile(cfg.File); err != nil { logger.Warn(fmt.Sprintf("Continue with settings from env, failed to load from: %s: %s", cfg.File, err)) } else { @@ -76,9 +99,10 @@ func main() { SDKCfg := mgsdk.Config{ UsersURL: cfg.Server.UsersURL, + ChannelsURL: cfg.Server.ChannelsURL, ClientsURL: cfg.Server.ClientsURL, BootstrapURL: cfg.Server.MgBSURL, - CertsURL: cfg.Server.MgCertsURL, + CertsURL: cfg.Server.CertsURL, MsgContentType: contentType, TLSVerification: cfg.Server.TLS, } @@ -91,10 +115,10 @@ func main() { cSdk := csdk.NewSDK(csdkConf) svc := provision.New(cfg, mgSdk, cSdk, logger) - svc = httpapi.NewLoggingMiddleware(svc, logger) + svc = middleware.NewLogging(svc, logger) - httpServerConfig := server.Config{Host: "", Port: cfg.Server.HTTPPort, KeyFile: cfg.Server.ServerKey, CertFile: cfg.Server.ServerCert} - hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, logger, cfg.InstanceID), logger) + httpServerConfig := server.Config{Host: "", Port: cfg.Server.Port, KeyFile: cfg.Server.ServerKey, CertFile: cfg.Server.ServerCert} + hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, logger, cfg.InstanceID), logger) if cfg.SendTelemetry { chc := chclient.New(svcName, supermq.Version, logger, cancel) diff --git a/docker/.env b/docker/.env index ae06e33d7..248eec4ad 100644 --- a/docker/.env +++ b/docker/.env @@ -286,13 +286,14 @@ MG_PROVISION_HTTP_PORT=9016 MG_PROVISION_ENV_CLIENTS_TLS=false MG_PROVISION_SERVER_CERT= MG_PROVISION_SERVER_KEY= -MG_PROVISION_USERS_LOCATION=http://users:9002 -MG_PROVISION_CLIENTS_LOCATION=http://clients:9006 +MG_PROVISION_USERS_URL=http://users:9002 +MG_PROVISION_CHANNELS_URL=http://channels:9005 +MG_PROVISION_CLIENTS_URL=http://clients:9006 +MG_PROVISION_CERTS_URL=http://certs:9019 MG_PROVISION_USER= MG_PROVISION_USERNAME= MG_PROVISION_PASS= MG_PROVISION_API_KEY= -MG_PROVISION_CERTS_SVC_URL=http://certs:9019 MG_PROVISION_X509_PROVISIONING=false MG_PROVISION_BS_SVC_URL=http://bootstrap:9013 MG_PROVISION_BS_CONFIG_PROVISIONING=true diff --git a/docker/addons/bootstrap/docker-compose.yaml b/docker/addons/bootstrap/docker-compose.yaml index 62a8a09d7..e16e9c399 100644 --- a/docker/addons/bootstrap/docker-compose.yaml +++ b/docker/addons/bootstrap/docker-compose.yaml @@ -8,6 +8,7 @@ networks: magistrala-base-net: + driver: bridge volumes: magistrala-bootstrap-db-volume: diff --git a/docker/addons/provision/configs/config.toml b/docker/addons/provision/configs/config.toml index ec1ee38bb..d40a4d663 100644 --- a/docker/addons/provision/configs/config.toml +++ b/docker/addons/provision/configs/config.toml @@ -55,10 +55,10 @@ type = "plain" workers = 10 -[[things]] - name = "thing" +[[clients]] + name = "client" - [things.metadata] + [clients.metadata] external_id = "xxxxxx" [[channels]] diff --git a/docker/addons/provision/docker-compose.yaml b/docker/addons/provision/docker-compose.yaml index c187b9c53..34227ba1e 100644 --- a/docker/addons/provision/docker-compose.yaml +++ b/docker/addons/provision/docker-compose.yaml @@ -8,6 +8,7 @@ networks: magistrala-base-net: + driver: bridge services: provision: @@ -25,13 +26,14 @@ services: MG_PROVISION_ENV_CLIENTS_TLS: ${MG_PROVISION_ENV_CLIENTS_TLS} MG_PROVISION_SERVER_CERT: ${MG_PROVISION_SERVER_CERT} MG_PROVISION_SERVER_KEY: ${MG_PROVISION_SERVER_KEY} - MG_PROVISION_USERS_LOCATION: ${MG_PROVISION_USERS_LOCATION} - MG_PROVISION_THINGS_LOCATION: ${MG_PROVISION_THINGS_LOCATION} + MG_PROVISION_USERS_URL: ${MG_PROVISION_USERS_URL} + MG_PROVISION_CHANNELS_URL: ${MG_PROVISION_CHANNELS_URL} + MG_PROVISION_CLIENTS_URL: ${MG_PROVISION_CLIENTS_URL} MG_PROVISION_USER: ${MG_PROVISION_USER} MG_PROVISION_USERNAME: ${MG_PROVISION_USERNAME} MG_PROVISION_PASS: ${MG_PROVISION_PASS} MG_PROVISION_API_KEY: ${MG_PROVISION_API_KEY} - MG_PROVISION_CERTS_SVC_URL: ${MG_PROVISION_CERTS_SVC_URL} + MG_PROVISION_CERTS_URL: ${MG_PROVISION_CERTS_URL} MG_PROVISION_X509_PROVISIONING: ${MG_PROVISION_X509_PROVISIONING} MG_PROVISION_BS_SVC_URL: ${MG_PROVISION_BS_SVC_URL} MG_PROVISION_BS_CONFIG_PROVISIONING: ${MG_PROVISION_BS_CONFIG_PROVISIONING} @@ -40,6 +42,12 @@ services: MG_PROVISION_CERTS_HOURS_VALID: ${MG_PROVISION_CERTS_HOURS_VALID} SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY} MG_PROVISION_INSTANCE_ID: ${MG_PROVISION_INSTANCE_ID} + SMQ_AUTH_GRPC_URL: ${SMQ_AUTH_GRPC_URL} + SMQ_AUTH_GRPC_TIMEOUT: ${SMQ_AUTH_GRPC_TIMEOUT} + SMQ_AUTH_GRPC_CLIENT_CERT: ${SMQ_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} + SMQ_AUTH_GRPC_CLIENT_KEY: ${SMQ_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key} + SMQ_AUTH_GRPC_SERVER_CA_CERTS: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + SMQ_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER} volumes: - ./configs:/configs - ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key diff --git a/provision/README.md b/provision/README.md index 36878fa5f..238675ab7 100644 --- a/provision/README.md +++ b/provision/README.md @@ -1,52 +1,52 @@ # Provision service -Provision service provides an HTTP API to create initial SuperMQ resources for gateways or edge deployments. It can create clients and channels based on a configurable layout, optionally create bootstrap configurations, whitelist clients, and issue X.509 certificates for mTLS. +Provision service provides an HTTP API to create initial Magistrala resources for gateways or edge deployments. It can create clients and channels based on a configurable layout, optionally create bootstrap configurations, whitelist clients, and issue X.509 certificates for mTLS. -For gateways to communicate with [SuperMQ][supermq], configuration is required (MQTT host, client, channels, certificates). A gateway can fetch bootstrap configuration from the [Bootstrap][bootstrap] service using its `` and ``. The [Agent][agent] service is typically used on gateways to retrieve that configuration. +For gateways to communicate with [Magistrala][magistrala], configuration is required (MQTT host, client, channels, certificates). A gateway can fetch bootstrap configuration from the [Bootstrap][bootstrap] service using its `` and ``. The [Agent][agent] service is typically used on gateways to retrieve that configuration. -You can create bootstrap configuration directly via [Bootstrap][bootstrap] or through Provision. [SuperMQ UI][mgxui] uses the Bootstrap service; Provision is intended to automate gateway setups where one physical gateway may require multiple clients and channels (for example, [Agent][agent] and [Export][export]). This setup is defined as a **provision layout**. +You can create bootstrap configuration directly via [Bootstrap][bootstrap] or through Provision. [Magistrala UI][mgxui] uses the Bootstrap service; Provision is intended to automate gateway setups where one physical gateway may require multiple clients and channels (for example, [Agent][agent] and [Export][export]). This setup is defined as a **provision layout**. ## Configuration -The service is configured using environment variables and/or a TOML config file. Defaults below are from `provision/config.go`. Docker add-on examples are in `docker/addons/provision/docker-compose.yaml` and [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env). The binary reads `SMQ_PROVISION_*` variables; the add-on compose file uses `MG_PROVISION_*`, so ensure the container receives the expected names. +The service is configured using environment variables and/or a TOML config file. Defaults below are from `provision/config.go`. Docker add-on examples are in `docker/addons/provision/docker-compose.yaml` and [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env). The binary reads `MG_PROVISION_*` variables; the add-on compose file uses `MG_PROVISION_*`, so ensure the container receives the expected names. ### Core service | Variable | Description | Default | | --- | --- | --- | -| `SMQ_PROVISION_HTTP_PORT` | Provision service listening port | `9016` | -| `SMQ_PROVISION_LOG_LEVEL` | Service log level | `info` | -| `SMQ_PROVISION_ENV_CLIENTS_TLS` | SDK TLS verification | `false` | -| `SMQ_PROVISION_SERVER_CERT` | HTTPS server certificate | "" | -| `SMQ_PROVISION_SERVER_KEY` | HTTPS server key | "" | -| `SMQ_SEND_TELEMETRY` | Send telemetry to SuperMQ call-home server | `true` | -| `SMQ_MQTT_ADAPTER_INSTANCE_ID` | Instance ID used in health output | "" | +| `MG_PROVISION_HTTP_PORT` | Provision service listening port | `9016` | +| `MG_PROVISION_LOG_LEVEL` | Service log level | `info` | +| `MG_PROVISION_ENV_CLIENTS_TLS` | SDK TLS verification | `false` | +| `MG_PROVISION_SERVER_CERT` | HTTPS server certificate | "" | +| `MG_PROVISION_SERVER_KEY` | HTTPS server key | "" | +| `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` | +| `MG_MQTT_ADAPTER_INSTANCE_ID` | Instance ID used in health output | "" | -### SuperMQ endpoints and credentials +### Magistrala endpoints and credentials | Variable | Description | Default | | --- | --- | --- | -| `SMQ_PROVISION_USERS_LOCATION` | Users service URL | `http://localhost` | -| `SMQ_PROVISION_CLIENTS_LOCATION` | Clients service URL | `http://localhost` | -| `SMQ_PROVISION_CERTS_LOCATION` | Certs service URL (certs SDK) | `http://localhost` | -| `SMQ_PROVISION_BS_SVC_URL` | Bootstrap service URL | `http://localhost:9000` | -| `SMQ_PROVISION_CERTS_SVC_URL` | Certs service URL (Magistrala SDK) | `http://localhost:9019` | -| `SMQ_PROVISION_USERNAME` | SuperMQ username | `user` | -| `SMQ_PROVISION_PASS` | SuperMQ password | `test` | -| `SMQ_PROVISION_API_KEY` | SuperMQ authentication token | "" | -| `SMQ_PROVISION_EMAIL` | SuperMQ user email | `test@example.com` | -| `SMQ_PROVISION_DOMAIN_ID` | Default domain ID (unused by HTTP API) | "" | +| `MG_PROVISION_USERS_LOCATION` | Users service URL | `http://localhost` | +| `MG_PROVISION_CLIENTS_LOCATION` | Clients service URL | `http://localhost` | +| `MG_PROVISION_CERTS_LOCATION` | Certs service URL (certs SDK) | `http://localhost` | +| `MG_PROVISION_BS_SVC_URL` | Bootstrap service URL | `http://localhost:9000` | +| `MG_PROVISION_CERTS_SVC_URL` | Certs service URL (Magistrala SDK) | `http://localhost:9019` | +| `MG_PROVISION_USERNAME` | Magistrala username | `user` | +| `MG_PROVISION_PASS` | Magistrala password | `test` | +| `MG_PROVISION_API_KEY` | Magistrala authentication token | "" | +| `MG_PROVISION_EMAIL` | Magistrala user email | `test@example.com` | +| `MG_PROVISION_DOMAIN_ID` | Default domain ID (unused by HTTP API) | "" | ### Provisioning behavior | Variable | Description | Default | | --- | --- | --- | -| `SMQ_PROVISION_CONFIG_FILE` | Provision config file | `config.toml` | -| `SMQ_PROVISION_X509_PROVISIONING` | Issue client certificates during provisioning | `false` | -| `SMQ_PROVISION_BS_CONFIG_PROVISIONING` | Save client config in Bootstrap | `true` | -| `SMQ_PROVISION_BS_AUTO_WHITELIST` | Auto-whitelist client | `true` | -| `SMQ_PROVISION_BS_CONTENT` | Bootstrap config content (JSON string) | "" | -| `SMQ_PROVISION_CERTS_HOURS_VALID` | Client cert validity period | `2400h` | +| `MG_PROVISION_CONFIG_FILE` | Provision config file | `config.toml` | +| `MG_PROVISION_X509_PROVISIONING` | Issue client certificates during provisioning | `false` | +| `MG_PROVISION_BS_CONFIG_PROVISIONING` | Save client config in Bootstrap | `true` | +| `MG_PROVISION_BS_AUTO_WHITELIST` | Auto-whitelist client | `true` | +| `MG_PROVISION_BS_CONTENT` | Bootstrap config content (JSON string) | "" | +| `MG_PROVISION_CERTS_HOURS_VALID` | Client cert validity period | `2400h` | ## Features @@ -66,7 +66,7 @@ Notes: - At least one client must include `external_id` in metadata. This value is replaced with the `external_id` from the provisioning request and is used for bootstrap creation. - Channel metadata `type` is reserved for `control`, `data`, and `export` and is used to enrich gateway metadata. -- Bootstrap content can be provided via `bootstrap.content` in the TOML file or as JSON through `SMQ_PROVISION_BS_CONTENT`. +- Bootstrap content can be provided via `bootstrap.content` in the TOML file or as JSON through `MG_PROVISION_BS_CONTENT`. Example layout: @@ -98,11 +98,11 @@ Example layout: ## Authentication -Provision uses SuperMQ APIs and requires a valid token. There are three ways to provide it: +Provision uses Magistrala APIs and requires a valid token. There are three ways to provide it: - `Authorization: Bearer ` on each request. -- `SMQ_PROVISION_API_KEY` in env or TOML (used when no header token is provided). -- `SMQ_PROVISION_USERNAME` and `SMQ_PROVISION_PASS` in env or TOML (used to create an access token when no header token is provided). +- `MG_PROVISION_API_KEY` in env or TOML (used when no header token is provided). +- `MG_PROVISION_USERNAME` and `MG_PROVISION_PASS` in env or TOML (used to create an access token when no header token is provided). `POST /{domainID}/mapping` can create its own token using API key or username/password if no `Authorization` header is provided. The `Authorization` header takes precedence when present. `GET /{domainID}/mapping` always requires a bearer token. @@ -126,10 +126,10 @@ Standalone: ```bash make provision -SMQ_PROVISION_BS_SVC_URL=http://localhost:9013 \ -SMQ_PROVISION_CLIENTS_LOCATION=http://localhost:9006 \ -SMQ_PROVISION_USERS_LOCATION=http://localhost:9002 \ -SMQ_PROVISION_CONFIG_FILE=provision/configs/config.toml \ +MG_PROVISION_BS_SVC_URL=http://localhost:9013 \ +MG_PROVISION_CLIENTS_LOCATION=http://localhost:9006 \ +MG_PROVISION_USERS_LOCATION=http://localhost:9002 \ +MG_PROVISION_CONFIG_FILE=provision/configs/config.toml \ ./build/provision ``` @@ -154,7 +154,7 @@ The Provision service exposes the following endpoints: When credentials are available via env/config, you can omit the `Authorization` header. `Content-Type` must be exactly `application/json`. ```bash -curl -s -S -X POST http://localhost://mapping \ +curl -s -S -X POST http://localhost://mapping \ -H 'Content-Type: application/json' \ -d '{"name": "gateway-a", "external_id": "33:52:77:99:43", "external_key": "223334fw2"}' ``` @@ -162,7 +162,7 @@ curl -s -S -X POST http://localhost://mapping If you want to supply a token explicitly: ```bash -curl -s -S -X POST http://localhost://mapping \ +curl -s -S -X POST http://localhost://mapping \ -H "Authorization: Bearer " \ -H 'Content-Type: application/json' \ -d '{"name": "gateway-a", "external_id": "", "external_key": ""}' @@ -207,14 +207,14 @@ Response contains created clients, channels, and optional certificate data: ### Example: Read bootstrap mapping ```bash -curl -s -S -X GET http://localhost://mapping \ +curl -s -S -X GET http://localhost://mapping \ -H "Authorization: Bearer " \ -H 'Content-Type: application/json' ``` ## Certificates -When `SMQ_PROVISION_X509_PROVISIONING=true`, the provisioning flow issues certificates for each client and returns them in the response as `client_cert`, `client_key`, and `ca_cert`. The certificate TTL is controlled by `SMQ_PROVISION_CERTS_HOURS_VALID`. +When `MG_PROVISION_X509_PROVISIONING=true`, the provisioning flow issues certificates for each client and returns them in the response as `client_cert`, `client_key`, and `ca_cert`. The certificate TTL is controlled by `MG_PROVISION_CERTS_HOURS_VALID`. ## Testing @@ -225,8 +225,8 @@ go test ./provision/... For an in-depth explanation of our Provision Service, see the [official documentation][doc]. [doc]: https://docs.magistrala.absmach.eu/dev-guide/provision/ -[supermq]: https://github.com/absmach/supermq -[bootstrap]: https://github.com/absmach/supermq/tree/main/bootstrap +[magistrala]: https://github.com/absmach/magistrala +[bootstrap]: https://github.com/absmach/magistrala/tree/main/bootstrap [export]: https://github.com/absmach/export [agent]: https://github.com/absmach/agent -[mgxui]: https://github.com/absmach/supermq/ui +[mgxui]: https://github.com/absmach/magistrala/ui diff --git a/provision/api/endpoint.go b/provision/api/endpoint.go index c864b9fd7..724accd0d 100644 --- a/provision/api/endpoint.go +++ b/provision/api/endpoint.go @@ -8,18 +8,24 @@ import ( "github.com/absmach/magistrala/provision" apiutil "github.com/absmach/supermq/api/http/util" + "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" + svcerr "github.com/absmach/supermq/pkg/errors/service" "github.com/go-kit/kit/endpoint" ) func doProvision(svc provision.Service) endpoint.Endpoint { return func(ctx context.Context, request any) (any, error) { + session, ok := ctx.Value(authn.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } req := request.(provisionReq) if err := req.validate(); err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) } - res, err := svc.Provision(ctx, req.domainID, req.token, req.Name, req.ExternalID, req.ExternalKey) + res, err := svc.Provision(ctx, session.DomainID, req.token, req.Name, req.ExternalID, req.ExternalKey) if err != nil { return nil, err } @@ -39,16 +45,31 @@ func doProvision(svc provision.Service) endpoint.Endpoint { func getMapping(svc provision.Service) endpoint.Endpoint { return func(ctx context.Context, request any) (any, error) { - req := request.(mappingReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - res, err := svc.Mapping(ctx, req.token) - if err != nil { - return nil, err - } + res := svc.Mapping() return mappingRes{Data: res}, nil } } + +func issueCert(svc provision.Service) endpoint.Endpoint { + return func(ctx context.Context, request any) (any, error) { + session, ok := ctx.Value(authn.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + req := request.(certReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + cert, key, err := svc.Cert(ctx, session.DomainID, req.token, req.ClientID, req.TTL) + if err != nil { + return nil, err + } + + return certRes{ + Certificate: cert, + Key: key, + }, nil + } +} diff --git a/provision/api/endpoint_test.go b/provision/api/endpoint_test.go index 448d5517b..e93b7d386 100644 --- a/provision/api/endpoint_test.go +++ b/provision/api/endpoint_test.go @@ -16,7 +16,11 @@ import ( "github.com/absmach/magistrala/provision/api" mocks "github.com/absmach/magistrala/provision/mocks" apiutil "github.com/absmach/supermq/api/http/util" + "github.com/absmach/supermq/auth" smqlog "github.com/absmach/supermq/logger" + smqauthn "github.com/absmach/supermq/pkg/authn" + authnmocks "github.com/absmach/supermq/pkg/authn/mocks" + "github.com/absmach/supermq/pkg/errors" svcerr "github.com/absmach/supermq/pkg/errors/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -26,6 +30,13 @@ var ( validToken = "valid" validContenType = "application/json" validID = testsutil.GenerateUUID(&testing.T{}) + userID = testsutil.GenerateUUID(&testing.T{}) + domainID = testsutil.GenerateUUID(&testing.T{}) + validSession = smqauthn.Session{ + DomainUserID: auth.EncodeDomainUserID(domainID, userID), + UserID: userID, + DomainID: domainID, + } ) type testRequest struct { @@ -54,16 +65,18 @@ func (tr testRequest) make() (*http.Response, error) { return tr.client.Do(req) } -func newProvisionServer() (*httptest.Server, *mocks.Service) { +func newProvisionServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) { svc := new(mocks.Service) logger := smqlog.NewMock() - mux := api.MakeHandler(svc, logger, "test") - return httptest.NewServer(mux), svc + authn := new(authnmocks.Authentication) + am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true)) + mux := api.MakeHandler(svc, am, logger, "test") + return httptest.NewServer(mux), svc, authn } func TestProvision(t *testing.T) { - is, svc := newProvisionServer() + is, svc, authn := newProvisionServer() cases := []struct { desc string @@ -72,6 +85,8 @@ func TestProvision(t *testing.T) { data string contentType string status int + authnRes smqauthn.Session + authnErr error svcErr error }{ { @@ -81,6 +96,7 @@ func TestProvision(t *testing.T) { data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), status: http.StatusCreated, contentType: validContenType, + authnRes: validSession, svcErr: nil, }, { @@ -90,7 +106,7 @@ func TestProvision(t *testing.T) { data: fmt.Sprintf(`{"name": "test", "external_key": "%s"}`, validID), status: http.StatusBadRequest, contentType: validContenType, - svcErr: nil, + authnRes: validSession, }, { desc: "request with empty external key", @@ -99,6 +115,7 @@ func TestProvision(t *testing.T) { data: fmt.Sprintf(`{"name": "test", "external_id": "%s"}`, validID), status: http.StatusUnauthorized, contentType: validContenType, + authnRes: validSession, svcErr: nil, }, { @@ -106,8 +123,10 @@ func TestProvision(t *testing.T) { token: "", domainID: validID, data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), - status: http.StatusCreated, + status: http.StatusUnauthorized, contentType: validContenType, + authnRes: smqauthn.Session{}, + authnErr: errors.ErrAuthentication, svcErr: nil, }, { @@ -117,6 +136,7 @@ func TestProvision(t *testing.T) { data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), status: http.StatusUnsupportedMediaType, contentType: "text/plain", + authnRes: validSession, svcErr: nil, }, { @@ -126,6 +146,7 @@ func TestProvision(t *testing.T) { data: `data`, status: http.StatusBadRequest, contentType: validContenType, + authnRes: validSession, svcErr: nil, }, { @@ -135,12 +156,14 @@ func TestProvision(t *testing.T) { data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), status: http.StatusForbidden, contentType: validContenType, + authnRes: validSession, svcErr: svcerr.ErrAuthorization, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { + authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) repocall := svc.On("Provision", mock.Anything, validID, tc.token, "test", validID, validID).Return(provision.Result{}, tc.svcErr) req := testRequest{ client: is.Client(), @@ -154,13 +177,14 @@ func TestProvision(t *testing.T) { resp, err := req.make() assert.Nil(t, err, tc.desc) assert.Equal(t, tc.status, resp.StatusCode, tc.desc) + authCall.Unset() repocall.Unset() }) } } func TestMapping(t *testing.T) { - is, svc := newProvisionServer() + is, svc, authn := newProvisionServer() cases := []struct { desc string @@ -168,6 +192,8 @@ func TestMapping(t *testing.T) { domainID string contentType string status int + authnRes smqauthn.Session + authnErr error svcErr error }{ { @@ -177,6 +203,8 @@ func TestMapping(t *testing.T) { status: http.StatusOK, contentType: validContenType, svcErr: nil, + authnRes: validSession, + authnErr: nil, }, { desc: "empty token", @@ -185,28 +213,15 @@ func TestMapping(t *testing.T) { status: http.StatusUnauthorized, contentType: validContenType, svcErr: nil, - }, - { - desc: "invalid content type", - token: validToken, - domainID: validID, - status: http.StatusUnsupportedMediaType, - contentType: "text/plain", - svcErr: nil, - }, - { - desc: "service error", - token: validToken, - domainID: validID, - status: http.StatusForbidden, - contentType: validContenType, - svcErr: svcerr.ErrAuthorization, + authnRes: smqauthn.Session{}, + authnErr: errors.ErrAuthentication, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - repocall := svc.On("Mapping", mock.Anything, tc.token).Return(map[string]any{}, tc.svcErr) + authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) + repocall := svc.On("Mapping").Return(map[string]any{}, tc.svcErr) req := testRequest{ client: is.Client(), method: http.MethodGet, @@ -218,6 +233,96 @@ func TestMapping(t *testing.T) { resp, err := req.make() assert.Nil(t, err, tc.desc) assert.Equal(t, tc.status, resp.StatusCode, tc.desc) + authCall.Unset() + repocall.Unset() + }) + } +} + +func TestCert(t *testing.T) { + is, svc, authn := newProvisionServer() + + cases := []struct { + desc string + token string + domainID string + data string + contentType string + status int + authnRes smqauthn.Session + authnErr error + svcErr error + }{ + { + desc: "valid request", + token: validToken, + domainID: validID, + data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID), + status: http.StatusCreated, + contentType: validContenType, + authnRes: validSession, + svcErr: nil, + }, + { + desc: "empty token", + token: "", + domainID: validID, + data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID), + status: http.StatusUnauthorized, + contentType: validContenType, + authnRes: smqauthn.Session{}, + authnErr: errors.ErrAuthentication, + svcErr: nil, + }, + { + desc: "invalid content type", + token: validToken, + domainID: validID, + data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID), + status: http.StatusUnsupportedMediaType, + contentType: "text/plain", + authnRes: validSession, + svcErr: nil, + }, + { + desc: "invalid request", + token: validToken, + domainID: validID, + data: `data`, + status: http.StatusBadRequest, + contentType: validContenType, + authnRes: validSession, + svcErr: nil, + }, + { + desc: "service error", + token: validToken, + domainID: validID, + data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID), + status: http.StatusForbidden, + contentType: validContenType, + authnRes: validSession, + svcErr: svcerr.ErrAuthorization, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) + repocall := svc.On("Cert", mock.Anything, validID, tc.token, validID, "1h").Return("cert", "key", tc.svcErr) + req := testRequest{ + client: is.Client(), + method: http.MethodPost, + url: is.URL + fmt.Sprintf("/%s/cert", tc.domainID), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(tc.data), + } + + resp, err := req.make() + assert.Nil(t, err, tc.desc) + assert.Equal(t, tc.status, resp.StatusCode, tc.desc) + authCall.Unset() repocall.Unset() }) } diff --git a/provision/api/requests.go b/provision/api/requests.go index b8466c3a4..828b6e923 100644 --- a/provision/api/requests.go +++ b/provision/api/requests.go @@ -9,7 +9,6 @@ import ( type provisionReq struct { token string - domainID string Name string `json:"name"` ExternalID string `json:"external_id"` ExternalKey string `json:"external_key"` @@ -19,9 +18,6 @@ func (req provisionReq) validate() error { if req.ExternalID == "" { return apiutil.ErrMissingID } - if req.domainID == "" { - return apiutil.ErrMissingDomainID - } if req.ExternalKey == "" { return apiutil.ErrBearerKey @@ -34,17 +30,16 @@ func (req provisionReq) validate() error { return nil } -type mappingReq struct { +type certReq struct { token string - domainID string + ClientID string `json:"client_id"` + TTL string `json:"ttl,omitempty"` } -func (req mappingReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.domainID == "" { - return apiutil.ErrMissingDomainID +func (req certReq) validate() error { + if req.ClientID == "" { + return apiutil.ErrMissingID } + return nil } diff --git a/provision/api/requests_test.go b/provision/api/requests_test.go index 962dfe507..4b4ef830c 100644 --- a/provision/api/requests_test.go +++ b/provision/api/requests_test.go @@ -23,7 +23,6 @@ func TestProvisioReq(t *testing.T) { desc: "valid request", req: provisionReq{ token: "token", - domainID: testsutil.GenerateUUID(t), Name: "name", ExternalID: testsutil.GenerateUUID(t), ExternalKey: testsutil.GenerateUUID(t), @@ -34,29 +33,16 @@ func TestProvisioReq(t *testing.T) { desc: "empty external id", req: provisionReq{ token: "token", - domainID: testsutil.GenerateUUID(t), Name: "name", ExternalID: "", ExternalKey: testsutil.GenerateUUID(t), }, err: apiutil.ErrMissingID, }, - { - desc: "empty domain id", - req: provisionReq{ - token: "token", - domainID: "", - Name: "name", - ExternalID: testsutil.GenerateUUID(t), - ExternalKey: testsutil.GenerateUUID(t), - }, - err: apiutil.ErrMissingDomainID, - }, { desc: "empty external key", req: provisionReq{ token: "token", - domainID: testsutil.GenerateUUID(t), Name: "name", ExternalID: testsutil.GenerateUUID(t), ExternalKey: "", @@ -70,41 +56,3 @@ func TestProvisioReq(t *testing.T) { assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected `%v` got `%v`", tc.desc, tc.err, err)) } } - -func TestMappingReq(t *testing.T) { - cases := []struct { - desc string - req mappingReq - err error - }{ - { - desc: "valid request", - req: mappingReq{ - token: "token", - domainID: testsutil.GenerateUUID(t), - }, - err: nil, - }, - { - desc: "empty token", - req: mappingReq{ - token: "", - domainID: testsutil.GenerateUUID(t), - }, - err: apiutil.ErrBearerToken, - }, - { - desc: "empty domain id", - req: mappingReq{ - token: "token", - domainID: "", - }, - err: apiutil.ErrMissingDomainID, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected `%v` got `%v`", tc.desc, tc.err, err)) - } -} diff --git a/provision/api/responses.go b/provision/api/responses.go index d8c4e58cf..ed4669b42 100644 --- a/provision/api/responses.go +++ b/provision/api/responses.go @@ -50,6 +50,23 @@ func (res mappingRes) Empty() bool { return false } +type certRes struct { + Certificate string `json:"certificate"` + Key string `json:"key"` +} + +func (res certRes) Code() int { + return http.StatusCreated +} + +func (res certRes) Headers() map[string]string { + return map[string]string{} +} + +func (res certRes) Empty() bool { + return false +} + func (res mappingRes) MarshalJSON() ([]byte, error) { return json.Marshal(res.Data) } diff --git a/provision/api/transport.go b/provision/api/transport.go index dfbb16e07..71bf8e9bc 100644 --- a/provision/api/transport.go +++ b/provision/api/transport.go @@ -13,6 +13,7 @@ import ( "github.com/absmach/supermq" api "github.com/absmach/supermq/api/http" apiutil "github.com/absmach/supermq/api/http/util" + smqauthn "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" "github.com/go-chi/chi/v5" kithttp "github.com/go-kit/kit/transport/http" @@ -24,7 +25,7 @@ const ( ) // MakeHandler returns a HTTP handler for API endpoints. -func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string) http.Handler { +func MakeHandler(svc provision.Service, authn smqauthn.AuthNMiddleware, logger *slog.Logger, instanceID string) http.Handler { opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), } @@ -32,6 +33,7 @@ func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string) r := chi.NewRouter() r.Route("/{domainID}", func(r chi.Router) { + r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()) r.Route("/mapping", func(r chi.Router) { r.Post("/", kithttp.NewServer( doProvision(svc), @@ -46,6 +48,12 @@ func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string) opts..., ).ServeHTTP) }) + r.Post("/cert", kithttp.NewServer( + issueCert(svc), + decodeCertRequest, + api.EncodeResponse, + opts..., + ).ServeHTTP) }) r.Handle("/metrics", promhttp.Handler()) r.Get("/health", supermq.Health("provision", instanceID)) @@ -59,8 +67,7 @@ func decodeProvisionRequest(_ context.Context, r *http.Request) (any, error) { } req := provisionReq{ - token: apiutil.ExtractBearerToken(r), - domainID: chi.URLParam(r, "domainID"), + token: apiutil.ExtractBearerToken(r), } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) @@ -70,13 +77,19 @@ func decodeProvisionRequest(_ context.Context, r *http.Request) (any, error) { } func decodeMappingRequest(_ context.Context, r *http.Request) (any, error) { + return nil, nil +} + +func decodeCertRequest(_ context.Context, r *http.Request) (any, error) { if r.Header.Get("Content-Type") != contentType { return nil, apiutil.ErrUnsupportedContentType } - req := mappingReq{ - token: apiutil.ExtractBearerToken(r), - domainID: chi.URLParam(r, "domainID"), + req := certReq{ + token: apiutil.ExtractBearerToken(r), + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) } return req, nil diff --git a/provision/config.go b/provision/config.go index 48f28eb61..bbf27a116 100644 --- a/provision/config.go +++ b/provision/config.go @@ -17,22 +17,21 @@ var errFailedToReadConfig = errors.New("failed to read config file") // ServiceConf represents service config. type ServiceConf struct { - Port string `toml:"port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"` - LogLevel string `toml:"log_level" env:"SMQ_PROVISION_LOG_LEVEL" envDefault:"info"` - TLS bool `toml:"tls" env:"SMQ_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"` - ServerCert string `toml:"server_cert" env:"SMQ_PROVISION_SERVER_CERT" envDefault:""` - ServerKey string `toml:"server_key" env:"SMQ_PROVISION_SERVER_KEY" envDefault:""` - ClientsURL string `toml:"clients_url" env:"SMQ_PROVISION_CLIENTS_LOCATION" envDefault:"http://localhost"` - UsersURL string `toml:"users_url" env:"SMQ_PROVISION_USERS_LOCATION" envDefault:"http://localhost"` - CertsURL string `toml:"certs_url" env:"SMQ_PROVISION_CERTS_LOCATION" envDefault:"http://localhost"` - HTTPPort string `toml:"http_port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"` - MgEmail string `toml:"smq_email" env:"SMQ_PROVISION_EMAIL" envDefault:"test@example.com"` - MgUsername string `toml:"smq_username" env:"SMQ_PROVISION_USERNAME" envDefault:"user"` - MgPass string `toml:"smq_pass" env:"SMQ_PROVISION_PASS" envDefault:"test"` - MgDomainID string `toml:"smq_domain_id" env:"SMQ_PROVISION_DOMAIN_ID" envDefault:""` - MgAPIKey string `toml:"smq_api_key" env:"SMQ_PROVISION_API_KEY" envDefault:""` - MgBSURL string `toml:"smq_bs_url" env:"SMQ_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"` - MgCertsURL string `toml:"smq_certs_url" env:"SMQ_PROVISION_CERTS_SVC_URL" envDefault:"http://localhost:9019"` + Port string `toml:"port" env:"MG_PROVISION_HTTP_PORT" envDefault:"9016"` + LogLevel string `toml:"log_level" env:"MG_PROVISION_LOG_LEVEL" envDefault:"info"` + TLS bool `toml:"tls" env:"MG_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"` + ServerCert string `toml:"server_cert" env:"MG_PROVISION_SERVER_CERT" envDefault:""` + ServerKey string `toml:"server_key" env:"MG_PROVISION_SERVER_KEY" envDefault:""` + ClientsURL string `toml:"clients_url" env:"MG_PROVISION_CLIENTS_URL" envDefault:"http://localhost"` + ChannelsURL string `toml:"channels_url" env:"MG_PROVISION_CHANNELS_URL" envDefault:"http://localhost"` + UsersURL string `toml:"users_url" env:"MG_PROVISION_USERS_URL" envDefault:"http://localhost"` + CertsURL string `toml:"certs_url" env:"MG_PROVISION_CERTS_URL" envDefault:"http://localhost"` + MgEmail string `toml:"mg_email" env:"MG_PROVISION_EMAIL" envDefault:"test@example.com"` + MgUsername string `toml:"mg_username" env:"MG_PROVISION_USERNAME" envDefault:"user"` + MgPass string `toml:"mg_pass" env:"MG_PROVISION_PASS" envDefault:"test"` + MgDomainID string `toml:"mg_domain_id" env:"MG_PROVISION_DOMAIN_ID" envDefault:""` + MgAPIKey string `toml:"mg_api_key" env:"MG_PROVISION_API_KEY" envDefault:""` + MgBSURL string `toml:"mg_bs_url" env:"MG_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"` } // Bootstrap represetns the Bootstrap config. @@ -61,13 +60,13 @@ type Cert struct { // Config struct of Provision. type Config struct { - File string `toml:"file" env:"SMQ_PROVISION_CONFIG_FILE" envDefault:"config.toml"` + File string `toml:"file" env:"MG_PROVISION_CONFIG_FILE" envDefault:"config.toml"` Server ServiceConf `toml:"server" mapstructure:"server"` Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"` Clients []clients.Client `toml:"clients" mapstructure:"clients"` Channels []channels.Channel `toml:"channels" mapstructure:"channels"` Cert Cert `toml:"cert" mapstructure:"cert"` - BSContent string `env:"SMQ_PROVISION_BS_CONTENT" envDefault:""` + BSContent string `env:"MG_PROVISION_BS_CONTENT" envDefault:""` SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"` InstanceID string `env:"SMQ_MQTT_ADAPTER_INSTANCE_ID" envDefault:""` } diff --git a/provision/api/logging.go b/provision/middleware/logging.go similarity index 71% rename from provision/api/logging.go rename to provision/middleware/logging.go index db177a969..136aa99c1 100644 --- a/provision/api/logging.go +++ b/provision/middleware/logging.go @@ -1,9 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -//go:build !test - -package api +package middleware import ( "context" @@ -20,8 +18,8 @@ type loggingMiddleware struct { svc provision.Service } -// NewLoggingMiddleware adds logging facilities to the core service. -func NewLoggingMiddleware(svc provision.Service, logger *slog.Logger) provision.Service { +// NewLogging adds logging facilities to the core service. +func NewLogging(svc provision.Service, logger *slog.Logger) provision.Service { return &loggingMiddleware{logger, svc} } @@ -33,7 +31,7 @@ func (lm *loggingMiddleware) Provision(ctx context.Context, domainID, token, nam slog.String("external_id", externalID), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Provision failed", args...) return } @@ -51,8 +49,8 @@ func (lm *loggingMiddleware) Cert(ctx context.Context, domainID, token, clientID slog.String("ttl", duration), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Client certificate failed to create successfully", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Client certificate creation failed", args...) return } lm.logger.Info("Client certificate created successfully", args...) @@ -61,18 +59,13 @@ func (lm *loggingMiddleware) Cert(ctx context.Context, domainID, token, clientID return lm.svc.Cert(ctx, domainID, token, clientID, duration) } -func (lm *loggingMiddleware) Mapping(ctx context.Context, token string) (res map[string]any, err error) { +func (lm *loggingMiddleware) Mapping() (res map[string]any) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Mapping failed", args...) - return - } lm.logger.Info("Mapping completed successfully", args...) }(time.Now()) - return lm.svc.Mapping(ctx, token) + return lm.svc.Mapping() } diff --git a/provision/mocks/service.go b/provision/mocks/service.go index 802f254bc..0b6cde9f9 100644 --- a/provision/mocks/service.go +++ b/provision/mocks/service.go @@ -133,31 +133,22 @@ func (_c *Service_Cert_Call) RunAndReturn(run func(ctx context.Context, domainID } // Mapping provides a mock function for the type Service -func (_mock *Service) Mapping(ctx context.Context, token string) (map[string]any, error) { - ret := _mock.Called(ctx, token) +func (_mock *Service) Mapping() map[string]any { + ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Mapping") } var r0 map[string]any - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (map[string]any, error)); ok { - return returnFunc(ctx, token) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) map[string]any); ok { - r0 = returnFunc(ctx, token) + if returnFunc, ok := ret.Get(0).(func() map[string]any); ok { + r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(map[string]any) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = returnFunc(ctx, token) - } else { - r1 = ret.Error(1) - } - return r0, r1 + return r0 } // Service_Mapping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mapping' @@ -166,36 +157,23 @@ type Service_Mapping_Call struct { } // Mapping is a helper method to define mock.On call -// - ctx context.Context -// - token string -func (_e *Service_Expecter) Mapping(ctx interface{}, token interface{}) *Service_Mapping_Call { - return &Service_Mapping_Call{Call: _e.mock.On("Mapping", ctx, token)} +func (_e *Service_Expecter) Mapping() *Service_Mapping_Call { + return &Service_Mapping_Call{Call: _e.mock.On("Mapping")} } -func (_c *Service_Mapping_Call) Run(run func(ctx context.Context, token string)) *Service_Mapping_Call { +func (_c *Service_Mapping_Call) Run(run func()) *Service_Mapping_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) + run() }) return _c } -func (_c *Service_Mapping_Call) Return(stringToV map[string]any, err error) *Service_Mapping_Call { - _c.Call.Return(stringToV, err) +func (_c *Service_Mapping_Call) Return(stringToV map[string]any) *Service_Mapping_Call { + _c.Call.Return(stringToV) return _c } -func (_c *Service_Mapping_Call) RunAndReturn(run func(ctx context.Context, token string) (map[string]any, error)) *Service_Mapping_Call { +func (_c *Service_Mapping_Call) RunAndReturn(run func() map[string]any) *Service_Mapping_Call { _c.Call.Return(run) return _c } diff --git a/provision/service.go b/provision/service.go index 3e75b33b4..4b17f75f4 100644 --- a/provision/service.go +++ b/provision/service.go @@ -27,25 +27,22 @@ const ( ) var ( - ErrUnauthorized = errors.New("unauthorized access") - ErrFailedToCreateToken = errors.New("failed to create access token") - ErrEmptyClientsList = errors.New("clients list in configuration empty") - ErrClientUpdate = errors.New("failed to update client") - ErrEmptyChannelsList = errors.New("channels list in configuration is empty") - ErrFailedChannelCreation = errors.New("failed to create channel") - ErrFailedChannelRetrieval = errors.New("failed to retrieve channel") - ErrFailedClientCreation = errors.New("failed to create client") - ErrFailedClientRetrieval = errors.New("failed to retrieve client") - ErrMissingCredentials = errors.New("missing credentials") - ErrFailedBootstrapRetrieval = errors.New("failed to retrieve bootstrap") - ErrFailedCertCreation = errors.New("failed to create certificates") - ErrFailedCertView = errors.New("failed to view certificate") - ErrFailedBootstrap = errors.New("failed to create bootstrap config") - ErrFailedBootstrapValidate = errors.New("failed to validate bootstrap config creation") - ErrGatewayUpdate = errors.New("failed to updated gateway metadata") - - limit uint = 10 - offset uint = 0 + ErrUnauthorized = errors.NewAuthNError("unauthorized access") + ErrFailedToCreateToken = errors.NewAuthNError("failed to create access token") + ErrEmptyClientsList = errors.NewRequestError("clients list in configuration empty") + ErrClientUpdate = errors.NewRequestError("failed to update client") + ErrEmptyChannelsList = errors.NewRequestError("channels list in configuration is empty") + ErrFailedChannelCreation = errors.NewRequestError("failed to create channel") + ErrFailedChannelRetrieval = errors.NewRequestError("failed to retrieve channel") + ErrFailedClientCreation = errors.NewRequestError("failed to create client") + ErrFailedClientRetrieval = errors.NewRequestError("failed to retrieve client") + ErrMissingCredentials = errors.NewRequestError("missing credentials") + ErrFailedBootstrapRetrieval = errors.NewServiceError("failed to retrieve bootstrap") + ErrFailedCertCreation = errors.NewServiceError("failed to create certificates") + ErrFailedCertView = errors.NewServiceError("failed to view certificate") + ErrFailedBootstrap = errors.NewServiceError("failed to create bootstrap config") + ErrFailedBootstrapValidate = errors.NewServiceError("failed to validate bootstrap config creation") + ErrGatewayUpdate = errors.NewServiceError("failed to update gateway metadata") ) var _ Service = (*provisionService)(nil) @@ -63,7 +60,7 @@ type Service interface { // Mapping returns current configuration used for provision // useful for using in ui to create configuration that matches // one created with Provision method. - Mapping(ctx context.Context, token string) (map[string]any, error) + Mapping() map[string]any // Certs creates certificate for clients that communicate over mTLS // A duration string is a possibly signed sequence of decimal numbers, @@ -101,17 +98,8 @@ func New(cfg Config, mgsdk sdk.SDK, certsSdk csdk.SDK, logger *slog.Logger) Serv } // Mapping retrieves current configuration. -func (ps *provisionService) Mapping(ctx context.Context, token string) (map[string]any, error) { - pm := smqSDK.PageMetadata{ - Offset: uint64(offset), - Limit: uint64(limit), - } - - if _, err := ps.sdk.Users(ctx, pm, token); err != nil { - return map[string]any{}, errors.Wrap(ErrUnauthorized, err) - } - - return ps.conf.Bootstrap.Content, nil +func (ps *provisionService) Mapping() map[string]any { + return ps.conf.Bootstrap.Content } // Provision is provision method for creating setup according to @@ -119,7 +107,7 @@ func (ps *provisionService) Mapping(ctx context.Context, token string) (map[stri func (ps *provisionService) Provision(ctx context.Context, domainID, token, name, externalID, externalKey string) (res Result, err error) { var channels []smqSDK.Channel var clients []smqSDK.Client - defer ps.recover(ctx, &err, &clients, &channels, &domainID, &token) + defer ps.recover(ctx, &err, &clients, &channels, domainID, token) token, err = ps.createTokenIfEmpty(ctx, token) if err != nil { @@ -360,11 +348,11 @@ func clean(ctx context.Context, ps *provisionService, clients []smqSDK.Client, c } } -func (ps *provisionService) recover(ctx context.Context, e *error, ths *[]smqSDK.Client, chs *[]smqSDK.Channel, dm, tkn *string) { +func (ps *provisionService) recover(ctx context.Context, e *error, ths *[]smqSDK.Client, chs *[]smqSDK.Channel, domainID, token string) { if e == nil { return } - clients, channels, domainID, token, err := *ths, *chs, *dm, *tkn, *e + clients, channels, err := *ths, *chs, *e if errors.Contains(err, ErrFailedClientRetrieval) || errors.Contains(err, ErrFailedChannelCreation) { for _, c := range clients { diff --git a/provision/service_test.go b/provision/service_test.go index 52ca415c9..9f6283a0f 100644 --- a/provision/service_test.go +++ b/provision/service_test.go @@ -31,35 +31,22 @@ func TestMapping(t *testing.T) { cases := []struct { desc string - token string content map[string]any sdkerr error err error }{ { - desc: "valid token", - token: validToken, + desc: "valid request", content: validConfig.Bootstrap.Content, sdkerr: nil, err: nil, }, - { - desc: "invalid token", - token: "invalid", - content: map[string]any{}, - sdkerr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401), - err: provision.ErrUnauthorized, - }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { - pm := smqSDK.PageMetadata{Offset: uint64(0), Limit: uint64(10)} - repocall := mgsdk.On("Users", mock.Anything, pm, c.token).Return(smqSDK.UsersPage{}, c.sdkerr) - content, err := svc.Mapping(context.Background(), c.token) - assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected error %v, got %v", c.err, err)) + content := svc.Mapping() assert.Equal(t, c.content, content) - repocall.Unset() }) } }