mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 04:30:25 +00:00
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:
+30
-6
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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]]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user