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" mgsdk "github.com/absmach/magistrala/pkg/sdk"
"github.com/absmach/magistrala/provision" "github.com/absmach/magistrala/provision"
httpapi "github.com/absmach/magistrala/provision/api" httpapi "github.com/absmach/magistrala/provision/api"
"github.com/absmach/magistrala/provision/middleware"
"github.com/absmach/supermq" "github.com/absmach/supermq"
"github.com/absmach/supermq/channels" "github.com/absmach/supermq/channels"
"github.com/absmach/supermq/clients" "github.com/absmach/supermq/clients"
smqlog "github.com/absmach/supermq/logger" 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/errors"
"github.com/absmach/supermq/pkg/grpcclient"
"github.com/absmach/supermq/pkg/server" "github.com/absmach/supermq/pkg/server"
httpserver "github.com/absmach/supermq/pkg/server/http" httpserver "github.com/absmach/supermq/pkg/server/http"
"github.com/absmach/supermq/pkg/uuid" "github.com/absmach/supermq/pkg/uuid"
@@ -30,8 +34,9 @@ import (
) )
const ( const (
svcName = "provision" svcName = "provision"
contentType = "application/json" contentType = "application/json"
envPrefixAuth = "SMQ_AUTH_GRPC_"
) )
var ( 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 { 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)) logger.Warn(fmt.Sprintf("Continue with settings from env, failed to load from: %s: %s", cfg.File, err))
} else { } else {
@@ -76,9 +99,10 @@ func main() {
SDKCfg := mgsdk.Config{ SDKCfg := mgsdk.Config{
UsersURL: cfg.Server.UsersURL, UsersURL: cfg.Server.UsersURL,
ChannelsURL: cfg.Server.ChannelsURL,
ClientsURL: cfg.Server.ClientsURL, ClientsURL: cfg.Server.ClientsURL,
BootstrapURL: cfg.Server.MgBSURL, BootstrapURL: cfg.Server.MgBSURL,
CertsURL: cfg.Server.MgCertsURL, CertsURL: cfg.Server.CertsURL,
MsgContentType: contentType, MsgContentType: contentType,
TLSVerification: cfg.Server.TLS, TLSVerification: cfg.Server.TLS,
} }
@@ -91,10 +115,10 @@ func main() {
cSdk := csdk.NewSDK(csdkConf) cSdk := csdk.NewSDK(csdkConf)
svc := provision.New(cfg, mgSdk, cSdk, logger) 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} 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, logger, cfg.InstanceID), logger) hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, logger, cfg.InstanceID), logger)
if cfg.SendTelemetry { if cfg.SendTelemetry {
chc := chclient.New(svcName, supermq.Version, logger, cancel) 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_ENV_CLIENTS_TLS=false
MG_PROVISION_SERVER_CERT= MG_PROVISION_SERVER_CERT=
MG_PROVISION_SERVER_KEY= MG_PROVISION_SERVER_KEY=
MG_PROVISION_USERS_LOCATION=http://users:9002 MG_PROVISION_USERS_URL=http://users:9002
MG_PROVISION_CLIENTS_LOCATION=http://clients:9006 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_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=http://certs:9019
MG_PROVISION_X509_PROVISIONING=false MG_PROVISION_X509_PROVISIONING=false
MG_PROVISION_BS_SVC_URL=http://bootstrap:9013 MG_PROVISION_BS_SVC_URL=http://bootstrap:9013
MG_PROVISION_BS_CONFIG_PROVISIONING=true MG_PROVISION_BS_CONFIG_PROVISIONING=true
@@ -8,6 +8,7 @@
networks: networks:
magistrala-base-net: magistrala-base-net:
driver: bridge
volumes: volumes:
magistrala-bootstrap-db-volume: magistrala-bootstrap-db-volume:
+3 -3
View File
@@ -55,10 +55,10 @@
type = "plain" type = "plain"
workers = 10 workers = 10
[[things]] [[clients]]
name = "thing" name = "client"
[things.metadata] [clients.metadata]
external_id = "xxxxxx" external_id = "xxxxxx"
[[channels]] [[channels]]
+11 -3
View File
@@ -8,6 +8,7 @@
networks: networks:
magistrala-base-net: magistrala-base-net:
driver: bridge
services: services:
provision: provision:
@@ -25,13 +26,14 @@ services:
MG_PROVISION_ENV_CLIENTS_TLS: ${MG_PROVISION_ENV_CLIENTS_TLS} MG_PROVISION_ENV_CLIENTS_TLS: ${MG_PROVISION_ENV_CLIENTS_TLS}
MG_PROVISION_SERVER_CERT: ${MG_PROVISION_SERVER_CERT} MG_PROVISION_SERVER_CERT: ${MG_PROVISION_SERVER_CERT}
MG_PROVISION_SERVER_KEY: ${MG_PROVISION_SERVER_KEY} MG_PROVISION_SERVER_KEY: ${MG_PROVISION_SERVER_KEY}
MG_PROVISION_USERS_LOCATION: ${MG_PROVISION_USERS_LOCATION} MG_PROVISION_USERS_URL: ${MG_PROVISION_USERS_URL}
MG_PROVISION_THINGS_LOCATION: ${MG_PROVISION_THINGS_LOCATION} MG_PROVISION_CHANNELS_URL: ${MG_PROVISION_CHANNELS_URL}
MG_PROVISION_CLIENTS_URL: ${MG_PROVISION_CLIENTS_URL}
MG_PROVISION_USER: ${MG_PROVISION_USER} MG_PROVISION_USER: ${MG_PROVISION_USER}
MG_PROVISION_USERNAME: ${MG_PROVISION_USERNAME} MG_PROVISION_USERNAME: ${MG_PROVISION_USERNAME}
MG_PROVISION_PASS: ${MG_PROVISION_PASS} MG_PROVISION_PASS: ${MG_PROVISION_PASS}
MG_PROVISION_API_KEY: ${MG_PROVISION_API_KEY} 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_X509_PROVISIONING: ${MG_PROVISION_X509_PROVISIONING}
MG_PROVISION_BS_SVC_URL: ${MG_PROVISION_BS_SVC_URL} MG_PROVISION_BS_SVC_URL: ${MG_PROVISION_BS_SVC_URL}
MG_PROVISION_BS_CONFIG_PROVISIONING: ${MG_PROVISION_BS_CONFIG_PROVISIONING} 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} MG_PROVISION_CERTS_HOURS_VALID: ${MG_PROVISION_CERTS_HOURS_VALID}
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY} SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
MG_PROVISION_INSTANCE_ID: ${MG_PROVISION_INSTANCE_ID} 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: volumes:
- ./configs:/configs - ./configs:/configs
- ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key - ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key
+43 -43
View File
@@ -1,52 +1,52 @@
# Provision service # 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 ## 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 ### Core service
| Variable | Description | Default | | Variable | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `SMQ_PROVISION_HTTP_PORT` | Provision service listening port | `9016` | | `MG_PROVISION_HTTP_PORT` | Provision service listening port | `9016` |
| `SMQ_PROVISION_LOG_LEVEL` | Service log level | `info` | | `MG_PROVISION_LOG_LEVEL` | Service log level | `info` |
| `SMQ_PROVISION_ENV_CLIENTS_TLS` | SDK TLS verification | `false` | | `MG_PROVISION_ENV_CLIENTS_TLS` | SDK TLS verification | `false` |
| `SMQ_PROVISION_SERVER_CERT` | HTTPS server certificate | "" | | `MG_PROVISION_SERVER_CERT` | HTTPS server certificate | "" |
| `SMQ_PROVISION_SERVER_KEY` | HTTPS server key | "" | | `MG_PROVISION_SERVER_KEY` | HTTPS server key | "" |
| `SMQ_SEND_TELEMETRY` | Send telemetry to SuperMQ call-home server | `true` | | `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` |
| `SMQ_MQTT_ADAPTER_INSTANCE_ID` | Instance ID used in health output | "" | | `MG_MQTT_ADAPTER_INSTANCE_ID` | Instance ID used in health output | "" |
### SuperMQ endpoints and credentials ### Magistrala endpoints and credentials
| Variable | Description | Default | | Variable | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `SMQ_PROVISION_USERS_LOCATION` | Users service URL | `http://localhost` | | `MG_PROVISION_USERS_LOCATION` | Users service URL | `http://localhost` |
| `SMQ_PROVISION_CLIENTS_LOCATION` | Clients service URL | `http://localhost` | | `MG_PROVISION_CLIENTS_LOCATION` | Clients service URL | `http://localhost` |
| `SMQ_PROVISION_CERTS_LOCATION` | Certs service URL (certs SDK) | `http://localhost` | | `MG_PROVISION_CERTS_LOCATION` | Certs service URL (certs SDK) | `http://localhost` |
| `SMQ_PROVISION_BS_SVC_URL` | Bootstrap service URL | `http://localhost:9000` | | `MG_PROVISION_BS_SVC_URL` | Bootstrap service URL | `http://localhost:9000` |
| `SMQ_PROVISION_CERTS_SVC_URL` | Certs service URL (Magistrala SDK) | `http://localhost:9019` | | `MG_PROVISION_CERTS_SVC_URL` | Certs service URL (Magistrala SDK) | `http://localhost:9019` |
| `SMQ_PROVISION_USERNAME` | SuperMQ username | `user` | | `MG_PROVISION_USERNAME` | Magistrala username | `user` |
| `SMQ_PROVISION_PASS` | SuperMQ password | `test` | | `MG_PROVISION_PASS` | Magistrala password | `test` |
| `SMQ_PROVISION_API_KEY` | SuperMQ authentication token | "" | | `MG_PROVISION_API_KEY` | Magistrala authentication token | "" |
| `SMQ_PROVISION_EMAIL` | SuperMQ user email | `test@example.com` | | `MG_PROVISION_EMAIL` | Magistrala user email | `test@example.com` |
| `SMQ_PROVISION_DOMAIN_ID` | Default domain ID (unused by HTTP API) | "" | | `MG_PROVISION_DOMAIN_ID` | Default domain ID (unused by HTTP API) | "" |
### Provisioning behavior ### Provisioning behavior
| Variable | Description | Default | | Variable | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `SMQ_PROVISION_CONFIG_FILE` | Provision config file | `config.toml` | | `MG_PROVISION_CONFIG_FILE` | Provision config file | `config.toml` |
| `SMQ_PROVISION_X509_PROVISIONING` | Issue client certificates during provisioning | `false` | | `MG_PROVISION_X509_PROVISIONING` | Issue client certificates during provisioning | `false` |
| `SMQ_PROVISION_BS_CONFIG_PROVISIONING` | Save client config in Bootstrap | `true` | | `MG_PROVISION_BS_CONFIG_PROVISIONING` | Save client config in Bootstrap | `true` |
| `SMQ_PROVISION_BS_AUTO_WHITELIST` | Auto-whitelist client | `true` | | `MG_PROVISION_BS_AUTO_WHITELIST` | Auto-whitelist client | `true` |
| `SMQ_PROVISION_BS_CONTENT` | Bootstrap config content (JSON string) | "" | | `MG_PROVISION_BS_CONTENT` | Bootstrap config content (JSON string) | "" |
| `SMQ_PROVISION_CERTS_HOURS_VALID` | Client cert validity period | `2400h` | | `MG_PROVISION_CERTS_HOURS_VALID` | Client cert validity period | `2400h` |
## Features ## 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. - 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. - 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: Example layout:
@@ -98,11 +98,11 @@ Example layout:
## Authentication ## 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. - `Authorization: Bearer <token>` on each request.
- `SMQ_PROVISION_API_KEY` in env or TOML (used when no header token is provided). - `MG_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_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. `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 ```bash
make provision make provision
SMQ_PROVISION_BS_SVC_URL=http://localhost:9013 \ MG_PROVISION_BS_SVC_URL=http://localhost:9013 \
SMQ_PROVISION_CLIENTS_LOCATION=http://localhost:9006 \ MG_PROVISION_CLIENTS_LOCATION=http://localhost:9006 \
SMQ_PROVISION_USERS_LOCATION=http://localhost:9002 \ MG_PROVISION_USERS_LOCATION=http://localhost:9002 \
SMQ_PROVISION_CONFIG_FILE=provision/configs/config.toml \ MG_PROVISION_CONFIG_FILE=provision/configs/config.toml \
./build/provision ./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`. When credentials are available via env/config, you can omit the `Authorization` header. `Content-Type` must be exactly `application/json`.
```bash ```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' \ -H 'Content-Type: application/json' \
-d '{"name": "gateway-a", "external_id": "33:52:77:99:43", "external_key": "223334fw2"}' -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: If you want to supply a token explicitly:
```bash ```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 "Authorization: Bearer <token|api_key>" \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"name": "gateway-a", "external_id": "<external_id>", "external_key": "<external_key>"}' -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 ### Example: Read bootstrap mapping
```bash ```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 "Authorization: Bearer <token|api_key>" \
-H 'Content-Type: application/json' -H 'Content-Type: application/json'
``` ```
## Certificates ## 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 ## Testing
@@ -225,8 +225,8 @@ go test ./provision/...
For an in-depth explanation of our Provision Service, see the [official documentation][doc]. For an in-depth explanation of our Provision Service, see the [official documentation][doc].
[doc]: https://docs.magistrala.absmach.eu/dev-guide/provision/ [doc]: https://docs.magistrala.absmach.eu/dev-guide/provision/
[supermq]: https://github.com/absmach/supermq [magistrala]: https://github.com/absmach/magistrala
[bootstrap]: https://github.com/absmach/supermq/tree/main/bootstrap [bootstrap]: https://github.com/absmach/magistrala/tree/main/bootstrap
[export]: https://github.com/absmach/export [export]: https://github.com/absmach/export
[agent]: https://github.com/absmach/agent [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" "github.com/absmach/magistrala/provision"
apiutil "github.com/absmach/supermq/api/http/util" apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/go-kit/kit/endpoint" "github.com/go-kit/kit/endpoint"
) )
func doProvision(svc provision.Service) endpoint.Endpoint { func doProvision(svc provision.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) { 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) req := request.(provisionReq)
if err := req.validate(); err != nil { if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -39,16 +45,31 @@ func doProvision(svc provision.Service) endpoint.Endpoint {
func getMapping(svc provision.Service) endpoint.Endpoint { func getMapping(svc provision.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) { return func(ctx context.Context, request any) (any, error) {
req := request.(mappingReq) res := svc.Mapping()
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
}
return mappingRes{Data: res}, nil 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" "github.com/absmach/magistrala/provision/api"
mocks "github.com/absmach/magistrala/provision/mocks" mocks "github.com/absmach/magistrala/provision/mocks"
apiutil "github.com/absmach/supermq/api/http/util" apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/auth"
smqlog "github.com/absmach/supermq/logger" 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" svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@@ -26,6 +30,13 @@ var (
validToken = "valid" validToken = "valid"
validContenType = "application/json" validContenType = "application/json"
validID = testsutil.GenerateUUID(&testing.T{}) 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 { type testRequest struct {
@@ -54,16 +65,18 @@ func (tr testRequest) make() (*http.Response, error) {
return tr.client.Do(req) return tr.client.Do(req)
} }
func newProvisionServer() (*httptest.Server, *mocks.Service) { func newProvisionServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) {
svc := new(mocks.Service) svc := new(mocks.Service)
logger := smqlog.NewMock() logger := smqlog.NewMock()
mux := api.MakeHandler(svc, logger, "test") authn := new(authnmocks.Authentication)
return httptest.NewServer(mux), svc 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) { func TestProvision(t *testing.T) {
is, svc := newProvisionServer() is, svc, authn := newProvisionServer()
cases := []struct { cases := []struct {
desc string desc string
@@ -72,6 +85,8 @@ func TestProvision(t *testing.T) {
data string data string
contentType string contentType string
status int status int
authnRes smqauthn.Session
authnErr error
svcErr 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), data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusCreated, status: http.StatusCreated,
contentType: validContenType, contentType: validContenType,
authnRes: validSession,
svcErr: nil, svcErr: nil,
}, },
{ {
@@ -90,7 +106,7 @@ func TestProvision(t *testing.T) {
data: fmt.Sprintf(`{"name": "test", "external_key": "%s"}`, validID), data: fmt.Sprintf(`{"name": "test", "external_key": "%s"}`, validID),
status: http.StatusBadRequest, status: http.StatusBadRequest,
contentType: validContenType, contentType: validContenType,
svcErr: nil, authnRes: validSession,
}, },
{ {
desc: "request with empty external key", desc: "request with empty external key",
@@ -99,6 +115,7 @@ func TestProvision(t *testing.T) {
data: fmt.Sprintf(`{"name": "test", "external_id": "%s"}`, validID), data: fmt.Sprintf(`{"name": "test", "external_id": "%s"}`, validID),
status: http.StatusUnauthorized, status: http.StatusUnauthorized,
contentType: validContenType, contentType: validContenType,
authnRes: validSession,
svcErr: nil, svcErr: nil,
}, },
{ {
@@ -106,8 +123,10 @@ func TestProvision(t *testing.T) {
token: "", token: "",
domainID: validID, domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusCreated, status: http.StatusUnauthorized,
contentType: validContenType, contentType: validContenType,
authnRes: smqauthn.Session{},
authnErr: errors.ErrAuthentication,
svcErr: nil, svcErr: nil,
}, },
{ {
@@ -117,6 +136,7 @@ func TestProvision(t *testing.T) {
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusUnsupportedMediaType, status: http.StatusUnsupportedMediaType,
contentType: "text/plain", contentType: "text/plain",
authnRes: validSession,
svcErr: nil, svcErr: nil,
}, },
{ {
@@ -126,6 +146,7 @@ func TestProvision(t *testing.T) {
data: `data`, data: `data`,
status: http.StatusBadRequest, status: http.StatusBadRequest,
contentType: validContenType, contentType: validContenType,
authnRes: validSession,
svcErr: nil, svcErr: nil,
}, },
{ {
@@ -135,12 +156,14 @@ func TestProvision(t *testing.T) {
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID), data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusForbidden, status: http.StatusForbidden,
contentType: validContenType, contentType: validContenType,
authnRes: validSession,
svcErr: svcerr.ErrAuthorization, svcErr: svcerr.ErrAuthorization,
}, },
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) { 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) repocall := svc.On("Provision", mock.Anything, validID, tc.token, "test", validID, validID).Return(provision.Result{}, tc.svcErr)
req := testRequest{ req := testRequest{
client: is.Client(), client: is.Client(),
@@ -154,13 +177,14 @@ func TestProvision(t *testing.T) {
resp, err := req.make() resp, err := req.make()
assert.Nil(t, err, tc.desc) assert.Nil(t, err, tc.desc)
assert.Equal(t, tc.status, resp.StatusCode, tc.desc) assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
authCall.Unset()
repocall.Unset() repocall.Unset()
}) })
} }
} }
func TestMapping(t *testing.T) { func TestMapping(t *testing.T) {
is, svc := newProvisionServer() is, svc, authn := newProvisionServer()
cases := []struct { cases := []struct {
desc string desc string
@@ -168,6 +192,8 @@ func TestMapping(t *testing.T) {
domainID string domainID string
contentType string contentType string
status int status int
authnRes smqauthn.Session
authnErr error
svcErr error svcErr error
}{ }{
{ {
@@ -177,6 +203,8 @@ func TestMapping(t *testing.T) {
status: http.StatusOK, status: http.StatusOK,
contentType: validContenType, contentType: validContenType,
svcErr: nil, svcErr: nil,
authnRes: validSession,
authnErr: nil,
}, },
{ {
desc: "empty token", desc: "empty token",
@@ -185,28 +213,15 @@ func TestMapping(t *testing.T) {
status: http.StatusUnauthorized, status: http.StatusUnauthorized,
contentType: validContenType, contentType: validContenType,
svcErr: nil, svcErr: nil,
}, authnRes: smqauthn.Session{},
{ authnErr: errors.ErrAuthentication,
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,
}, },
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) { 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{ req := testRequest{
client: is.Client(), client: is.Client(),
method: http.MethodGet, method: http.MethodGet,
@@ -218,6 +233,96 @@ func TestMapping(t *testing.T) {
resp, err := req.make() resp, err := req.make()
assert.Nil(t, err, tc.desc) assert.Nil(t, err, tc.desc)
assert.Equal(t, tc.status, resp.StatusCode, 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() repocall.Unset()
}) })
} }
+7 -12
View File
@@ -9,7 +9,6 @@ import (
type provisionReq struct { type provisionReq struct {
token string token string
domainID string
Name string `json:"name"` Name string `json:"name"`
ExternalID string `json:"external_id"` ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"` ExternalKey string `json:"external_key"`
@@ -19,9 +18,6 @@ func (req provisionReq) validate() error {
if req.ExternalID == "" { if req.ExternalID == "" {
return apiutil.ErrMissingID return apiutil.ErrMissingID
} }
if req.domainID == "" {
return apiutil.ErrMissingDomainID
}
if req.ExternalKey == "" { if req.ExternalKey == "" {
return apiutil.ErrBearerKey return apiutil.ErrBearerKey
@@ -34,17 +30,16 @@ func (req provisionReq) validate() error {
return nil return nil
} }
type mappingReq struct { type certReq struct {
token string token string
domainID string ClientID string `json:"client_id"`
TTL string `json:"ttl,omitempty"`
} }
func (req mappingReq) validate() error { func (req certReq) validate() error {
if req.token == "" { if req.ClientID == "" {
return apiutil.ErrBearerToken return apiutil.ErrMissingID
}
if req.domainID == "" {
return apiutil.ErrMissingDomainID
} }
return nil return nil
} }
-52
View File
@@ -23,7 +23,6 @@ func TestProvisioReq(t *testing.T) {
desc: "valid request", desc: "valid request",
req: provisionReq{ req: provisionReq{
token: "token", token: "token",
domainID: testsutil.GenerateUUID(t),
Name: "name", Name: "name",
ExternalID: testsutil.GenerateUUID(t), ExternalID: testsutil.GenerateUUID(t),
ExternalKey: testsutil.GenerateUUID(t), ExternalKey: testsutil.GenerateUUID(t),
@@ -34,29 +33,16 @@ func TestProvisioReq(t *testing.T) {
desc: "empty external id", desc: "empty external id",
req: provisionReq{ req: provisionReq{
token: "token", token: "token",
domainID: testsutil.GenerateUUID(t),
Name: "name", Name: "name",
ExternalID: "", ExternalID: "",
ExternalKey: testsutil.GenerateUUID(t), ExternalKey: testsutil.GenerateUUID(t),
}, },
err: apiutil.ErrMissingID, 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", desc: "empty external key",
req: provisionReq{ req: provisionReq{
token: "token", token: "token",
domainID: testsutil.GenerateUUID(t),
Name: "name", Name: "name",
ExternalID: testsutil.GenerateUUID(t), ExternalID: testsutil.GenerateUUID(t),
ExternalKey: "", 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)) 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 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) { func (res mappingRes) MarshalJSON() ([]byte, error) {
return json.Marshal(res.Data) return json.Marshal(res.Data)
} }
+19 -6
View File
@@ -13,6 +13,7 @@ import (
"github.com/absmach/supermq" "github.com/absmach/supermq"
api "github.com/absmach/supermq/api/http" api "github.com/absmach/supermq/api/http"
apiutil "github.com/absmach/supermq/api/http/util" apiutil "github.com/absmach/supermq/api/http/util"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/pkg/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http" kithttp "github.com/go-kit/kit/transport/http"
@@ -24,7 +25,7 @@ const (
) )
// MakeHandler returns a HTTP handler for API endpoints. // 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{ opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), 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 := chi.NewRouter()
r.Route("/{domainID}", func(r chi.Router) { r.Route("/{domainID}", func(r chi.Router) {
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
r.Route("/mapping", func(r chi.Router) { r.Route("/mapping", func(r chi.Router) {
r.Post("/", kithttp.NewServer( r.Post("/", kithttp.NewServer(
doProvision(svc), doProvision(svc),
@@ -46,6 +48,12 @@ func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string)
opts..., opts...,
).ServeHTTP) ).ServeHTTP)
}) })
r.Post("/cert", kithttp.NewServer(
issueCert(svc),
decodeCertRequest,
api.EncodeResponse,
opts...,
).ServeHTTP)
}) })
r.Handle("/metrics", promhttp.Handler()) r.Handle("/metrics", promhttp.Handler())
r.Get("/health", supermq.Health("provision", instanceID)) r.Get("/health", supermq.Health("provision", instanceID))
@@ -59,8 +67,7 @@ func decodeProvisionRequest(_ context.Context, r *http.Request) (any, error) {
} }
req := provisionReq{ req := provisionReq{
token: apiutil.ExtractBearerToken(r), token: apiutil.ExtractBearerToken(r),
domainID: chi.URLParam(r, "domainID"),
} }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err) 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) { 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 { if r.Header.Get("Content-Type") != contentType {
return nil, apiutil.ErrUnsupportedContentType return nil, apiutil.ErrUnsupportedContentType
} }
req := mappingReq{ req := certReq{
token: apiutil.ExtractBearerToken(r), token: apiutil.ExtractBearerToken(r),
domainID: chi.URLParam(r, "domainID"), }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
} }
return req, nil return req, nil
+17 -18
View File
@@ -17,22 +17,21 @@ var errFailedToReadConfig = errors.New("failed to read config file")
// ServiceConf represents service config. // ServiceConf represents service config.
type ServiceConf struct { type ServiceConf struct {
Port string `toml:"port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"` Port string `toml:"port" env:"MG_PROVISION_HTTP_PORT" envDefault:"9016"`
LogLevel string `toml:"log_level" env:"SMQ_PROVISION_LOG_LEVEL" envDefault:"info"` LogLevel string `toml:"log_level" env:"MG_PROVISION_LOG_LEVEL" envDefault:"info"`
TLS bool `toml:"tls" env:"SMQ_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"` TLS bool `toml:"tls" env:"MG_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"`
ServerCert string `toml:"server_cert" env:"SMQ_PROVISION_SERVER_CERT" envDefault:""` ServerCert string `toml:"server_cert" env:"MG_PROVISION_SERVER_CERT" envDefault:""`
ServerKey string `toml:"server_key" env:"SMQ_PROVISION_SERVER_KEY" envDefault:""` ServerKey string `toml:"server_key" env:"MG_PROVISION_SERVER_KEY" envDefault:""`
ClientsURL string `toml:"clients_url" env:"SMQ_PROVISION_CLIENTS_LOCATION" envDefault:"http://localhost"` ClientsURL string `toml:"clients_url" env:"MG_PROVISION_CLIENTS_URL" envDefault:"http://localhost"`
UsersURL string `toml:"users_url" env:"SMQ_PROVISION_USERS_LOCATION" envDefault:"http://localhost"` ChannelsURL string `toml:"channels_url" env:"MG_PROVISION_CHANNELS_URL" envDefault:"http://localhost"`
CertsURL string `toml:"certs_url" env:"SMQ_PROVISION_CERTS_LOCATION" envDefault:"http://localhost"` UsersURL string `toml:"users_url" env:"MG_PROVISION_USERS_URL" envDefault:"http://localhost"`
HTTPPort string `toml:"http_port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"` CertsURL string `toml:"certs_url" env:"MG_PROVISION_CERTS_URL" envDefault:"http://localhost"`
MgEmail string `toml:"smq_email" env:"SMQ_PROVISION_EMAIL" envDefault:"test@example.com"` MgEmail string `toml:"mg_email" env:"MG_PROVISION_EMAIL" envDefault:"test@example.com"`
MgUsername string `toml:"smq_username" env:"SMQ_PROVISION_USERNAME" envDefault:"user"` MgUsername string `toml:"mg_username" env:"MG_PROVISION_USERNAME" envDefault:"user"`
MgPass string `toml:"smq_pass" env:"SMQ_PROVISION_PASS" envDefault:"test"` MgPass string `toml:"mg_pass" env:"MG_PROVISION_PASS" envDefault:"test"`
MgDomainID string `toml:"smq_domain_id" env:"SMQ_PROVISION_DOMAIN_ID" envDefault:""` MgDomainID string `toml:"mg_domain_id" env:"MG_PROVISION_DOMAIN_ID" envDefault:""`
MgAPIKey string `toml:"smq_api_key" env:"SMQ_PROVISION_API_KEY" envDefault:""` MgAPIKey string `toml:"mg_api_key" env:"MG_PROVISION_API_KEY" envDefault:""`
MgBSURL string `toml:"smq_bs_url" env:"SMQ_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"` MgBSURL string `toml:"mg_bs_url" env:"MG_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"`
MgCertsURL string `toml:"smq_certs_url" env:"SMQ_PROVISION_CERTS_SVC_URL" envDefault:"http://localhost:9019"`
} }
// Bootstrap represetns the Bootstrap config. // Bootstrap represetns the Bootstrap config.
@@ -61,13 +60,13 @@ type Cert struct {
// Config struct of Provision. // Config struct of Provision.
type Config struct { 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"` Server ServiceConf `toml:"server" mapstructure:"server"`
Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"` Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"`
Clients []clients.Client `toml:"clients" mapstructure:"clients"` Clients []clients.Client `toml:"clients" mapstructure:"clients"`
Channels []channels.Channel `toml:"channels" mapstructure:"channels"` Channels []channels.Channel `toml:"channels" mapstructure:"channels"`
Cert Cert `toml:"cert" mapstructure:"cert"` 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"` SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
InstanceID string `env:"SMQ_MQTT_ADAPTER_INSTANCE_ID" envDefault:""` InstanceID string `env:"SMQ_MQTT_ADAPTER_INSTANCE_ID" envDefault:""`
} }
@@ -1,9 +1,7 @@
// Copyright (c) Abstract Machines // Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
//go:build !test package middleware
package api
import ( import (
"context" "context"
@@ -20,8 +18,8 @@ type loggingMiddleware struct {
svc provision.Service svc provision.Service
} }
// NewLoggingMiddleware adds logging facilities to the core service. // NewLogging adds logging facilities to the core service.
func NewLoggingMiddleware(svc provision.Service, logger *slog.Logger) provision.Service { func NewLogging(svc provision.Service, logger *slog.Logger) provision.Service {
return &loggingMiddleware{logger, svc} return &loggingMiddleware{logger, svc}
} }
@@ -33,7 +31,7 @@ func (lm *loggingMiddleware) Provision(ctx context.Context, domainID, token, nam
slog.String("external_id", externalID), slog.String("external_id", externalID),
} }
if err != nil { if err != nil {
args = append(args, slog.Any("error", err)) args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Provision failed", args...) lm.logger.Warn("Provision failed", args...)
return return
} }
@@ -51,8 +49,8 @@ func (lm *loggingMiddleware) Cert(ctx context.Context, domainID, token, clientID
slog.String("ttl", duration), slog.String("ttl", duration),
} }
if err != nil { if err != nil {
args = append(args, slog.Any("error", err)) args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Client certificate failed to create successfully", args...) lm.logger.Warn("Client certificate creation failed", args...)
return return
} }
lm.logger.Info("Client certificate created successfully", args...) 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) 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) { defer func(begin time.Time) {
args := []any{ args := []any{
slog.String("duration", time.Since(begin).String()), 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...) lm.logger.Info("Mapping completed successfully", args...)
}(time.Now()) }(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 // Mapping provides a mock function for the type Service
func (_mock *Service) Mapping(ctx context.Context, token string) (map[string]any, error) { func (_mock *Service) Mapping() map[string]any {
ret := _mock.Called(ctx, token) ret := _mock.Called()
if len(ret) == 0 { if len(ret) == 0 {
panic("no return value specified for Mapping") panic("no return value specified for Mapping")
} }
var r0 map[string]any var r0 map[string]any
var r1 error if returnFunc, ok := ret.Get(0).(func() map[string]any); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (map[string]any, error)); ok { r0 = returnFunc()
return returnFunc(ctx, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) map[string]any); ok {
r0 = returnFunc(ctx, token)
} else { } else {
if ret.Get(0) != nil { if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]any) r0 = ret.Get(0).(map[string]any)
} }
} }
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { return r0
r1 = returnFunc(ctx, token)
} else {
r1 = ret.Error(1)
}
return r0, r1
} }
// Service_Mapping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mapping' // 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 // Mapping is a helper method to define mock.On call
// - ctx context.Context func (_e *Service_Expecter) Mapping() *Service_Mapping_Call {
// - token string return &Service_Mapping_Call{Call: _e.mock.On("Mapping")}
func (_e *Service_Expecter) Mapping(ctx interface{}, token interface{}) *Service_Mapping_Call {
return &Service_Mapping_Call{Call: _e.mock.On("Mapping", ctx, token)}
} }
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) { _c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context run()
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
}) })
return _c return _c
} }
func (_c *Service_Mapping_Call) Return(stringToV map[string]any, err error) *Service_Mapping_Call { func (_c *Service_Mapping_Call) Return(stringToV map[string]any) *Service_Mapping_Call {
_c.Call.Return(stringToV, err) _c.Call.Return(stringToV)
return _c 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) _c.Call.Return(run)
return _c return _c
} }
+22 -34
View File
@@ -27,25 +27,22 @@ const (
) )
var ( var (
ErrUnauthorized = errors.New("unauthorized access") ErrUnauthorized = errors.NewAuthNError("unauthorized access")
ErrFailedToCreateToken = errors.New("failed to create access token") ErrFailedToCreateToken = errors.NewAuthNError("failed to create access token")
ErrEmptyClientsList = errors.New("clients list in configuration empty") ErrEmptyClientsList = errors.NewRequestError("clients list in configuration empty")
ErrClientUpdate = errors.New("failed to update client") ErrClientUpdate = errors.NewRequestError("failed to update client")
ErrEmptyChannelsList = errors.New("channels list in configuration is empty") ErrEmptyChannelsList = errors.NewRequestError("channels list in configuration is empty")
ErrFailedChannelCreation = errors.New("failed to create channel") ErrFailedChannelCreation = errors.NewRequestError("failed to create channel")
ErrFailedChannelRetrieval = errors.New("failed to retrieve channel") ErrFailedChannelRetrieval = errors.NewRequestError("failed to retrieve channel")
ErrFailedClientCreation = errors.New("failed to create client") ErrFailedClientCreation = errors.NewRequestError("failed to create client")
ErrFailedClientRetrieval = errors.New("failed to retrieve client") ErrFailedClientRetrieval = errors.NewRequestError("failed to retrieve client")
ErrMissingCredentials = errors.New("missing credentials") ErrMissingCredentials = errors.NewRequestError("missing credentials")
ErrFailedBootstrapRetrieval = errors.New("failed to retrieve bootstrap") ErrFailedBootstrapRetrieval = errors.NewServiceError("failed to retrieve bootstrap")
ErrFailedCertCreation = errors.New("failed to create certificates") ErrFailedCertCreation = errors.NewServiceError("failed to create certificates")
ErrFailedCertView = errors.New("failed to view certificate") ErrFailedCertView = errors.NewServiceError("failed to view certificate")
ErrFailedBootstrap = errors.New("failed to create bootstrap config") ErrFailedBootstrap = errors.NewServiceError("failed to create bootstrap config")
ErrFailedBootstrapValidate = errors.New("failed to validate bootstrap config creation") ErrFailedBootstrapValidate = errors.NewServiceError("failed to validate bootstrap config creation")
ErrGatewayUpdate = errors.New("failed to updated gateway metadata") ErrGatewayUpdate = errors.NewServiceError("failed to update gateway metadata")
limit uint = 10
offset uint = 0
) )
var _ Service = (*provisionService)(nil) var _ Service = (*provisionService)(nil)
@@ -63,7 +60,7 @@ type Service interface {
// Mapping returns current configuration used for provision // Mapping returns current configuration used for provision
// useful for using in ui to create configuration that matches // useful for using in ui to create configuration that matches
// one created with Provision method. // 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 // Certs creates certificate for clients that communicate over mTLS
// A duration string is a possibly signed sequence of decimal numbers, // 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. // Mapping retrieves current configuration.
func (ps *provisionService) Mapping(ctx context.Context, token string) (map[string]any, error) { func (ps *provisionService) Mapping() map[string]any {
pm := smqSDK.PageMetadata{ return ps.conf.Bootstrap.Content
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
} }
// Provision is provision method for creating setup according to // 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) { func (ps *provisionService) Provision(ctx context.Context, domainID, token, name, externalID, externalKey string) (res Result, err error) {
var channels []smqSDK.Channel var channels []smqSDK.Channel
var clients []smqSDK.Client 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) token, err = ps.createTokenIfEmpty(ctx, token)
if err != nil { 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 { if e == nil {
return 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) { if errors.Contains(err, ErrFailedClientRetrieval) || errors.Contains(err, ErrFailedChannelCreation) {
for _, c := range clients { for _, c := range clients {
+2 -15
View File
@@ -31,35 +31,22 @@ func TestMapping(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
token string
content map[string]any content map[string]any
sdkerr error sdkerr error
err error err error
}{ }{
{ {
desc: "valid token", desc: "valid request",
token: validToken,
content: validConfig.Bootstrap.Content, content: validConfig.Bootstrap.Content,
sdkerr: nil, sdkerr: nil,
err: 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 { for _, c := range cases {
t.Run(c.desc, func(t *testing.T) { t.Run(c.desc, func(t *testing.T) {
pm := smqSDK.PageMetadata{Offset: uint64(0), Limit: uint64(10)} content := svc.Mapping()
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))
assert.Equal(t, c.content, content) assert.Equal(t, c.content, content)
repocall.Unset()
}) })
} }
} }