MG-344 - Update Provision Service (#386)

* feat: update provison service

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

* refactor: remove duplicate env variables

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

* ci: make fetch_supermq

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

* docs(README.md): update README

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

---------

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
Felix Gateru
2026-02-28 19:55:22 +03:00
committed by GitHub
parent d652652b79
commit eb14615cf5
17 changed files with 356 additions and 278 deletions
+30 -6
View File
@@ -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)
+4 -3
View File
@@ -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
@@ -8,6 +8,7 @@
networks:
magistrala-base-net:
driver: bridge
volumes:
magistrala-bootstrap-db-volume:
+3 -3
View File
@@ -55,10 +55,10 @@
type = "plain"
workers = 10
[[things]]
name = "thing"
[[clients]]
name = "client"
[things.metadata]
[clients.metadata]
external_id = "xxxxxx"
[[channels]]
+11 -3
View File
@@ -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
+43 -43
View File
@@ -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 `<external_id>` and `<external_key>`. 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 `<external_id>` and `<external_key>`. 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 <token>` 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:<SMQ_PROVISION_HTTP_PORT>/<domainID>/mapping \
curl -s -S -X POST http://localhost:<MG_PROVISION_HTTP_PORT>/<domainID>/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:<SMQ_PROVISION_HTTP_PORT>/<domainID>/mapping
If you want to supply a token explicitly:
```bash
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/<domainID>/mapping \
curl -s -S -X POST http://localhost:<MG_PROVISION_HTTP_PORT>/<domainID>/mapping \
-H "Authorization: Bearer <token|api_key>" \
-H 'Content-Type: application/json' \
-d '{"name": "gateway-a", "external_id": "<external_id>", "external_key": "<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:<SMQ_PROVISION_HTTP_PORT>/<domainID>/mapping \
curl -s -S -X GET http://localhost:<MG_PROVISION_HTTP_PORT>/<domainID>/mapping \
-H "Authorization: Bearer <token|api_key>" \
-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
+31 -10
View File
@@ -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
}
}
+129 -24
View File
@@ -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()
})
}
+7 -12
View File
@@ -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
}
-52
View File
@@ -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))
}
}
+17
View File
@@ -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)
}
+19 -6
View File
@@ -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
+17 -18
View File
@@ -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:""`
}
@@ -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()
}
+12 -34
View File
@@ -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
}
+22 -34
View File
@@ -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 {
+2 -15
View File
@@ -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()
})
}
}