SMQ-2629 - Remove Boostrap and Provision services (#2640)

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
Felix Gateru
2025-01-10 11:56:03 +03:00
committed by GitHub
parent cd73d36bdf
commit df5d752c4b
88 changed files with 23 additions and 18145 deletions
+8 -68
View File
@@ -11,7 +11,6 @@ on:
- ".github/workflows/api-tests.yml"
- "api/**"
- "auth/api/http/**"
- "bootstrap/api**"
- "certs/api/**"
- "channels/api/http/**"
- "clients/api/http/**"
@@ -21,7 +20,6 @@ on:
- "http/api/**"
- "invitations/api/**"
- "journal/api/**"
- "provision/api/**"
- "readers/api/**"
- "users/api/**"
@@ -39,9 +37,7 @@ env:
HTTP_ADAPTER_URL: http://localhost:8008
INVITATIONS_URL: http://localhost:9020
AUTH_URL: http://localhost:9001
BOOTSTRAP_URL: http://localhost:9013
CERTS_URL: http://localhost:9019
PROVISION_URL: http://localhost:9016
POSTGRES_READER_URL: http://localhost:9009
TIMESCALE_READER_URL: http://localhost:9011
JOURNAL_URL: http://localhost:9021
@@ -88,16 +84,11 @@ jobs:
- "apidocs/openapi/auth.yml"
- "auth/api/http/**"
bootstrap:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/bootstrap.yml"
- "bootstrap/api/**"
certs:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/certs.yml"
- "certs/api/**"
domains:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/domains.yml"
@@ -113,11 +104,6 @@ jobs:
- "apidocs/openapi/invitations.yml"
- "invitations/api/**"
provision:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/provision.yml"
- "provision/api/**"
readers:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/readers.yml"
@@ -127,12 +113,12 @@ jobs:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/clients.yml"
- "clients/api/http/**"
channels:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/channels.yml"
- "channels/api/http/**"
groups:
- ".github/workflows/api-tests.yml"
- "apidocs/openapi/groups.yml"
@@ -152,7 +138,7 @@ jobs:
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Groups API tests
if: steps.changes.outputs.groups == 'true'
uses: schemathesis/action@v1
@@ -162,7 +148,7 @@ jobs:
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Clients API tests
if: steps.changes.outputs.clients == 'true'
uses: schemathesis/action@v1
@@ -172,7 +158,7 @@ jobs:
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Channels API tests
if: steps.changes.outputs.channels == 'true'
uses: schemathesis/action@v1
@@ -212,7 +198,7 @@ jobs:
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Domains API tests
if: steps.changes.outputs.domains == 'true'
uses: schemathesis/action@v1
@@ -222,7 +208,7 @@ jobs:
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Journal API tests
if: steps.changes.outputs.journal == 'true'
uses: schemathesis/action@v1
@@ -233,16 +219,6 @@ jobs:
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Bootstrap API tests
if: steps.changes.outputs.bootstrap == 'true'
uses: schemathesis/action@v1
with:
schema: apidocs/openapi/bootstrap.yml
base-url: ${{ env.BOOTSTRAP_URL }}
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Certs API tests
if: steps.changes.outputs.certs == 'true'
uses: schemathesis/action@v1
@@ -253,42 +229,6 @@ jobs:
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Provision API tests
if: steps.changes.outputs.provision == 'true'
uses: schemathesis/action@v1
with:
schema: apidocs/openapi/provision.yml
base-url: ${{ env.PROVISION_URL }}
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Seed Messages
if: steps.changes.outputs.readers == 'true'
run: |
make cli
./build/cli provision test
- name: Run Postgres Reader API tests
if: steps.changes.outputs.readers == 'true'
uses: schemathesis/action@v1
with:
schema: apidocs/openapi/readers.yml
base-url: ${{ env.POSTGRES_READER_URL }}
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Run Timescale Reader API tests
if: steps.changes.outputs.readers == 'true'
uses: schemathesis/action@v1
with:
schema: apidocs/openapi/readers.yml
base-url: ${{ env.TIMESCALE_READER_URL }}
checks: all
report: false
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
- name: Stop containers
if: always()
run: make run down args="-v" && make run_addons down args="-v"
@@ -57,10 +57,7 @@ jobs:
- "auth/keys.go"
- "auth/service.go"
- "pkg/events/events.go"
- "provision/service.go"
- "pkg/groups/groups.go"
- "bootstrap/service.go"
- "bootstrap/configs.go"
- "invitations/invitations.go"
- "users/emailer.go"
- "users/hasher.go"
@@ -132,9 +129,6 @@ jobs:
mv ./auth/mocks/authz.go ./auth/mocks/authz.go.tmp
mv ./auth/mocks/keys.go ./auth/mocks/keys.go.tmp
mv ./auth/mocks/service.go ./auth/mocks/service.go.tmp
mv ./bootstrap/mocks/configs.go ./bootstrap/mocks/configs.go.tmp
mv ./bootstrap/mocks/config_reader.go ./bootstrap/mocks/config_reader.go.tmp
mv ./bootstrap/mocks/service.go ./bootstrap/mocks/service.go.tmp
mv ./domains/mocks/domains_client.go ./domains/mocks/domains_client.go.tmp
mv ./domains/mocks/repository.go ./domains/mocks/repository.go.tmp
mv ./domains/mocks/service.go ./domains/mocks/service.go.tmp
@@ -156,7 +150,6 @@ jobs:
mv ./consumers/notifiers/mocks/service.go ./consumers/notifiers/mocks/service.go.tmp
mv ./certs/mocks/pki.go ./certs/mocks/pki.go.tmp
mv ./certs/mocks/service.go ./certs/mocks/service.go.tmp
mv ./provision/mocks/service.go ./provision/mocks/service.go.tmp
mv ./clients/private/mocks/service.go ./clients/private/mocks/service.go.tmp
mv ./clients/mocks/repository.go ./clients/mocks/repository.go.tmp
mv ./clients/mocks/clients_client.go ./clients/mocks/clients_client.go.tmp
@@ -197,9 +190,6 @@ jobs:
check_mock_changes ./auth/mocks/authz.go " ./auth/mocks/authz.go"
check_mock_changes ./auth/mocks/keys.go " ./auth/mocks/keys.go"
check_mock_changes ./auth/mocks/service.go " ./auth/mocks/service.go"
check_mock_changes ./bootstrap/mocks/configs.go " ./bootstrap/mocks/configs.go"
check_mock_changes ./bootstrap/mocks/config_reader.go " ./bootstrap/mocks/config_reader.go"
check_mock_changes ./bootstrap/mocks/service.go " ./bootstrap/mocks/service.go"
check_mock_changes ./domains/mocks/domains_client.go " ./domains/mocks/domains_client.go"
check_mock_changes ./domains/mocks/repository.go " ./domains/mocks/repository.go"
check_mock_changes ./domains/mocks/service.go " ./domains/mocks/service.go"
@@ -221,7 +211,6 @@ jobs:
check_mock_changes ./consumers/notifiers/mocks/service.go " ./consumers/notifiers/mocks/service.go"
check_mock_changes ./certs/mocks/pki.go " ./certs/mocks/pki.go"
check_mock_changes ./certs/mocks/service.go " ./certs/mocks/service.go"
check_mock_changes ./provision/mocks/service.go " ./provision/mocks/service.go"
check_mock_changes ./clients/private/mocks/service.go " ./clients/private/mocks/service.go"
check_mock_changes ./clients/mocks/repository.go " ./clients/mocks/repository.go"
check_mock_changes ./clients/mocks/clients_client.go " ./clients/mocks/clients_client.go"
+8 -35
View File
@@ -29,7 +29,7 @@ jobs:
- name: Lint Protobuf Files
run: |
protolint .
protolint .
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
@@ -83,15 +83,6 @@ jobs:
- "auth_grpc.pb.go"
- "pkg/ulid/**"
- "pkg/uuid/**"
bootstrap:
- "bootstrap/**"
- "cmd/bootstrap/**"
- "auth.pb.go"
- "auth_grpc.pb.go"
- "auth/**"
- "pkg/sdk/**"
- "pkg/events/**"
certs:
- "certs/**"
@@ -100,7 +91,7 @@ jobs:
- "auth_grpc.pb.go"
- "auth/**"
- "pkg/sdk/**"
channels:
- "channels/**"
- "cmd/channels/**"
@@ -152,7 +143,7 @@ jobs:
- "pkg/ulid/**"
- "pkg/uuid/**"
- "pkg/messaging/**"
domains:
- "domain/**"
- "cmd/domain/**"
@@ -193,7 +184,7 @@ jobs:
- "auth_grpc.pb.go"
- "auth/**"
- "pkg/sdk/**"
journal:
- "journal/**"
- "cmd/journal/**"
@@ -201,7 +192,7 @@ jobs:
- "auth_grpc.pb.go"
- "auth/**"
- "pkg/events/**"
logger:
- "logger/**"
@@ -233,7 +224,6 @@ jobs:
- "pkg/errors/**"
- "pkg/groups/**"
- "auth/**"
- "bootstrap/**"
- "certs/**"
- "consumers/**"
- "http/**"
@@ -242,7 +232,6 @@ jobs:
- "internal/apiutil/**"
- "internal/groups/**"
- "invitations/**"
- "provision/**"
- "readers/**"
- "clients/**"
- "users/**"
@@ -256,12 +245,6 @@ jobs:
pkg-uuid:
- "pkg/uuid/**"
provision:
- "provision/**"
- "cmd/provision/**"
- "logger/**"
- "pkg/sdk/**"
readers:
- "readers/**"
- "cmd/postgres-reader/**"
@@ -302,17 +285,12 @@ jobs:
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/auth.out ./auth/...
- name: Run domains tests
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/domains.out ./domains/...
- name: Run bootstrap tests
if: steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/bootstrap.out ./bootstrap/...
- name: Run certs tests
if: steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true'
run: |
@@ -398,11 +376,6 @@ jobs:
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-uuid.out ./pkg/uuid/...
- name: Run provision tests
if: steps.changes.outputs.provision == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/provision.out ./provision/...
- name: Run readers tests
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
run: |
@@ -412,7 +385,7 @@ jobs:
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/clients.out ./clients/...
- name: Run channels tests
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
run: |
@@ -422,7 +395,7 @@ jobs:
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/users.out ./users/...
- name: Run groups tests
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
run: |
+3 -5
View File
@@ -4,8 +4,8 @@
SMQ_DOCKER_IMAGE_NAME_PREFIX ?= supermq
BUILD_DIR ?= build
SERVICES = auth users clients groups channels domains http coap ws postgres-writer postgres-reader timescale-writer \
timescale-reader cli bootstrap mqtt provision certs invitations journal
TEST_API_SERVICES = journal auth bootstrap certs http invitations notifiers provision readers clients users channels groups domains
timescale-reader cli mqtt certs invitations journal
TEST_API_SERVICES = journal auth certs http invitations notifiers readers clients users channels groups domains
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
@@ -73,7 +73,7 @@ define make_docker_dev
-f docker/Dockerfile.dev ./build
endef
ADDON_SERVICES = bootstrap journal provision certs timescale-reader timescale-writer postgres-reader postgres-writer
ADDON_SERVICES = journal certs timescale-reader timescale-writer postgres-reader postgres-writer
EXTERNAL_SERVICES = vault prometheus
@@ -175,9 +175,7 @@ test_api_groups: TEST_API_URL := http://localhost:9004
test_api_http: TEST_API_URL := http://localhost:8008
test_api_invitations: TEST_API_URL := http://localhost:9020
test_api_auth: TEST_API_URL := http://localhost:9001
test_api_bootstrap: TEST_API_URL := http://localhost:9013
test_api_certs: TEST_API_URL := http://localhost:9019
test_api_provision: TEST_API_URL := http://localhost:9016
test_api_readers: TEST_API_URL := http://localhost:9009 # This can be the URL of any reader service.
test_api_journal: TEST_API_URL := http://localhost:9021
+2 -8
View File
@@ -10,7 +10,6 @@ import (
"github.com/absmach/supermq"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/certs"
"github.com/absmach/supermq/clients"
"github.com/absmach/supermq/groups"
@@ -127,9 +126,7 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
switch {
case errors.Contains(err, svcerr.ErrAuthorization),
errors.Contains(err, svcerr.ErrDomainAuthorization),
errors.Contains(err, svcerr.ErrUnauthorizedPAT),
errors.Contains(err, bootstrap.ErrExternalKey),
errors.Contains(err, bootstrap.ErrExternalKeySecure):
errors.Contains(err, svcerr.ErrUnauthorizedPAT):
err = unwrap(err)
w.WriteHeader(http.StatusForbidden)
@@ -169,11 +166,9 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
errors.Contains(err, apiutil.ErrInvitationState),
errors.Contains(err, apiutil.ErrInvalidAPIKey),
errors.Contains(err, svcerr.ErrViewEntity),
errors.Contains(err, apiutil.ErrBootstrapState),
errors.Contains(err, apiutil.ErrMissingCertData),
errors.Contains(err, apiutil.ErrInvalidContact),
errors.Contains(err, apiutil.ErrInvalidTopic),
errors.Contains(err, bootstrap.ErrAddBootstrap),
errors.Contains(err, apiutil.ErrInvalidCertData),
errors.Contains(err, apiutil.ErrEmptyMessage),
errors.Contains(err, apiutil.ErrInvalidLevel),
@@ -214,8 +209,7 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
err = unwrap(err)
w.WriteHeader(http.StatusUnprocessableEntity)
case errors.Contains(err, svcerr.ErrNotFound),
errors.Contains(err, bootstrap.ErrBootstrap):
case errors.Contains(err, svcerr.ErrNotFound):
err = unwrap(err)
w.WriteHeader(http.StatusNotFound)
-3
View File
@@ -156,9 +156,6 @@ var (
// ErrInvalidAPIKey indicates an invalid API key type.
ErrInvalidAPIKey = errors.New("invalid api key type")
// ErrBootstrapState indicates an invalid bootstrap state.
ErrBootstrapState = errors.New("invalid bootstrap state")
// ErrInvitationState indicates an invalid invitation state.
ErrInvitationState = errors.New("invalid invitation state")
-690
View File
@@ -1,690 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: SuperMQ Bootstrap service
description: |
HTTP API for managing platform clients configuration.
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
version: 0.15.1
servers:
- url: http://localhost:9013
- url: https://localhost:9013
tags:
- name: configs
description: Everything about your Configs
externalDocs:
description: Find out more about Configs
url: https://docs.supermq.abstractmachines.fr/
paths:
/{domainID}/clients/configs:
post:
operationId: createConfig
summary: Adds new config
description: |
Adds new config to the list of config owned by user identified using
the provided access token.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
requestBody:
$ref: "#/components/requestBodies/ConfigCreateReq"
responses:
"201":
$ref: "#/components/responses/ConfigCreateRes"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: A non-existent entity request.
"409":
description: Failed due to using an existing identity.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
"503":
description: Failed to receive response from the clients service.
get:
operationId: getConfigs
summary: Retrieves managed configs
description: |
Retrieves a list of managed configs. Due to performance concerns, data
is retrieved in subsets. The API configs must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/State"
- $ref: "#/components/parameters/Name"
responses:
"200":
$ref: "#/components/responses/ConfigListRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/clients/configs/{configId}:
get:
operationId: getConfig
summary: Retrieves config info (with channels).
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/ConfigId"
responses:
"200":
$ref: "#/components/responses/ConfigRes"
"400":
description: Missing or invalid config.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: Config does not exist.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
put:
operationId: updateConfig
summary: Updates config info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the owner, ID, external ID,
external key, SuperMQ Client ID and key cannot be changed.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigUpdateReq"
responses:
"200":
description: Config updated.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: Config does not exist.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: removeConfig
summary: Removes a Config
description: |
Removes a Config. In case of successful removal the service will ensure
that the removed config is disconnected from all of the SuperMQ channels.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/ConfigId"
responses:
"204":
description: Config removed.
"400":
description: Failed due to malformed config ID.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/clients/configs/certs/{configId}:
patch:
operationId: updateConfigCerts
summary: Updates certs
description: |
Update is performed by replacing the current certificate data with values
provided in a request payload.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
responses:
"200":
description: Config updated.
$ref: "#/components/responses/ConfigUpdateCertsRes"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: Config does not exist.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/clients/configs/connections/{configId}:
put:
operationId: updateConfigConnections
summary: Updates channels the client is connected to
description: |
Update connections performs update of the channel list corresponding
Client is connected to.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
responses:
"200":
description: Config updated.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: Config does not exist.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/clients/bootstrap/{externalId}:
get:
operationId: getBootstrapConfig
summary: Retrieves configuration.
description: |
Retrieves a configuration with given external ID and external key.
tags:
- configs
security:
- bootstrapAuth: []
parameters:
- $ref: "#/components/parameters/ExternalId"
responses:
"200":
$ref: "#/components/responses/BootstrapConfigRes"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid external key provided.
"404":
description: Failed to retrieve corresponding config.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/clients/bootstrap/secure/{externalId}:
get:
operationId: getSecureBootstrapConfig
summary: Retrieves configuration.
description: |
Retrieves a configuration with given external ID and encrypted external key.
tags:
- configs
security:
- bootstrapEncAuth: []
parameters:
- $ref: "#/components/parameters/ExternalId"
responses:
"200":
$ref: "#/components/responses/BootstrapConfigRes"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"404":
description: |
Failed to retrieve corresponding config.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/clients/state/{configId}:
put:
operationId: updateConfigState
summary: Updates Config state.
description: |
Updating state represents enabling/disabling Config, i.e. connecting
and disconnecting corresponding SuperMQ Client to the list of Channels.
tags:
- configs
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigStateUpdateReq"
responses:
"204":
description: Config removed.
"400":
description: Failed due to malformed config's ID.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
State:
type: integer
enum: [0, 1]
Config:
type: object
properties:
client_id:
type: string
format: uuid
description: Corresponding SuperMQ Client ID.
magistrala_secret:
type: string
format: uuid
description: Corresponding SuperMQ Client key.
channels:
type: array
minItems: 0
items:
type: object
properties:
id:
type: string
format: uuid
description: Channel unique identifier.
name:
type: string
description: Name of the Channel.
metadata:
type: object
description: Custom metadata related to the Channel.
external_id:
type: string
description: External ID (MAC address or some unique identifier).
external_key:
type: string
description: External key.
content:
type: string
description: Free-form custom configuration.
state:
$ref: "#/components/schemas/State"
client_cert:
type: string
description: Client certificate.
ca_cert:
type: string
description: Issuing CA certificate.
required:
- external_id
- external_key
ConfigList:
type: object
properties:
total:
type: integer
description: Total number of results.
minimum: 0
offset:
type: integer
description: Number of items to skip during retrieval.
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve.
maximum: 100
default: 10
configs:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/Config"
required:
- configs
BootstrapConfig:
type: object
properties:
client_id:
type: string
format: uuid
description: Corresponding SuperMQ Client ID.
client_key:
type: string
format: uuid
description: Corresponding SuperMQ Client key.
channels:
type: array
minItems: 0
items:
type: string
content:
type: string
description: Free-form custom configuration.
client_cert:
type: string
description: Client certificate.
required:
- client_id
- client_key
- channels
- content
ConfigUpdateCerts:
type: object
properties:
client_id:
type: string
format: uuid
description: Corresponding SuperMQ Client ID.
client_cert:
type: string
description: Client certificate.
client_key:
type: string
description: Key for the client_cert.
ca_cert:
type: string
description: Issuing CA certificate.
required:
- client_id
- client_key
- channels
- content
parameters:
ConfigId:
name: configId
description: Unique Config identifier. It's the ID of the corresponding Client.
in: path
schema:
type: string
format: uuid
required: true
ExternalId:
name: externalId
description: Unique Config identifier provided by external entity.
in: path
schema:
type: string
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
State:
name: state
description: A state of items
in: query
schema:
$ref: "#/components/schemas/State"
required: false
Name:
name: name
description: Name of the config. Search by name is partial-match and case-insensitive.
in: query
schema:
type: string
required: false
requestBodies:
ConfigCreateReq:
description: JSON-formatted document describing the new config.
required: true
content:
application/json:
schema:
type: object
properties:
external_id:
type: string
description: External ID (MAC address or some unique identifier).
external_key:
type: string
description: External key.
client_id:
type: string
format: uuid
description: ID of the corresponding SuperMQ Client.
channels:
type: array
minItems: 0
items:
type: string
format: uuid
content:
type: string
name:
type: string
client_cert:
type: string
description: Client Certificate.
client_key:
type: string
description: Client Private Key.
ca_cert:
type: string
required:
- external_id
- external_key
ConfigUpdateReq:
description: JSON-formatted document describing the updated client.
content:
application/json:
schema:
type: object
properties:
content:
type: string
name:
type: string
required:
- content
- name
ConfigCertUpdateReq:
description: JSON-formatted document describing the updated client.
content:
application/json:
schema:
type: object
properties:
client_cert:
type: string
client_key:
type: string
ca_cert:
type: string
ConfigConnUpdateReq:
description: Array if IDs the client is be connected to.
content:
application/json:
schema:
type: object
properties:
channels:
type: array
minItems: 0
items:
type: string
format: uuid
ConfigStateUpdateReq:
description: Update the state of the Config.
content:
application/json:
schema:
type: object
properties:
state:
$ref: "#/components/schemas/State"
responses:
ConfigCreateRes:
description: Config registered.
headers:
Location:
content:
text/plain:
schema:
type: string
description: Created configuration's relative URL (i.e. /clients/configs/{configId}).
ConfigListRes:
description: Data retrieved. Configs from this list don't contain channels.
content:
application/json:
schema:
$ref: "#/components/schemas/ConfigList"
ConfigRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Config"
links:
update:
operationId: updateConfig
parameters:
configId: $response.body#/id
updateCerts:
operationId: updateConfigCerts
parameters:
configId: $response.body#/id
updateConnections:
operationId: updateConfigConnections
parameters:
configId: $response.body#/id
updateState:
operationId: updateConfigState
parameters:
configId: $response.body#/id
delete:
operationId: removeConfig
parameters:
configId: $response.body#/id
BootstrapConfigRes:
description: |
Data retrieved. If secure, a response is encrypted using
the secret key, so the response is in the binary form.
content:
application/json:
schema:
$ref: "#/components/schemas/BootstrapConfig"
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yml"
ConfigUpdateCertsRes:
description: Data retrieved. Config certs updated.
content:
application/json:
schema:
$ref: "#/components/schemas/ConfigUpdateCerts"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
bootstrapAuth:
type: http
scheme: bearer
bearerFormat: string
description: |
* Clients access: "Authorization: Client <external_key>"
bootstrapEncAuth:
type: http
scheme: bearer
bearerFormat: aes-sha256-uuid
description: |
* Clients access: "Authorization: Client <external_enc_key>"
Hex-encoded configuration external key encrypted using
the AES algorithm and SHA256 sum of the external key
itself as an encryption key.
security:
- bearerAuth: []
-133
View File
@@ -1,133 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: SuperMQ Provision service
description: |
HTTP API for Provision service
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstracmachines.fr
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
version: 0.15.1
servers:
- url: http://localhost:9016
- url: https://localhost:9016
tags:
- name: provision
description: Everything about your Provision
externalDocs:
description: Find out more about provision
url: https://docs.supermq.abstractmachines.fr/
paths:
/{domainID}/mapping:
post:
summary: Adds new device to proxy
description: Adds new device to proxy
tags:
- provision
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
requestBody:
$ref: "#/components/requestBodies/ProvisionReq"
responses:
"201":
description: Created
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
get:
summary: Gets current mapping.
description: Gets current mapping. This can be used in UI
so that when bootstrap config is created from UI matches
configuration created with provision service.
tags:
- provision
parameters:
- $ref: "auth.yml#/components/parameters/DomainID"
responses:
"200":
$ref: "#/components/responses/ProvisionRes"
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
requestBodies:
ProvisionReq:
description: MAC address of device or other identifier
content:
application/json:
schema:
type: object
required:
- external_id
- external_key
properties:
external_id:
type: string
external_key:
type: string
name:
type: string
responses:
ServiceError:
description: Unexpected server-side error occurred.
ProvisionRes:
description: Current mapping JSON representation.
content:
application/json:
schema:
type: object
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
security:
- bearerAuth: []
-313
View File
@@ -1,313 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: SuperMQ reader service
description: |
HTTP API for reading messages.
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
version: 0.15.1
servers:
- url: http://localhost:9003
- url: https://localhost:9003
- url: http://localhost:9005
- url: https://localhost:9005
- url: http://localhost:9007
- url: https://localhost:9007
- url: http://localhost:9009
- url: https://localhost:9009
- url: http://localhost:9011
- url: https://localhost:9011
tags:
- name: readers
description: Everything about your Readers
externalDocs:
description: Find out more about readers
url: https://docs.supermq.abstractmachines.fr/
paths:
/channels/{chanId}/messages:
get:
operationId: getMessages
summary: Retrieves messages sent to single channel
description: |
Retrieves a list of messages sent to specific channel. Due to
performance concerns, data is retrieved in subsets. The API readers must
ensure that the entire dataset is consumed either by making subsequent
requests, or by increasing the subset size of the initial request.
tags:
- readers
parameters:
- $ref: "#/components/parameters/ChanId"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Publisher"
- $ref: "#/components/parameters/Name"
- $ref: "#/components/parameters/Value"
- $ref: "#/components/parameters/BoolValue"
- $ref: "#/components/parameters/StringValue"
- $ref: "#/components/parameters/DataValue"
- $ref: "#/components/parameters/From"
- $ref: "#/components/parameters/To"
- $ref: "#/components/parameters/Aggregation"
- $ref: "#/components/parameters/Interval"
responses:
"200":
$ref: "#/components/responses/MessagesPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
operationId: health
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
MessagesPage:
type: object
properties:
total:
type: number
description: Total number of items that are present on the system.
offset:
type: number
description: Number of items that were skipped during retrieval.
limit:
type: number
description: Size of the subset that was retrieved.
messages:
type: array
minItems: 0
uniqueItems: true
items:
type: object
properties:
channel:
type: integer
description: Unique channel id.
publisher:
type: integer
description: Unique publisher id.
protocol:
type: string
description: Protocol name.
name:
type: string
description: Measured parameter name.
unit:
type: string
description: Value unit.
value:
type: number
description: Measured value in number.
stringValue:
type: string
description: Measured value in string format.
boolValue:
type: boolean
description: Measured value in boolean format.
dataValue:
type: string
description: Measured value in binary format.
valueSum:
type: number
description: Sum value.
time:
type: number
description: Time of measurement.
updateTime:
type: number
description: Time of updating measurement.
parameters:
DomainID:
name: domainID
description: Unique domain identifier.
in: path
schema:
type: string
format: uuid
required: true
ChanId:
name: chanId
description: Unique channel identifier.
in: path
schema:
type: string
format: uuid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Publisher:
name: Publisher
description: Unique client identifier.
in: query
schema:
type: string
format: uuid
required: false
Name:
name: name
description: SenML message name.
in: query
schema:
type: string
required: false
Value:
name: v
description: SenML message value.
in: query
schema:
type: string
required: false
BoolValue:
name: vb
description: SenML message bool value.
in: query
schema:
type: boolean
required: false
StringValue:
name: vs
description: SenML message string value.
in: query
schema:
type: string
required: false
DataValue:
name: vd
description: SenML message data value.
in: query
schema:
type: string
required: false
Comparator:
name: comparator
description: Value comparison operator.
in: query
schema:
type: string
default: eq
enum:
- eq
- lt
- le
- gt
- ge
required: false
From:
name: from
description: SenML message time in nanoseconds (integer part represents seconds).
in: query
schema:
type: number
example: 1709218556069
required: false
To:
name: to
description: SenML message time in nanoseconds (integer part represents seconds).
in: query
schema:
type: number
example: 1709218757503
required: false
Aggregation:
name: aggregation
description: Aggregation function.
in: query
schema:
type: string
enum:
- MAX
- AVG
- MIN
- SUM
- COUNT
- max
- min
- sum
- avg
- count
example: MAX
required: false
Interval:
name: interval
description: Aggregation interval.
in: query
schema:
type: string
example: 10s
required: false
responses:
MessagesPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/MessagesPage"
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
clientAuth:
type: http
scheme: bearer
bearerFormat: uuid
description: |
* Clients access: "Authorization: Client <client_key>"
security:
- bearerAuth: []
- clientAuth: []
-122
View File
@@ -1,122 +0,0 @@
# BOOTSTRAP SERVICE
New devices need to be configured properly and connected to the Magistrala. Bootstrap service is used in order to accomplish that. This service provides the following features:
1. Creating new Magistrala Clients
2. Providing basic configuration for the newly created Clients
3. Enabling/disabling Clients
Pre-provisioning a new Client is as simple as sending Configuration data to the Bootstrap service. Once the Client is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Clients. Only enabled Clients can exchange messages over Magistrala. Bootstrapping does not implicitly enable Clients, it has to be done manually.
In order to bootstrap successfully, the Client needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Client is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Client will be saved so that it can be provisioned later.
## Client Configuration Entity
Client Configuration consists of two logical parts: the custom configuration that can be interpreted by the Client itself and Magistrala-related configuration. Magistrala config contains:
1. corresponding Magistrala Client ID
2. corresponding Magistrala Client key
3. list of the Magistrala channels the Client is connected to
> Note: list of channels contains IDs of the Magistrala channels. These channels are _pre-provisioned_ on the Magistrala side and, unlike corresponding Magistrala Client, Bootstrap service is not able to create Magistrala Channels.
Enabling and disabling Client (adding Client to/from whitelist) is as simple as connecting corresponding Magistrala Client to the given list of Channels. Configuration keeps _state_ of the Client:
| State | What it means |
| -------- | ---------------------------------------------- |
| Inactive | Client is created, but isn't enabled |
| Active | Client is able to communicate using Magistrala |
Switching between states `Active` and `Inactive` enables and disables Client, respectively.
Client configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Client. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
## Configuration
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
| Variable | Description | Default |
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------- |
| SMQ_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | info |
| SMQ_BOOTSTRAP_DB_HOST | Database host address | localhost |
| SMQ_BOOTSTRAP_DB_PORT | Database host port | 5432 |
| SMQ_BOOTSTRAP_DB_USER | Database user | magistrala |
| SMQ_BOOTSTRAP_DB_PASS | Database password | magistrala |
| SMQ_BOOTSTRAP_DB_NAME | Name of the database used by the service | bootstrap |
| SMQ_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| SMQ_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
| SMQ_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | "" |
| SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
| SMQ_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
| SMQ_BOOTSTRAP_HTTP_HOST | Bootstrap service HTTP host | "" |
| SMQ_BOOTSTRAP_HTTP_PORT | Bootstrap service HTTP port | 9013 |
| SMQ_BOOTSTRAP_HTTP_SERVER_CERT | Path to server certificate in pem format | "" |
| SMQ_BOOTSTRAP_HTTP_SERVER_KEY | Path to server key in pem format | "" |
| SMQ_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
| SMQ_ES_URL | Event store URL | <nats://localhost:4222> |
| SMQ_AUTH_GRPC_URL | Auth service Auth gRPC URL | <localhost:8181> |
| SMQ_AUTH_GRPC_TIMEOUT | Auth service Auth gRPC request timeout in seconds | 1s |
| SMQ_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service Auth gRPC client certificate file | "" |
| SMQ_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service Auth gRPC client key file | "" |
| SMQ_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server Auth gRPC server trusted CA certificate file | "" |
| SMQ_CLIENTS_URL | Base URL for Magistrala Clients | <http://localhost:9000> |
| SMQ_JAEGER_URL | Jaeger server URL | <http://localhost:4318/v1/traces> |
| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
| SMQ_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
| SMQ_BOOTSTRAP_INSTANCE_ID | Bootstrap service instance ID | "" |
## Deployment
The service itself is distributed as Docker container. Check the [`bootstrap`](https://github.com/absmach/magistrala/blob/main/docker/addons/bootstrap/docker-compose.yml) service section in docker-compose file to see how service is deployed.
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
git clone https://github.com/absmach/magistrala
cd magistrala
# compile the servic e
make bootstrap
# copy binary to bin
make install
# set the environment variables and run the service
SMQ_BOOTSTRAP_LOG_LEVEL=info \
SMQ_BOOTSTRAP_DB_HOST=localhost \
SMQ_BOOTSTRAP_DB_PORT=5432 \
SMQ_BOOTSTRAP_DB_USER=magistrala \
SMQ_BOOTSTRAP_DB_PASS=magistrala \
SMQ_BOOTSTRAP_DB_NAME=bootstrap \
SMQ_BOOTSTRAP_DB_SSL_MODE=disable \
SMQ_BOOTSTRAP_DB_SSL_CERT="" \
SMQ_BOOTSTRAP_DB_SSL_KEY="" \
SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT="" \
SMQ_BOOTSTRAP_HTTP_HOST=localhost \
SMQ_BOOTSTRAP_HTTP_PORT=9013 \
SMQ_BOOTSTRAP_HTTP_SERVER_CERT="" \
SMQ_BOOTSTRAP_HTTP_SERVER_KEY="" \
SMQ_BOOTSTRAP_EVENT_CONSUMER=bootstrap \
SMQ_ES_URL=nats://localhost:4222 \
SMQ_AUTH_GRPC_URL=localhost:8181 \
SMQ_AUTH_GRPC_TIMEOUT=1s \
SMQ_AUTH_GRPC_CLIENT_CERT="" \
SMQ_AUTH_GRPC_CLIENT_KEY="" \
SMQ_AUTH_GRPC_SERVER_CERTS="" \
SMQ_CLIENTS_URL=http://localhost:9000 \
SMQ_JAEGER_URL=http://localhost:14268/api/traces \
SMQ_JAEGER_TRACE_RATIO=1.0 \
SMQ_SEND_TELEMETRY=true \
SMQ_BOOTSTRAP_INSTANCE_ID="" \
$GOBIN/magistrala-bootstrap
```
Setting `SMQ_BOOTSTRAP_HTTP_SERVER_CERT` and `SMQ_BOOTSTRAP_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
Setting `SMQ_AUTH_GRPC_CLIENT_CERT` and `SMQ_AUTH_GRPC_CLIENT_KEY` will enable TLS against the auth service. The service expects a file in PEM format for both the certificate and the key. Setting `SMQ_AUTH_GRPC_SERVER_CERTS` will enable TLS against the auth service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
## Usage
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.magistrala.abstractmachines.fr/?urls.primaryName=bootstrap.yml).
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package api contains implementation of bootstrap service HTTP API.
package api
-290
View File
@@ -1,290 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
api "github.com/absmach/supermq/api/http"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/go-kit/kit/endpoint"
)
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(addReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
channels := []bootstrap.Channel{}
for _, c := range req.Channels {
channels = append(channels, bootstrap.Channel{ID: c})
}
config := bootstrap.Config{
ClientID: req.ClientID,
ExternalID: req.ExternalID,
ExternalKey: req.ExternalKey,
Channels: channels,
Name: req.Name,
ClientCert: req.ClientCert,
ClientKey: req.ClientKey,
CACert: req.CACert,
Content: req.Content,
}
saved, err := svc.Add(ctx, session, req.token, config)
if err != nil {
return nil, err
}
res := configRes{
id: saved.ClientID,
created: true,
}
return res, nil
}
}
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateCertReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
cfg, err := svc.UpdateCert(ctx, session, req.clientID, req.ClientCert, req.ClientKey, req.CACert)
if err != nil {
return nil, err
}
res := updateConfigRes{
ClientID: cfg.ClientID,
ClientCert: cfg.ClientCert,
CACert: cfg.CACert,
ClientKey: cfg.ClientKey,
}
return res, nil
}
}
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
config, err := svc.View(ctx, session, req.id)
if err != nil {
return nil, err
}
var channels []channelRes
for _, ch := range config.Channels {
channels = append(channels, channelRes{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
res := viewRes{
ClientID: config.ClientID,
CLientSecret: config.ClientSecret,
Channels: channels,
ExternalID: config.ExternalID,
ExternalKey: config.ExternalKey,
Name: config.Name,
Content: config.Content,
State: config.State,
}
return res, nil
}
}
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
config := bootstrap.Config{
ClientID: req.id,
Name: req.Name,
Content: req.Content,
}
if err := svc.Update(ctx, session, config); err != nil {
return nil, err
}
res := configRes{
id: config.ClientID,
created: false,
}
return res, nil
}
}
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateConnReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
if err := svc.UpdateConnections(ctx, session, req.token, req.id, req.Channels); err != nil {
return nil, err
}
res := configRes{
id: req.id,
created: false,
}
return res, nil
}
}
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
page, err := svc.List(ctx, session, req.filter, req.offset, req.limit)
if err != nil {
return nil, err
}
res := listRes{
Total: page.Total,
Offset: page.Offset,
Limit: page.Limit,
Configs: []viewRes{},
}
for _, cfg := range page.Configs {
var channels []channelRes
for _, ch := range cfg.Channels {
channels = append(channels, channelRes{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
view := viewRes{
ClientID: cfg.ClientID,
CLientSecret: cfg.ClientSecret,
Channels: channels,
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Name: cfg.Name,
Content: cfg.Content,
State: cfg.State,
}
res.Configs = append(res.Configs, view)
}
return res, nil
}
}
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
if err := req.validate(); err != nil {
return removeRes{}, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
if err := svc.Remove(ctx, session, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(bootstrapReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
cfg, err := svc.Bootstrap(ctx, req.key, req.id, secure)
if err != nil {
return nil, err
}
return reader.ReadConfig(cfg, secure)
}
}
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(changeStateReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
if err := svc.ChangeState(ctx, session, req.token, req.id, req.State); err != nil {
return nil, err
}
return stateRes{}, nil
}
}
File diff suppressed because it is too large Load Diff
-163
View File
@@ -1,163 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/bootstrap"
)
const maxLimitSize = 100
type addReq struct {
token string
ClientID string `json:"client_id"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
Channels []string `json:"channels"`
Name string `json:"name"`
Content string `json:"content"`
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
}
func (req addReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.ExternalID == "" {
return apiutil.ErrMissingID
}
if req.ExternalKey == "" {
return apiutil.ErrBearerKey
}
if len(req.Channels) == 0 {
return apiutil.ErrEmptyList
}
for _, channel := range req.Channels {
if channel == "" {
return apiutil.ErrMissingID
}
}
return nil
}
type entityReq struct {
id string
}
func (req entityReq) validate() error {
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
type updateReq struct {
id string
Name string `json:"name"`
Content string `json:"content"`
}
func (req updateReq) validate() error {
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
type updateCertReq struct {
clientID string
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
}
func (req updateCertReq) validate() error {
if req.clientID == "" {
return apiutil.ErrMissingID
}
return nil
}
type updateConnReq struct {
token string
id string
Channels []string `json:"channels"`
}
func (req updateConnReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
type listReq struct {
filter bootstrap.Filter
offset uint64
limit uint64
}
func (req listReq) validate() error {
if req.limit > maxLimitSize {
return apiutil.ErrLimitSize
}
return nil
}
type bootstrapReq struct {
key string
id string
}
func (req bootstrapReq) validate() error {
if req.key == "" {
return apiutil.ErrBearerKey
}
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
type changeStateReq struct {
token string
id string
State bootstrap.State `json:"state"`
}
func (req changeStateReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.id == "" {
return apiutil.ErrMissingID
}
if req.State != bootstrap.Inactive &&
req.State != bootstrap.Active {
return apiutil.ErrBootstrapState
}
return nil
}
-313
View File
@@ -1,313 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"fmt"
"testing"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/internal/testsutil"
"github.com/stretchr/testify/assert"
)
var (
channel1 = testsutil.GenerateUUID(&testing.T{})
channel2 = testsutil.GenerateUUID(&testing.T{})
)
func TestAddReqValidation(t *testing.T) {
cases := []struct {
desc string
token string
externalID string
externalKey string
channels []string
err error
}{
{
desc: "valid request",
token: "token",
externalID: "external-id",
externalKey: "external-key",
channels: []string{channel1, channel2},
err: nil,
},
{
desc: "empty token",
token: "",
externalID: "external-id",
externalKey: "external-key",
channels: []string{channel1, channel2},
err: apiutil.ErrBearerToken,
},
{
desc: "empty external ID",
token: "token",
externalID: "",
externalKey: "external-key",
channels: []string{channel1, channel2},
err: apiutil.ErrMissingID,
},
{
desc: "empty external key",
token: "token",
externalID: "external-id",
externalKey: "",
channels: []string{channel1, channel2},
err: apiutil.ErrBearerKey,
},
{
desc: "empty external key and external ID",
token: "token",
externalID: "",
externalKey: "",
channels: []string{channel1, channel2},
err: apiutil.ErrMissingID,
},
{
desc: "empty channels",
token: "token",
externalID: "external-id",
externalKey: "external-key",
channels: []string{},
err: apiutil.ErrEmptyList,
},
{
desc: "empty channel value",
token: "token",
externalID: "external-id",
externalKey: "external-key",
channels: []string{channel1, ""},
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
req := addReq{
token: tc.token,
ExternalID: tc.externalID,
ExternalKey: tc.externalKey,
Channels: tc.channels,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestEntityReqValidation(t *testing.T) {
cases := []struct {
desc string
id string
err error
}{
{
desc: "empty id",
id: "",
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
req := entityReq{
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateReqValidation(t *testing.T) {
cases := []struct {
desc string
id string
err error
}{
{
desc: "valid request",
id: "id",
err: nil,
},
{
desc: "empty id",
id: "",
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
req := updateReq{
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateCertReqValidation(t *testing.T) {
cases := []struct {
desc string
clientID string
err error
}{
{
desc: "empty client id",
clientID: "",
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
req := updateCertReq{
clientID: tc.clientID,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnReqValidation(t *testing.T) {
cases := []struct {
desc string
id string
token string
err error
}{
{
desc: "empty token",
token: "",
id: "id",
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
token: "token",
id: "",
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
req := updateConnReq{
token: tc.token,
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListReqValidation(t *testing.T) {
cases := []struct {
desc string
offset uint64
limit uint64
err error
}{
{
desc: "too large limit",
offset: 0,
limit: maxLimitSize + 1,
err: apiutil.ErrLimitSize,
},
{
desc: "default limit",
offset: 0,
limit: defLimit,
err: nil,
},
}
for _, tc := range cases {
req := listReq{
offset: tc.offset,
limit: tc.limit,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestBootstrapReqValidation(t *testing.T) {
cases := []struct {
desc string
externKey string
externID string
err error
}{
{
desc: "empty external key",
externKey: "",
externID: "id",
err: apiutil.ErrBearerKey,
},
{
desc: "empty external id",
externKey: "key",
externID: "",
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
req := bootstrapReq{
id: tc.externID,
key: tc.externKey,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChangeStateReqValidation(t *testing.T) {
cases := []struct {
desc string
token string
id string
state bootstrap.State
err error
}{
{
desc: "empty token",
token: "",
id: "id",
state: bootstrap.State(1),
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
token: "token",
id: "",
state: bootstrap.State(0),
err: apiutil.ErrMissingID,
},
{
desc: "invalid state",
token: "token",
id: "id",
state: bootstrap.State(14),
err: apiutil.ErrBootstrapState,
},
}
for _, tc := range cases {
req := changeStateReq{
token: tc.token,
id: tc.id,
State: tc.state,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
-144
View File
@@ -1,144 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"fmt"
"net/http"
"github.com/absmach/supermq"
"github.com/absmach/supermq/bootstrap"
)
var (
_ supermq.Response = (*removeRes)(nil)
_ supermq.Response = (*configRes)(nil)
_ supermq.Response = (*stateRes)(nil)
_ supermq.Response = (*viewRes)(nil)
_ supermq.Response = (*listRes)(nil)
)
type removeRes struct{}
func (res removeRes) Code() int {
return http.StatusNoContent
}
func (res removeRes) Headers() map[string]string {
return map[string]string{}
}
func (res removeRes) Empty() bool {
return true
}
type configRes struct {
id string
created bool
}
func (res configRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res configRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/clients/configs/%s", res.id),
}
}
return map[string]string{}
}
func (res configRes) Empty() bool {
return true
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
type viewRes struct {
ClientID string `json:"client_id,omitempty"`
CLientSecret string `json:"client_secret,omitempty"`
Channels []channelRes `json:"channels,omitempty"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key,omitempty"`
Content string `json:"content,omitempty"`
Name string `json:"name,omitempty"`
State bootstrap.State `json:"state"`
ClientCert string `json:"client_cert,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
func (res viewRes) Code() int {
return http.StatusOK
}
func (res viewRes) Headers() map[string]string {
return map[string]string{}
}
func (res viewRes) Empty() bool {
return false
}
type listRes struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Configs []viewRes `json:"configs"`
}
func (res listRes) Code() int {
return http.StatusOK
}
func (res listRes) Headers() map[string]string {
return map[string]string{}
}
func (res listRes) Empty() bool {
return false
}
type stateRes struct{}
func (res stateRes) Code() int {
return http.StatusOK
}
func (res stateRes) Headers() map[string]string {
return map[string]string{}
}
func (res stateRes) Empty() bool {
return true
}
type updateConfigRes struct {
ClientID string `json:"client_id,omitempty"`
CACert string `json:"ca_cert,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
}
func (res updateConfigRes) Code() int {
return http.StatusOK
}
func (res updateConfigRes) Headers() map[string]string {
return map[string]string{}
}
func (res updateConfigRes) Empty() bool {
return false
}
-284
View File
@@ -1,284 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/absmach/supermq"
api "github.com/absmach/supermq/api/http"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/bootstrap"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
"github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
const (
contentType = "application/json"
byteContentType = "application/octet-stream"
offsetKey = "offset"
limitKey = "limit"
defOffset = 0
defLimit = 10
)
var (
fullMatch = []string{"state", "external_id", "client_id", "client_key"}
partialMatch = []string{"name"}
// ErrBootstrap indicates error in getting bootstrap configuration.
ErrBootstrap = errors.New("failed to read bootstrap configuration")
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc bootstrap.Service, authn smqauthn.Authentication, reader bootstrap.ConfigReader, logger *slog.Logger, instanceID string) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
}
r := chi.NewRouter()
r.Route("/{domainID}/clients", func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(api.AuthenticateMiddleware(authn, true))
r.Route("/configs", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
addEndpoint(svc),
decodeAddRequest,
api.EncodeResponse,
opts...), "add").ServeHTTP)
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
listEndpoint(svc),
decodeListRequest,
api.EncodeResponse,
opts...), "list").ServeHTTP)
r.Get("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
viewEndpoint(svc),
decodeEntityRequest,
api.EncodeResponse,
opts...), "view").ServeHTTP)
r.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
updateEndpoint(svc),
decodeUpdateRequest,
api.EncodeResponse,
opts...), "update").ServeHTTP)
r.Delete("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
removeEndpoint(svc),
decodeEntityRequest,
api.EncodeResponse,
opts...), "remove").ServeHTTP)
r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer(
updateCertEndpoint(svc),
decodeUpdateCertRequest,
api.EncodeResponse,
opts...), "update_cert").ServeHTTP)
r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer(
updateConnEndpoint(svc),
decodeUpdateConnRequest,
api.EncodeResponse,
opts...), "update_connections").ServeHTTP)
})
})
r.With(api.AuthenticateMiddleware(authn, true)).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
stateEndpoint(svc),
decodeStateRequest,
api.EncodeResponse,
opts...), "update_state").ServeHTTP)
})
r.Route("/clients/bootstrap", func(r chi.Router) {
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
bootstrapEndpoint(svc, reader, false),
decodeBootstrapRequest,
api.EncodeResponse,
opts...), "bootstrap").ServeHTTP)
r.Get("/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
bootstrapEndpoint(svc, reader, false),
decodeBootstrapRequest,
api.EncodeResponse,
opts...), "bootstrap").ServeHTTP)
r.Get("/secure/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
bootstrapEndpoint(svc, reader, true),
decodeBootstrapRequest,
encodeSecureRes,
opts...), "bootstrap_secure").ServeHTTP)
})
r.Get("/health", supermq.Health("bootstrap", instanceID))
r.Handle("/metrics", promhttp.Handler())
return r
}
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := addReq{
token: apiutil.ExtractBearerToken(r),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
}
return req, nil
}
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := updateReq{
id: chi.URLParam(r, "configID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
}
return req, nil
}
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := updateCertReq{
clientID: chi.URLParam(r, "certID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
}
return req, nil
}
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := updateConnReq{
token: apiutil.ExtractBearerToken(r),
id: chi.URLParam(r, "connID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
}
return req, nil
}
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
o, err := apiutil.ReadNumQuery[uint64](r, offsetKey, defOffset)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
l, err := apiutil.ReadNumQuery[uint64](r, limitKey, defLimit)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidQueryParams)
}
req := listReq{
filter: parseFilter(q),
offset: o,
limit: l,
}
return req, nil
}
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := bootstrapReq{
id: chi.URLParam(r, "externalID"),
key: apiutil.ExtractClientSecret(r),
}
return req, nil
}
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := changeStateReq{
token: apiutil.ExtractBearerToken(r),
id: chi.URLParam(r, "clientID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
}
return req, nil
}
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := entityReq{
id: chi.URLParam(r, "configID"),
}
return req, nil
}
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", byteContentType)
w.WriteHeader(http.StatusOK)
if b, ok := response.([]byte); ok {
if _, err := w.Write(b); err != nil {
return err
}
}
return nil
}
func parseFilter(values url.Values) bootstrap.Filter {
ret := bootstrap.Filter{
FullMatch: make(map[string]string),
PartialMatch: make(map[string]string),
}
for k := range values {
if contains(fullMatch, k) {
ret.FullMatch[k] = values.Get(k)
}
if contains(partialMatch, k) {
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
}
}
return ret
}
func contains(l []string, s string) bool {
for _, v := range l {
if v == s {
return true
}
}
return false
}
-120
View File
@@ -1,120 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"context"
"time"
"github.com/absmach/supermq/clients"
)
// Config represents Configuration entity. It wraps information about external entity
// as well as info about corresponding SuperMQ entities.
// MGClient represents corresponding SuperMQ Client ID.
// MGKey is key of corresponding SuperMQ Client.
// MGChannels is a list of SuperMQ Channels corresponding SuperMQ Client connects to.
type Config struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
DomainID string `json:"domain_id,omitempty"`
Name string `json:"name,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
Channels []Channel `json:"channels,omitempty"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
Content string `json:"content,omitempty"`
State State `json:"state"`
}
// Channel represents SuperMQ channel corresponding SuperMQ Client is connected to.
type Channel struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
DomainID string `json:"domain_id"`
Parent string `json:"parent_id,omitempty"`
Description string `json:"description,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status clients.Status `json:"status"`
}
// Filter is used for the search filters.
type Filter struct {
FullMatch map[string]string
PartialMatch map[string]string
}
// ConfigsPage contains page related metadata as well as list of Configs that
// belong to this page.
type ConfigsPage struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Configs []Config `json:"configs"`
}
// ConfigRepository specifies a Config persistence API.
//
//go:generate mockery --name ConfigRepository --output=./mocks --filename configs.go --quiet --note "Copyright (c) Abstract Machines"
type ConfigRepository interface {
// Save persists the Config. Successful operation is indicated by non-nil
// error response.
Save(ctx context.Context, cfg Config, chsConnIDs []string) (string, error)
// RetrieveByID retrieves the Config having the provided identifier, that is owned
// by the specified user.
RetrieveByID(ctx context.Context, domainID, id string) (Config, error)
// RetrieveAll retrieves a subset of Configs that are owned
// by the specific user, with given filter parameters.
RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter Filter, offset, limit uint64) ConfigsPage
// RetrieveByExternalID returns Config for given external ID.
RetrieveByExternalID(ctx context.Context, externalID string) (Config, error)
// Update updates an existing Config. A non-nil error is returned
// to indicate operation failure.
Update(ctx context.Context, cfg Config) error
// UpdateCerts updates and returns an existing Config certificate and domainID.
// A non-nil error is returned to indicate operation failure.
UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (Config, error)
// UpdateConnections updates a list of Channels the Config is connected to
// adding new Channels if needed.
UpdateConnections(ctx context.Context, domainID, id string, channels []Channel, connections []string) error
// Remove removes the Config having the provided identifier, that is owned
// by the specified user.
Remove(ctx context.Context, domainID, id string) error
// ChangeState changes of the Config, that is owned by the specific user.
ChangeState(ctx context.Context, domainID, id string, state State) error
// ListExisting retrieves those channels from the given list that exist in DB.
ListExisting(ctx context.Context, domainID string, ids []string) ([]Channel, error)
// Methods RemoveClient, UpdateChannel, and RemoveChannel are related to
// event sourcing. That's why these methods surpass ownership check.
// RemoveClient removes Config of the Client with the given ID.
RemoveClient(ctx context.Context, id string) error
// UpdateChannel updates channel with the given ID.
UpdateChannel(ctx context.Context, c Channel) error
// RemoveChannel removes channel with the given ID.
RemoveChannel(ctx context.Context, id string) error
// ConnectClient changes state of the Config when the corresponding Client is connected to the Channel.
ConnectClient(ctx context.Context, channelID, clientID string) error
// DisconnectClient changes state of the Config when the corresponding Client is disconnected from the Channel.
DisconnectClient(ctx context.Context, channelID, clientID string) error
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package bootstrap contains the domain concept definitions needed to support
// SuperMQ bootstrap service functionality.
package bootstrap
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package consumer contains events consumer for events
// published by Bootstrap service.
package consumer
-24
View File
@@ -1,24 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package consumer
import "time"
type removeEvent struct {
id string
}
type updateChannelEvent struct {
id string
name string
metadata map[string]interface{}
updatedAt time.Time
updatedBy string
}
// Connection event is either connect or disconnect event.
type connectionEvent struct {
clientIDs []string
channelID string
}
-148
View File
@@ -1,148 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package consumer
import (
"context"
"time"
"github.com/absmach/supermq/bootstrap"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/events"
)
const (
clientRemove = "client.remove"
clientConnect = "group.assign"
clientDisconnect = "group.unassign"
channelPrefix = "channels."
channelUpdate = channelPrefix + "update"
channelRemove = channelPrefix + "remove"
memberKind = "client"
relation = "group"
)
type eventHandler struct {
svc bootstrap.Service
}
// NewEventHandler returns new event store handler.
func NewEventHandler(svc bootstrap.Service) events.EventHandler {
return &eventHandler{
svc: svc,
}
}
func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
msg, err := event.Encode()
if err != nil {
return err
}
switch msg["operation"] {
case clientRemove:
rte := decodeRemoveClient(msg)
err = es.svc.RemoveConfigHandler(ctx, rte.id)
case clientConnect:
cte := decodeConnectClient(msg)
if cte.channelID == "" || len(cte.clientIDs) == 0 {
return svcerr.ErrMalformedEntity
}
for _, clientID := range cte.clientIDs {
if clientID == "" {
return svcerr.ErrMalformedEntity
}
if err := es.svc.ConnectClientHandler(ctx, cte.channelID, clientID); err != nil {
return err
}
}
case clientDisconnect:
dte := decodeDisconnectClient(msg)
if dte.channelID == "" || len(dte.clientIDs) == 0 {
return svcerr.ErrMalformedEntity
}
for _, clientID := range dte.clientIDs {
if clientID == "" {
return svcerr.ErrMalformedEntity
}
}
for _, c := range dte.clientIDs {
if err = es.svc.DisconnectClientHandler(ctx, dte.channelID, c); err != nil {
return err
}
}
case channelUpdate:
uce := decodeUpdateChannel(msg)
err = es.handleUpdateChannel(ctx, uce)
case channelRemove:
rce := decodeRemoveChannel(msg)
err = es.svc.RemoveChannelHandler(ctx, rce.id)
}
if err != nil {
return err
}
return nil
}
func decodeRemoveClient(event map[string]interface{}) removeEvent {
return removeEvent{
id: events.Read(event, "id", ""),
}
}
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
metadata := events.Read(event, "metadata", map[string]interface{}{})
return updateChannelEvent{
id: events.Read(event, "id", ""),
name: events.Read(event, "name", ""),
metadata: metadata,
updatedAt: events.Read(event, "updated_at", time.Now()),
updatedBy: events.Read(event, "updated_by", ""),
}
}
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
return removeEvent{
id: events.Read(event, "id", ""),
}
}
func decodeConnectClient(event map[string]interface{}) connectionEvent {
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
return connectionEvent{}
}
return connectionEvent{
channelID: events.Read(event, "group_id", ""),
clientIDs: events.ReadStringSlice(event, "member_ids"),
}
}
func decodeDisconnectClient(event map[string]interface{}) connectionEvent {
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
return connectionEvent{}
}
return connectionEvent{
channelID: events.Read(event, "group_id", ""),
clientIDs: events.ReadStringSlice(event, "member_ids"),
}
}
func (es *eventHandler) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
channel := bootstrap.Channel{
ID: uce.id,
Name: uce.name,
Metadata: uce.metadata,
UpdatedAt: uce.updatedAt,
UpdatedBy: uce.updatedBy,
}
return es.svc.UpdateChannelHandler(ctx, channel)
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package events provides the domain concept definitions needed to support
// bootstrap events functionality.
package events
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package producer contains the domain events needed to support
// event sourcing of Bootstrap service actions.
package producer
-277
View File
@@ -1,277 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package producer
import (
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/pkg/events"
)
const (
configPrefix = "bootstrap.config."
configCreate = configPrefix + "create"
configUpdate = configPrefix + "update"
configRemove = configPrefix + "remove"
configView = configPrefix + "view"
configList = configPrefix + "list"
configHandlerRemove = configPrefix + "remove_handler"
clientPrefix = "bootstrap.client."
clientBootstrap = clientPrefix + "bootstrap"
clientStateChange = clientPrefix + "change_state"
clientUpdateConnections = clientPrefix + "update_connections"
clientConnect = clientPrefix + "connect"
clientDisconnect = clientPrefix + "disconnect"
channelPrefix = "bootstrap.channel."
channelHandlerRemove = channelPrefix + "remove_handler"
channelUpdateHandler = channelPrefix + "update_handler"
certUpdate = "bootstrap.cert.update"
)
var (
_ events.Event = (*configEvent)(nil)
_ events.Event = (*removeConfigEvent)(nil)
_ events.Event = (*bootstrapEvent)(nil)
_ events.Event = (*changeStateEvent)(nil)
_ events.Event = (*updateConnectionsEvent)(nil)
_ events.Event = (*updateCertEvent)(nil)
_ events.Event = (*listConfigsEvent)(nil)
_ events.Event = (*removeHandlerEvent)(nil)
)
type configEvent struct {
bootstrap.Config
operation string
}
func (ce configEvent) Encode() (map[string]interface{}, error) {
val := map[string]interface{}{
"state": ce.State.String(),
"operation": ce.operation,
}
if ce.ClientID != "" {
val["client_id"] = ce.ClientID
}
if ce.Content != "" {
val["content"] = ce.Content
}
if ce.DomainID != "" {
val["domain_id "] = ce.DomainID
}
if ce.Name != "" {
val["name"] = ce.Name
}
if ce.ExternalID != "" {
val["external_id"] = ce.ExternalID
}
if len(ce.Channels) > 0 {
channels := make([]string, len(ce.Channels))
for i, ch := range ce.Channels {
channels[i] = ch.ID
}
val["channels"] = channels
}
if ce.ClientCert != "" {
val["client_cert"] = ce.ClientCert
}
if ce.ClientKey != "" {
val["client_key"] = ce.ClientKey
}
if ce.CACert != "" {
val["ca_cert"] = ce.CACert
}
if ce.Content != "" {
val["content"] = ce.Content
}
return val, nil
}
type removeConfigEvent struct {
client string
}
func (rce removeConfigEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"client_id": rce.client,
"operation": configRemove,
}, nil
}
type listConfigsEvent struct {
offset uint64
limit uint64
fullMatch map[string]string
partialMatch map[string]string
}
func (rce listConfigsEvent) Encode() (map[string]interface{}, error) {
val := map[string]interface{}{
"offset": rce.offset,
"limit": rce.limit,
"operation": configList,
}
if len(rce.fullMatch) > 0 {
val["full_match"] = rce.fullMatch
}
if len(rce.partialMatch) > 0 {
val["full_match"] = rce.partialMatch
}
return val, nil
}
type bootstrapEvent struct {
bootstrap.Config
externalID string
success bool
}
func (be bootstrapEvent) Encode() (map[string]interface{}, error) {
val := map[string]interface{}{
"external_id": be.externalID,
"success": be.success,
"operation": clientBootstrap,
}
if be.ClientID != "" {
val["client_id"] = be.ClientID
}
if be.Content != "" {
val["content"] = be.Content
}
if be.DomainID != "" {
val["domain_id "] = be.DomainID
}
if be.Name != "" {
val["name"] = be.Name
}
if be.ExternalID != "" {
val["external_id"] = be.ExternalID
}
if len(be.Channels) > 0 {
channels := make([]string, len(be.Channels))
for i, ch := range be.Channels {
channels[i] = ch.ID
}
val["channels"] = channels
}
if be.ClientCert != "" {
val["client_cert"] = be.ClientCert
}
if be.ClientKey != "" {
val["client_key"] = be.ClientKey
}
if be.CACert != "" {
val["ca_cert"] = be.CACert
}
if be.Content != "" {
val["content"] = be.Content
}
return val, nil
}
type changeStateEvent struct {
mgClient string
state bootstrap.State
}
func (cse changeStateEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"client_id": cse.mgClient,
"state": cse.state.String(),
"operation": clientStateChange,
}, nil
}
type updateConnectionsEvent struct {
mgClient string
mgChannels []string
}
func (uce updateConnectionsEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"client_id": uce.mgClient,
"channels": uce.mgChannels,
"operation": clientUpdateConnections,
}, nil
}
type updateCertEvent struct {
clientID string
clientCert string
clientKey string
caCert string
}
func (uce updateCertEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"client_id": uce.clientID,
"client_cert": uce.clientCert,
"client_key": uce.clientKey,
"ca_cert": uce.caCert,
"operation": certUpdate,
}, nil
}
type removeHandlerEvent struct {
id string
operation string
}
func (rhe removeHandlerEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"config_id": rhe.id,
"operation": rhe.operation,
}, nil
}
type updateChannelHandlerEvent struct {
bootstrap.Channel
}
func (uche updateChannelHandlerEvent) Encode() (map[string]interface{}, error) {
val := map[string]interface{}{
"operation": channelUpdateHandler,
}
if uche.ID != "" {
val["channel_id"] = uche.ID
}
if uche.Name != "" {
val["name"] = uche.Name
}
if uche.Metadata != nil {
val["metadata"] = uche.Metadata
}
return val, nil
}
type connectClientEvent struct {
clientID string
channelID string
}
func (cte connectClientEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"client_id": cte.clientID,
"channel_id": cte.channelID,
"operation": clientConnect,
}, nil
}
type disconnectClientEvent struct {
clientID string
channelID string
}
func (dte disconnectClientEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"client_id": dte.clientID,
"channel_id": dte.channelID,
"operation": clientDisconnect,
}, nil
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package producer_test
import (
"context"
"fmt"
"log"
"os"
"testing"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/redis/go-redis/v9"
)
var (
redisClient *redis.Client
redisURL string
)
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
container, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "redis",
Tag: "7.2.4-alpine",
}, func(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
})
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
redisURL = fmt.Sprintf("redis://localhost:%s/0", container.GetPort("6379/tcp"))
opts, err := redis.ParseURL(redisURL)
if err != nil {
log.Fatalf("Could not parse redis URL: %s", err)
}
if err := pool.Retry(func() error {
redisClient = redis.NewClient(opts)
return redisClient.Ping(context.Background()).Err()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
code := m.Run()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}
-235
View File
@@ -1,235 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package producer
import (
"context"
"github.com/absmach/supermq/bootstrap"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/events"
)
var _ bootstrap.Service = (*eventStore)(nil)
type eventStore struct {
events.Publisher
svc bootstrap.Service
}
// NewEventStoreMiddleware returns wrapper around bootstrap service that sends
// events to event store.
func NewEventStoreMiddleware(svc bootstrap.Service, publisher events.Publisher) bootstrap.Service {
return &eventStore{
svc: svc,
Publisher: publisher,
}
}
func (es *eventStore) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
saved, err := es.svc.Add(ctx, session, token, cfg)
if err != nil {
return saved, err
}
ev := configEvent{
saved, configCreate,
}
if err := es.Publish(ctx, ev); err != nil {
return saved, err
}
return saved, err
}
func (es *eventStore) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
cfg, err := es.svc.View(ctx, session, id)
if err != nil {
return cfg, err
}
ev := configEvent{
cfg, configView,
}
if err := es.Publish(ctx, ev); err != nil {
return cfg, err
}
return cfg, err
}
func (es *eventStore) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
if err := es.svc.Update(ctx, session, cfg); err != nil {
return err
}
ev := configEvent{
cfg, configUpdate,
}
return es.Publish(ctx, ev)
}
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
cfg, err := es.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
if err != nil {
return cfg, err
}
ev := updateCertEvent{
clientID: clientID,
clientCert: clientCert,
clientKey: clientKey,
caCert: caCert,
}
if err := es.Publish(ctx, ev); err != nil {
return cfg, err
}
return cfg, nil
}
func (es *eventStore) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
if err := es.svc.UpdateConnections(ctx, session, token, id, connections); err != nil {
return err
}
ev := updateConnectionsEvent{
mgClient: id,
mgChannels: connections,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
bp, err := es.svc.List(ctx, session, filter, offset, limit)
if err != nil {
return bp, err
}
ev := listConfigsEvent{
offset: offset,
limit: limit,
fullMatch: filter.FullMatch,
partialMatch: filter.PartialMatch,
}
if err := es.Publish(ctx, ev); err != nil {
return bp, err
}
return bp, nil
}
func (es *eventStore) Remove(ctx context.Context, session smqauthn.Session, id string) error {
if err := es.svc.Remove(ctx, session, id); err != nil {
return err
}
ev := removeConfigEvent{
client: id,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
ev := bootstrapEvent{
cfg,
externalID,
true,
}
if err != nil {
ev.success = false
}
if err := es.Publish(ctx, ev); err != nil {
return cfg, err
}
return cfg, err
}
func (es *eventStore) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
if err := es.svc.ChangeState(ctx, session, token, id, state); err != nil {
return err
}
ev := changeStateEvent{
mgClient: id,
state: state,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
return err
}
ev := removeHandlerEvent{
id: id,
operation: configHandlerRemove,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
return err
}
ev := removeHandlerEvent{
id: id,
operation: channelHandlerRemove,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
return err
}
ev := updateChannelHandlerEvent{
channel,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := es.svc.ConnectClientHandler(ctx, channelID, clientID); err != nil {
return err
}
ev := connectClientEvent{
clientID: clientID,
channelID: channelID,
}
return es.Publish(ctx, ev)
}
func (es *eventStore) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := es.svc.DisconnectClientHandler(ctx, channelID, clientID); err != nil {
return err
}
ev := disconnectClientEvent{
clientID: clientID,
channelID: channelID,
}
return es.Publish(ctx, ev)
}
File diff suppressed because it is too large Load Diff
-145
View File
@@ -1,145 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"github.com/absmach/supermq/bootstrap"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/authz"
smqauthz "github.com/absmach/supermq/pkg/authz"
"github.com/absmach/supermq/pkg/policies"
)
var _ bootstrap.Service = (*authorizationMiddleware)(nil)
type authorizationMiddleware struct {
svc bootstrap.Service
authz smqauthz.Authorization
}
// AuthorizationMiddleware adds authorization to the clients service.
func AuthorizationMiddleware(svc bootstrap.Service, authz smqauthz.Authorization) bootstrap.Service {
return &authorizationMiddleware{
svc: svc,
authz: authz,
}
}
func (am *authorizationMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil {
return bootstrap.Config{}, err
}
return am.svc.Add(ctx, session, token, cfg)
}
func (am *authorizationMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.ClientType, id); err != nil {
return bootstrap.Config{}, err
}
return am.svc.View(ctx, session, id)
}
func (am *authorizationMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ClientType, cfg.ClientID); err != nil {
return err
}
return am.svc.Update(ctx, session, cfg)
}
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ClientType, clientID); err != nil {
return bootstrap.Config{}, err
}
return am.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
}
func (am *authorizationMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ClientType, id); err != nil {
return err
}
return am.svc.UpdateConnections(ctx, session, token, id, connections)
}
func (am *authorizationMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
if err := am.checkSuperAdmin(ctx, session.DomainUserID); err == nil {
session.SuperAdmin = true
}
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err == nil {
session.SuperAdmin = true
}
return am.svc.List(ctx, session, filter, offset, limit)
}
func (am *authorizationMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ClientType, id); err != nil {
return err
}
return am.svc.Remove(ctx, session, id)
}
func (am *authorizationMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
return am.svc.Bootstrap(ctx, externalKey, externalID, secure)
}
func (am *authorizationMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
return am.svc.ChangeState(ctx, session, token, id, state)
}
func (am *authorizationMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
return am.svc.UpdateChannelHandler(ctx, channel)
}
func (am *authorizationMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
return am.svc.RemoveConfigHandler(ctx, id)
}
func (am *authorizationMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
return am.svc.RemoveChannelHandler(ctx, id)
}
func (am *authorizationMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
return am.svc.ConnectClientHandler(ctx, channelID, clientID)
}
func (am *authorizationMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
return am.svc.DisconnectClientHandler(ctx, channelID, clientID)
}
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error {
if err := am.authz.Authorize(ctx, authz.PolicyReq{
SubjectType: policies.UserType,
Subject: adminID,
Permission: policies.AdminPermission,
ObjectType: policies.PlatformType,
Object: policies.SuperMQObject,
}); err != nil {
return err
}
return nil
}
func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error {
req := authz.PolicyReq{
Domain: domain,
SubjectType: subjType,
SubjectKind: subjKind,
Subject: subj,
Permission: perm,
ObjectType: objType,
Object: obj,
}
if err := am.authz.Authorize(ctx, req); err != nil {
return err
}
return nil
}
-295
View File
@@ -1,295 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
//go:build !test
package middleware
import (
"context"
"log/slog"
"time"
"github.com/absmach/supermq/bootstrap"
smqauthn "github.com/absmach/supermq/pkg/authn"
)
var _ bootstrap.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger *slog.Logger
svc bootstrap.Service
}
// LoggingMiddleware adds logging facilities to the bootstrap service.
func LoggingMiddleware(svc bootstrap.Service, logger *slog.Logger) bootstrap.Service {
return &loggingMiddleware{logger, svc}
}
// Add logs the add request. It logs the client ID and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", saved.ClientID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Add new bootstrap failed", args...)
return
}
lm.logger.Info("Add new bootstrap completed successfully", args...)
}(time.Now())
return lm.svc.Add(ctx, session, token, cfg)
}
// View logs the view request. It logs the client ID and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("View client config failed", args...)
return
}
lm.logger.Info("View client config completed successfully", args...)
}(time.Now())
return lm.svc.View(ctx, session, id)
}
// Update logs the update request. It logs bootstrap client ID and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.Group("config",
slog.String("client_id", cfg.ClientID),
slog.String("name", cfg.Name),
),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Update bootstrap config failed", args...)
return
}
lm.logger.Info("Update bootstrap config completed successfully", args...)
}(time.Now())
return lm.svc.Update(ctx, session, cfg)
}
// UpdateCert logs the update_cert request. It logs client ID and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", cfg.ClientID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Update bootstrap config certificate failed", args...)
return
}
lm.logger.Info("Update bootstrap config certificate completed successfully", args...)
}(time.Now())
return lm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
}
// UpdateConnections logs the update_connections request. It logs bootstrap ID and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", id),
slog.Any("connections", connections),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Update config connections failed", args...)
return
}
lm.logger.Info("Update config connections completed successfully", args...)
}(time.Now())
return lm.svc.UpdateConnections(ctx, session, token, id, connections)
}
// List logs the list request. It logs offset, limit and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.Group("page",
slog.Any("filter", filter),
slog.Uint64("offset", offset),
slog.Uint64("limit", limit),
slog.Uint64("total", res.Total),
),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("List configs failed", args...)
return
}
lm.logger.Info("List configs completed successfully", args...)
}(time.Now())
return lm.svc.List(ctx, session, filter, offset, limit)
}
// Remove logs the remove request. It logs bootstrap ID and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Remove bootstrap config failed", args...)
return
}
lm.logger.Info("Remove bootstrap config completed successfully", args...)
}(time.Now())
return lm.svc.Remove(ctx, session, id)
}
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("external_id", externalID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("View bootstrap config failed", args...)
return
}
lm.logger.Info("View bootstrap completed successfully", args...)
}(time.Now())
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
}
func (lm *loggingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("id", id),
slog.Any("state", state),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Change client state failed", args...)
return
}
lm.logger.Info("Change client state completed successfully", args...)
}(time.Now())
return lm.svc.ChangeState(ctx, session, token, id, state)
}
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.Group("channel",
slog.String("id", channel.ID),
slog.String("name", channel.Name),
slog.Any("metadata", channel.Metadata),
),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Update channel handler failed", args...)
return
}
lm.logger.Info("Update channel handler completed successfully", args...)
}(time.Now())
return lm.svc.UpdateChannelHandler(ctx, channel)
}
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("config_id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Remove config handler failed", args...)
return
}
lm.logger.Info("Remove config handler completed successfully", args...)
}(time.Now())
return lm.svc.RemoveConfigHandler(ctx, id)
}
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("channel_id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Remove channel handler failed", args...)
return
}
lm.logger.Info("Remove channel handler completed successfully", args...)
}(time.Now())
return lm.svc.RemoveChannelHandler(ctx, id)
}
func (lm *loggingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("channel_id", channelID),
slog.String("client_id", clientID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Connect client handler failed", args...)
return
}
lm.logger.Info("Connect client handler completed successfully", args...)
}(time.Now())
return lm.svc.ConnectClientHandler(ctx, channelID, clientID)
}
func (lm *loggingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("channel_id", channelID),
slog.String("client_id", clientID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Disconnect client handler failed", args...)
return
}
lm.logger.Info("Disconnect client handler completed successfully", args...)
}(time.Now())
return lm.svc.DisconnectClientHandler(ctx, channelID, clientID)
}
-172
View File
@@ -1,172 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
//go:build !test
package middleware
import (
"context"
"time"
"github.com/absmach/supermq/bootstrap"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/go-kit/kit/metrics"
)
var _ bootstrap.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc bootstrap.Service
}
// MetricsMiddleware instruments core service by tracking request count and latency.
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
// Add instruments Add method with metrics.
func (mm *metricsMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "add").Add(1)
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Add(ctx, session, token, cfg)
}
// View instruments View method with metrics.
func (mm *metricsMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "view").Add(1)
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.View(ctx, session, id)
}
// Update instruments Update method with metrics.
func (mm *metricsMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update").Add(1)
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Update(ctx, session, cfg)
}
// UpdateCert instruments UpdateCert method with metrics.
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_cert").Add(1)
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
}
// UpdateConnections instruments UpdateConnections method with metrics.
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_connections").Add(1)
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateConnections(ctx, session, token, id, connections)
}
// List instruments List method with metrics.
func (mm *metricsMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "list").Add(1)
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.List(ctx, session, filter, offset, limit)
}
// Remove instruments Remove method with metrics.
func (mm *metricsMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove").Add(1)
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Remove(ctx, session, id)
}
// Bootstrap instruments Bootstrap method with metrics.
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "bootstrap").Add(1)
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
}
// ChangeState instruments ChangeState method with metrics.
func (mm *metricsMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "change_state").Add(1)
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ChangeState(ctx, session, token, id, state)
}
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_channel").Add(1)
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateChannelHandler(ctx, channel)
}
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove_config").Add(1)
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveConfigHandler(ctx, id)
}
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove_channel").Add(1)
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveChannelHandler(ctx, id)
}
// ConnectClientHandler instruments ConnectClientHandler method with metrics.
func (mm *metricsMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "connect_client_handler").Add(1)
mm.latency.With("method", "connect_client_handler").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ConnectClientHandler(ctx, channelID, clientID)
}
// DisconnectClientHandler instruments DisconnectClientHandler method with metrics.
func (mm *metricsMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "disconnect_client_handler").Add(1)
mm.latency.With("method", "disconnect_client_handler").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.DisconnectClientHandler(ctx, channelID, clientID)
}
-59
View File
@@ -1,59 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Copyright (c) Abstract Machines
package mocks
import (
bootstrap "github.com/absmach/supermq/bootstrap"
mock "github.com/stretchr/testify/mock"
)
// ConfigReader is an autogenerated mock type for the ConfigReader type
type ConfigReader struct {
mock.Mock
}
// ReadConfig provides a mock function with given fields: _a0, _a1
func (_m *ConfigReader) ReadConfig(_a0 bootstrap.Config, _a1 bool) (interface{}, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for ReadConfig")
}
var r0 interface{}
var r1 error
if rf, ok := ret.Get(0).(func(bootstrap.Config, bool) (interface{}, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(bootstrap.Config, bool) interface{}); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(interface{})
}
}
if rf, ok := ret.Get(1).(func(bootstrap.Config, bool) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewConfigReader creates a new instance of ConfigReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewConfigReader(t interface {
mock.TestingT
Cleanup(func())
}) *ConfigReader {
mock := &ConfigReader{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
-354
View File
@@ -1,354 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Copyright (c) Abstract Machines
package mocks
import (
context "context"
bootstrap "github.com/absmach/supermq/bootstrap"
mock "github.com/stretchr/testify/mock"
)
// ConfigRepository is an autogenerated mock type for the ConfigRepository type
type ConfigRepository struct {
mock.Mock
}
// ChangeState provides a mock function with given fields: ctx, domainID, id, state
func (_m *ConfigRepository) ChangeState(ctx context.Context, domainID string, id string, state bootstrap.State) error {
ret := _m.Called(ctx, domainID, id, state)
if len(ret) == 0 {
panic("no return value specified for ChangeState")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, bootstrap.State) error); ok {
r0 = rf(ctx, domainID, id, state)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConnectClient provides a mock function with given fields: ctx, channelID, clientID
func (_m *ConfigRepository) ConnectClient(ctx context.Context, channelID string, clientID string) error {
ret := _m.Called(ctx, channelID, clientID)
if len(ret) == 0 {
panic("no return value specified for ConnectClient")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, channelID, clientID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DisconnectClient provides a mock function with given fields: ctx, channelID, clientID
func (_m *ConfigRepository) DisconnectClient(ctx context.Context, channelID string, clientID string) error {
ret := _m.Called(ctx, channelID, clientID)
if len(ret) == 0 {
panic("no return value specified for DisconnectClient")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, channelID, clientID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ListExisting provides a mock function with given fields: ctx, domainID, ids
func (_m *ConfigRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
ret := _m.Called(ctx, domainID, ids)
if len(ret) == 0 {
panic("no return value specified for ListExisting")
}
var r0 []bootstrap.Channel
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, []string) ([]bootstrap.Channel, error)); ok {
return rf(ctx, domainID, ids)
}
if rf, ok := ret.Get(0).(func(context.Context, string, []string) []bootstrap.Channel); ok {
r0 = rf(ctx, domainID, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]bootstrap.Channel)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok {
r1 = rf(ctx, domainID, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Remove provides a mock function with given fields: ctx, domainID, id
func (_m *ConfigRepository) Remove(ctx context.Context, domainID string, id string) error {
ret := _m.Called(ctx, domainID, id)
if len(ret) == 0 {
panic("no return value specified for Remove")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, domainID, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveChannel provides a mock function with given fields: ctx, id
func (_m *ConfigRepository) RemoveChannel(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveChannel")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveClient provides a mock function with given fields: ctx, id
func (_m *ConfigRepository) RemoveClient(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveClient")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// RetrieveAll provides a mock function with given fields: ctx, domainID, clientIDs, filter, offset, limit
func (_m *ConfigRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage {
ret := _m.Called(ctx, domainID, clientIDs, filter, offset, limit)
if len(ret) == 0 {
panic("no return value specified for RetrieveAll")
}
var r0 bootstrap.ConfigsPage
if rf, ok := ret.Get(0).(func(context.Context, string, []string, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
r0 = rf(ctx, domainID, clientIDs, filter, offset, limit)
} else {
r0 = ret.Get(0).(bootstrap.ConfigsPage)
}
return r0
}
// RetrieveByExternalID provides a mock function with given fields: ctx, externalID
func (_m *ConfigRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
ret := _m.Called(ctx, externalID)
if len(ret) == 0 {
panic("no return value specified for RetrieveByExternalID")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (bootstrap.Config, error)); ok {
return rf(ctx, externalID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) bootstrap.Config); ok {
r0 = rf(ctx, externalID)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, externalID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RetrieveByID provides a mock function with given fields: ctx, domainID, id
func (_m *ConfigRepository) RetrieveByID(ctx context.Context, domainID string, id string) (bootstrap.Config, error) {
ret := _m.Called(ctx, domainID, id)
if len(ret) == 0 {
panic("no return value specified for RetrieveByID")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (bootstrap.Config, error)); ok {
return rf(ctx, domainID, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) bootstrap.Config); ok {
r0 = rf(ctx, domainID, id)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, domainID, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: ctx, cfg, chsConnIDs
func (_m *ConfigRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (string, error) {
ret := _m.Called(ctx, cfg, chsConnIDs)
if len(ret) == 0 {
panic("no return value specified for Save")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Config, []string) (string, error)); ok {
return rf(ctx, cfg, chsConnIDs)
}
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Config, []string) string); ok {
r0 = rf(ctx, cfg, chsConnIDs)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, bootstrap.Config, []string) error); ok {
r1 = rf(ctx, cfg, chsConnIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: ctx, cfg
func (_m *ConfigRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
ret := _m.Called(ctx, cfg)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Config) error); ok {
r0 = rf(ctx, cfg)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateCert provides a mock function with given fields: ctx, domainID, clientID, clientCert, clientKey, caCert
func (_m *ConfigRepository) UpdateCert(ctx context.Context, domainID string, clientID string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
ret := _m.Called(ctx, domainID, clientID, clientCert, clientKey, caCert)
if len(ret) == 0 {
panic("no return value specified for UpdateCert")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) (bootstrap.Config, error)); ok {
return rf(ctx, domainID, clientID, clientCert, clientKey, caCert)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) bootstrap.Config); ok {
r0 = rf(ctx, domainID, clientID, clientCert, clientKey, caCert)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, string) error); ok {
r1 = rf(ctx, domainID, clientID, clientCert, clientKey, caCert)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateChannel provides a mock function with given fields: ctx, c
func (_m *ConfigRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
ret := _m.Called(ctx, c)
if len(ret) == 0 {
panic("no return value specified for UpdateChannel")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Channel) error); ok {
r0 = rf(ctx, c)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateConnections provides a mock function with given fields: ctx, domainID, id, channels, connections
func (_m *ConfigRepository) UpdateConnections(ctx context.Context, domainID string, id string, channels []bootstrap.Channel, connections []string) error {
ret := _m.Called(ctx, domainID, id, channels, connections)
if len(ret) == 0 {
panic("no return value specified for UpdateConnections")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, []bootstrap.Channel, []string) error); ok {
r0 = rf(ctx, domainID, id, channels, connections)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewConfigRepository creates a new instance of ConfigRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewConfigRepository(t interface {
mock.TestingT
Cleanup(func())
}) *ConfigRepository {
mock := &ConfigRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package mocks contains mocks for testing purposes.
package mocks
-335
View File
@@ -1,335 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Copyright (c) Abstract Machines
package mocks
import (
bootstrap "github.com/absmach/supermq/bootstrap"
authn "github.com/absmach/supermq/pkg/authn"
context "context"
mock "github.com/stretchr/testify/mock"
)
// Service is an autogenerated mock type for the Service type
type Service struct {
mock.Mock
}
// Add provides a mock function with given fields: ctx, session, token, cfg
func (_m *Service) Add(ctx context.Context, session authn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
ret := _m.Called(ctx, session, token, cfg)
if len(ret) == 0 {
panic("no return value specified for Add")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, bootstrap.Config) (bootstrap.Config, error)); ok {
return rf(ctx, session, token, cfg)
}
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, bootstrap.Config) bootstrap.Config); ok {
r0 = rf(ctx, session, token, cfg)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, bootstrap.Config) error); ok {
r1 = rf(ctx, session, token, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Bootstrap provides a mock function with given fields: ctx, externalKey, externalID, secure
func (_m *Service) Bootstrap(ctx context.Context, externalKey string, externalID string, secure bool) (bootstrap.Config, error) {
ret := _m.Called(ctx, externalKey, externalID, secure)
if len(ret) == 0 {
panic("no return value specified for Bootstrap")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) (bootstrap.Config, error)); ok {
return rf(ctx, externalKey, externalID, secure)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) bootstrap.Config); ok {
r0 = rf(ctx, externalKey, externalID, secure)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, bool) error); ok {
r1 = rf(ctx, externalKey, externalID, secure)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChangeState provides a mock function with given fields: ctx, session, token, id, state
func (_m *Service) ChangeState(ctx context.Context, session authn.Session, token string, id string, state bootstrap.State) error {
ret := _m.Called(ctx, session, token, id, state)
if len(ret) == 0 {
panic("no return value specified for ChangeState")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, bootstrap.State) error); ok {
r0 = rf(ctx, session, token, id, state)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConnectClientHandler provides a mock function with given fields: ctx, channelID, clientID
func (_m *Service) ConnectClientHandler(ctx context.Context, channelID string, clientID string) error {
ret := _m.Called(ctx, channelID, clientID)
if len(ret) == 0 {
panic("no return value specified for ConnectClientHandler")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, channelID, clientID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DisconnectClientHandler provides a mock function with given fields: ctx, channelID, clientID
func (_m *Service) DisconnectClientHandler(ctx context.Context, channelID string, clientID string) error {
ret := _m.Called(ctx, channelID, clientID)
if len(ret) == 0 {
panic("no return value specified for DisconnectClientHandler")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, channelID, clientID)
} else {
r0 = ret.Error(0)
}
return r0
}
// List provides a mock function with given fields: ctx, session, filter, offset, limit
func (_m *Service) List(ctx context.Context, session authn.Session, filter bootstrap.Filter, offset uint64, limit uint64) (bootstrap.ConfigsPage, error) {
ret := _m.Called(ctx, session, filter, offset, limit)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 bootstrap.ConfigsPage
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, bootstrap.Filter, uint64, uint64) (bootstrap.ConfigsPage, error)); ok {
return rf(ctx, session, filter, offset, limit)
}
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
r0 = rf(ctx, session, filter, offset, limit)
} else {
r0 = ret.Get(0).(bootstrap.ConfigsPage)
}
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, bootstrap.Filter, uint64, uint64) error); ok {
r1 = rf(ctx, session, filter, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Remove provides a mock function with given fields: ctx, session, id
func (_m *Service) Remove(ctx context.Context, session authn.Session, id string) error {
ret := _m.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for Remove")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok {
r0 = rf(ctx, session, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveChannelHandler provides a mock function with given fields: ctx, id
func (_m *Service) RemoveChannelHandler(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveChannelHandler")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveConfigHandler provides a mock function with given fields: ctx, id
func (_m *Service) RemoveConfigHandler(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveConfigHandler")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: ctx, session, cfg
func (_m *Service) Update(ctx context.Context, session authn.Session, cfg bootstrap.Config) error {
ret := _m.Called(ctx, session, cfg)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, bootstrap.Config) error); ok {
r0 = rf(ctx, session, cfg)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateCert provides a mock function with given fields: ctx, session, clientID, clientCert, clientKey, caCert
func (_m *Service) UpdateCert(ctx context.Context, session authn.Session, clientID string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
ret := _m.Called(ctx, session, clientID, clientCert, clientKey, caCert)
if len(ret) == 0 {
panic("no return value specified for UpdateCert")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string, string) (bootstrap.Config, error)); ok {
return rf(ctx, session, clientID, clientCert, clientKey, caCert)
}
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string, string) bootstrap.Config); ok {
r0 = rf(ctx, session, clientID, clientCert, clientKey, caCert)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string, string) error); ok {
r1 = rf(ctx, session, clientID, clientCert, clientKey, caCert)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateChannelHandler provides a mock function with given fields: ctx, channel
func (_m *Service) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
ret := _m.Called(ctx, channel)
if len(ret) == 0 {
panic("no return value specified for UpdateChannelHandler")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Channel) error); ok {
r0 = rf(ctx, channel)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateConnections provides a mock function with given fields: ctx, session, token, id, connections
func (_m *Service) UpdateConnections(ctx context.Context, session authn.Session, token string, id string, connections []string) error {
ret := _m.Called(ctx, session, token, id, connections)
if len(ret) == 0 {
panic("no return value specified for UpdateConnections")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok {
r0 = rf(ctx, session, token, id, connections)
} else {
r0 = ret.Error(0)
}
return r0
}
// View provides a mock function with given fields: ctx, session, id
func (_m *Service) View(ctx context.Context, session authn.Session, id string) (bootstrap.Config, error) {
ret := _m.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for View")
}
var r0 bootstrap.Config
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (bootstrap.Config, error)); ok {
return rf(ctx, session, id)
}
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) bootstrap.Config); ok {
r0 = rf(ctx, session, id)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
r1 = rf(ctx, session, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewService(t interface {
mock.TestingT
Cleanup(func())
}) *Service {
mock := &Service{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
-778
View File
@@ -1,778 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/clients"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
"github.com/absmach/supermq/pkg/postgres"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgtype"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jmoiron/sqlx"
)
var (
errSaveChannels = errors.New("failed to insert channels to database")
errSaveConnections = errors.New("failed to insert connections to database")
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
errConnectClient = errors.New("failed to connect client in bootstrap configuration in database")
errDisconnectClient = errors.New("failed to disconnect client in bootstrap configuration in database")
)
const cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
SELECT channel_id FROM connections c WHERE ch.magistrala_channel = c.channel_id);`
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
type configRepository struct {
db postgres.Database
log *slog.Logger
}
// NewConfigRepository instantiates a PostgreSQL implementation of config
// repository.
func NewConfigRepository(db postgres.Database, log *slog.Logger) bootstrap.ConfigRepository {
return &configRepository{db: db, log: log}
}
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (clientID string, err error) {
q := `INSERT INTO configs (magistrala_client, domain_id, name, client_cert, client_key, ca_cert, magistrala_secret, external_id, external_key, content, state)
VALUES (:magistrala_client, :domain_id, :name, :client_cert, :client_key, :ca_cert, :magistrala_secret, :external_id, :external_key, :content, :state)`
tx, err := cr.db.BeginTxx(ctx, nil)
if err != nil {
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
}
dbcfg := toDBConfig(cfg)
defer func() {
if err != nil {
err = cr.rollback("Save method", err, tx)
}
}()
if _, err := tx.NamedExec(q, dbcfg); err != nil {
switch pgErr := err.(type) {
case *pgconn.PgError:
if pgErr.Code == pgerrcode.UniqueViolation {
err = repoerr.ErrConflict
}
}
return "", err
}
if err := insertChannels(cfg.DomainID, cfg.Channels, tx); err != nil {
return "", errors.Wrap(errSaveChannels, err)
}
if err := insertConnections(ctx, cfg, chsConnIDs, tx); err != nil {
return "", errors.Wrap(errSaveConnections, err)
}
if commitErr := tx.Commit(); commitErr != nil {
return "", commitErr
}
return cfg.ClientID, nil
}
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state, client_cert, ca_cert
FROM configs
WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
dbcfg := dbConfig{
ClientID: id,
DomainID: domainID,
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
if err == sql.ErrNoRows {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, err)
}
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
if ok := row.Next(); !ok {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
}
if err := row.StructScan(&dbcfg); err != nil {
return bootstrap.Config{}, err
}
q = `SELECT magistrala_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer rows.Close()
chans := []bootstrap.Channel{}
for rows.Next() {
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
dbch.DomainID = nullString(dbcfg.DomainID)
ch, err := toChannel(dbch)
if err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
chans = append(chans, ch)
}
cfg := toConfig(dbcfg)
cfg.Channels = chans
return cfg, nil
}
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
search, params := buildRetrieveQueryParams(domainID, clientIDs, filter)
n := len(params)
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state
FROM configs %s ORDER BY magistrala_client LIMIT $%d OFFSET $%d`
q = fmt.Sprintf(q, search, n+1, n+2)
rows, err := cr.db.QueryContext(ctx, q, append(params, limit, offset)...)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
return bootstrap.ConfigsPage{}
}
defer rows.Close()
var name, content sql.NullString
configs := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{DomainID: domainID}
if err := rows.Scan(&c.ClientID, &c.ClientSecret, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
c.Name = name.String
c.Content = content.String
configs = append(configs, c)
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
var total uint64
if err := cr.db.QueryRowxContext(ctx, q, params...).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
return bootstrap.ConfigsPage{}
}
return bootstrap.ConfigsPage{
Total: total,
Limit: limit,
Offset: offset,
Configs: configs,
}
}
func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
q := `SELECT magistrala_client, magistrala_secret, external_key, domain_id, name, client_cert, client_key, ca_cert, content, state
FROM configs
WHERE external_id = :external_id`
dbcfg := dbConfig{
ExternalID: externalID,
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
if err == sql.ErrNoRows {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, err)
}
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
if ok := row.Next(); !ok {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
}
if err := row.StructScan(&dbcfg); err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
q = `SELECT magistrala_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer rows.Close()
channels := []bootstrap.Channel{}
for rows.Next() {
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
channels = append(channels, ch)
}
cfg := toConfig(dbcfg)
cfg.Channels = channels
return cfg, nil
}
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
q := `UPDATE configs SET name = :name, content = :content WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id `
dbcfg := dbConfig{
Name: nullString(cfg.Name),
Content: nullString(cfg.Content),
ClientID: cfg.ClientID,
DomainID: cfg.DomainID,
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if cnt == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
RETURNING magistrala_client, client_cert, client_key, ca_cert`
dbcfg := dbConfig{
ClientID: clientID,
ClientCert: nullString(clientCert),
DomainID: domainID,
ClientKey: nullString(clientKey),
CaCert: nullString(caCert),
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
defer row.Close()
if ok := row.Next(); !ok {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
}
if err := row.StructScan(&dbcfg); err != nil {
return bootstrap.Config{}, err
}
return toConfig(dbcfg), nil
}
func (cr configRepository) UpdateConnections(ctx context.Context, domainID, id string, channels []bootstrap.Channel, connections []string) (err error) {
tx, err := cr.db.BeginTxx(ctx, nil)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
defer func() {
if err != nil {
err = cr.rollback("UpdateConnections method", err, tx)
} else {
if commitErr := tx.Commit(); commitErr != nil {
err = commitErr
}
}
}()
if err = insertChannels(domainID, channels, tx); err != nil {
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
return err
}
if err = updateConnections(domainID, id, connections, tx); err != nil {
if e, ok := err.(*pgconn.PgError); ok {
if e.Code == pgerrcode.ForeignKeyViolation {
err = repoerr.ErrNotFound
}
}
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
return err
}
return nil
}
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
q := `DELETE FROM configs WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
dbcfg := dbConfig{
ClientID: id,
DomainID: domainID,
}
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
return nil
}
func (cr configRepository) ChangeState(ctx context.Context, domainID, id string, state bootstrap.State) error {
q := `UPDATE configs SET state = :state WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id;`
dbcfg := dbConfig{
ClientID: id,
State: state,
DomainID: domainID,
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if cnt == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
var channels []bootstrap.Channel
if len(ids) == 0 {
return channels, nil
}
var chans pgtype.TextArray
if err := chans.Set(ids); err != nil {
return []bootstrap.Channel{}, err
}
q := "SELECT magistrala_channel, name, metadata FROM channels WHERE domain_id = $1 AND magistrala_channel = ANY ($2)"
rows, err := cr.db.QueryxContext(ctx, q, domainID, chans)
if err != nil {
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
for rows.Next() {
var dbch dbChannel
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return []bootstrap.Channel{}, err
}
channels = append(channels, ch)
}
return channels, nil
}
func (cr configRepository) RemoveClient(ctx context.Context, id string) error {
q := `DELETE FROM configs WHERE magistrala_client = $1`
_, err := cr.db.ExecContext(ctx, q, id)
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
if err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
return nil
}
func (cr configRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
dbch, err := toDBChannel("", c)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
WHERE magistrala_channel = :magistrala_channel`
if _, err = cr.db.NamedExecContext(ctx, q, dbch); err != nil {
return errors.Wrap(errUpdateChannels, err)
}
return nil
}
func (cr configRepository) RemoveChannel(ctx context.Context, id string) error {
q := `DELETE FROM channels WHERE magistrala_channel = $1`
if _, err := cr.db.ExecContext(ctx, q, id); err != nil {
return errors.Wrap(errRemoveChannels, err)
}
return nil
}
func (cr configRepository) ConnectClient(ctx context.Context, channelID, clientID string) error {
q := `UPDATE configs SET state = $1
WHERE magistrala_client = $2
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
result, err := cr.db.ExecContext(ctx, q, bootstrap.Active, clientID, channelID)
if err != nil {
return errors.Wrap(errConnectClient, err)
}
if rows, _ := result.RowsAffected(); rows == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) DisconnectClient(ctx context.Context, channelID, clientID string) error {
q := `UPDATE configs SET state = $1
WHERE magistrala_client = $2
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
_, err := cr.db.ExecContext(ctx, q, bootstrap.Inactive, clientID, channelID)
if err != nil {
return errors.Wrap(errDisconnectClient, err)
}
return nil
}
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []interface{}) {
params := []interface{}{}
queries := []string{}
if len(clientIDs) != 0 {
queries = append(queries, fmt.Sprintf("magistrala_client IN ('%s')", strings.Join(clientIDs, "','")))
} else if domainID != "" {
params = append(params, domainID)
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
}
// Adjust the starting point for placeholders based on the current length of params
counter := len(params) + 1
for k, v := range filter.FullMatch {
params = append(params, v)
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
counter++
}
for k, v := range filter.PartialMatch {
params = append(params, v)
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
counter++
}
if len(queries) > 0 {
return "WHERE " + strings.Join(queries, " AND "), params
}
return "", params
}
func (cr configRepository) rollback(content string, defErr error, tx *sqlx.Tx) error {
if err := tx.Rollback(); err != nil {
return errors.Wrap(defErr, errors.Wrap(errors.New("failed to rollback at "+content), err))
}
return defErr
}
func insertChannels(domainID string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
if len(channels) == 0 {
return nil
}
var chans []dbChannel
for _, ch := range channels {
dbch, err := toDBChannel(domainID, ch)
if err != nil {
return err
}
chans = append(chans, dbch)
}
q := `INSERT INTO channels (magistrala_channel, domain_id, name, metadata, parent_id, description, created_at, updated_at, updated_by, status)
VALUES (:magistrala_channel, :domain_id, :name, :metadata, :parent_id, :description, :created_at, :updated_at, :updated_by, :status)`
if _, err := tx.NamedExec(q, chans); err != nil {
e := err
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
e = repoerr.ErrConflict
}
return e
}
return nil
}
func insertConnections(_ context.Context, cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
if len(connections) == 0 {
return nil
}
q := `INSERT INTO connections (config_id, channel_id, domain_id)
VALUES (:config_id, :channel_id, :domain_id)`
conns := []dbConnection{}
for _, conn := range connections {
dbconn := dbConnection{
Config: cfg.ClientID,
Channel: conn,
DomainID: cfg.DomainID,
}
conns = append(conns, dbconn)
}
_, err := tx.NamedExec(q, conns)
return err
}
func updateConnections(domainID, id string, connections []string, tx *sqlx.Tx) error {
if len(connections) == 0 {
return nil
}
q := `DELETE FROM connections
WHERE config_id = $1 AND domain_id = $2
AND channel_id NOT IN ($3)`
var conn pgtype.TextArray
if err := conn.Set(connections); err != nil {
return err
}
res, err := tx.Exec(q, id, domainID, conn)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
q = `INSERT INTO connections (config_id, channel_id, domain_id)
VALUES (:config_id, :channel_id, :domain_id)`
conns := []dbConnection{}
for _, conn := range connections {
dbconn := dbConnection{
Config: id,
Channel: conn,
DomainID: domainID,
}
conns = append(conns, dbconn)
}
if _, err := tx.NamedExec(q, conns); err != nil {
return err
}
if cnt == 0 {
return nil
}
_, err = tx.Exec(cleanupQuery)
return err
}
func nullString(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{
String: s,
Valid: true,
}
}
func nullTime(t time.Time) sql.NullTime {
if t.IsZero() {
return sql.NullTime{}
}
return sql.NullTime{
Time: t,
Valid: true,
}
}
type dbConfig struct {
DomainID string `db:"domain_id"`
ClientID string `db:"magistrala_client"`
ClientSecret string `db:"magistrala_secret"`
Name sql.NullString `db:"name"`
ClientCert sql.NullString `db:"client_cert"`
ClientKey sql.NullString `db:"client_key"`
CaCert sql.NullString `db:"ca_cert"`
ExternalID string `db:"external_id"`
ExternalKey string `db:"external_key"`
Content sql.NullString `db:"content"`
State bootstrap.State `db:"state"`
}
func toDBConfig(cfg bootstrap.Config) dbConfig {
return dbConfig{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
DomainID: cfg.DomainID,
Name: nullString(cfg.Name),
ClientCert: nullString(cfg.ClientCert),
ClientKey: nullString(cfg.ClientKey),
CaCert: nullString(cfg.CACert),
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Content: nullString(cfg.Content),
State: cfg.State,
}
}
func toConfig(dbcfg dbConfig) bootstrap.Config {
cfg := bootstrap.Config{
ClientID: dbcfg.ClientID,
ClientSecret: dbcfg.ClientSecret,
DomainID: dbcfg.DomainID,
ExternalID: dbcfg.ExternalID,
ExternalKey: dbcfg.ExternalKey,
State: dbcfg.State,
}
if dbcfg.Name.Valid {
cfg.Name = dbcfg.Name.String
}
if dbcfg.Content.Valid {
cfg.Content = dbcfg.Content.String
}
if dbcfg.ClientCert.Valid {
cfg.ClientCert = dbcfg.ClientCert.String
}
if dbcfg.ClientKey.Valid {
cfg.ClientKey = dbcfg.ClientKey.String
}
if dbcfg.CaCert.Valid {
cfg.CACert = dbcfg.CaCert.String
}
return cfg
}
type dbChannel struct {
ID string `db:"magistrala_channel"`
Name sql.NullString `db:"name"`
DomainID sql.NullString `db:"domain_id"`
Metadata string `db:"metadata"`
Parent sql.NullString `db:"parent_id,omitempty"`
Description string `db:"description,omitempty"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
UpdatedBy sql.NullString `db:"updated_by,omitempty"`
Status clients.Status `db:"status"`
}
func toDBChannel(domainID string, ch bootstrap.Channel) (dbChannel, error) {
dbch := dbChannel{
ID: ch.ID,
Name: nullString(ch.Name),
DomainID: nullString(domainID),
Parent: nullString(ch.Parent),
Description: ch.Description,
CreatedAt: ch.CreatedAt,
UpdatedAt: nullTime(ch.UpdatedAt),
UpdatedBy: nullString(ch.UpdatedBy),
Status: ch.Status,
}
metadata, err := json.Marshal(ch.Metadata)
if err != nil {
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
dbch.Metadata = string(metadata)
return dbch, nil
}
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
ch := bootstrap.Channel{
ID: dbch.ID,
Description: dbch.Description,
CreatedAt: dbch.CreatedAt,
Status: dbch.Status,
}
if dbch.Name.Valid {
ch.Name = dbch.Name.String
}
if dbch.DomainID.Valid {
ch.DomainID = dbch.DomainID.String
}
if dbch.Parent.Valid {
ch.Parent = dbch.Parent.String
}
if dbch.UpdatedBy.Valid {
ch.UpdatedBy = dbch.UpdatedBy.String
}
if dbch.UpdatedAt.Valid {
ch.UpdatedAt = dbch.UpdatedAt.Time
}
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
return ch, nil
}
type dbConnection struct {
Config string `db:"config_id"`
Channel string `db:"channel_id"`
DomainID string `db:"domain_id"`
}
-913
View File
@@ -1,913 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"strconv"
"testing"
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/bootstrap/postgres"
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
"github.com/gofrs/uuid/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const numConfigs = 10
var (
config = bootstrap.Config{
ClientID: "smq-client",
ClientSecret: "smq-key",
ExternalID: "external-id",
ExternalKey: "external-key",
DomainID: testsutil.GenerateUUID(&testing.T{}),
Channels: []bootstrap.Channel{
{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
},
Content: "content",
State: bootstrap.Inactive,
}
channels = []string{"1", "2"}
)
func TestSave(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
diff := "different"
duplicateClient := config
duplicateClient.ExternalID = diff
duplicateClient.ClientSecret = diff
duplicateClient.Channels = []bootstrap.Channel{}
duplicateExternal := config
duplicateExternal.ClientID = diff
duplicateExternal.ClientSecret = diff
duplicateExternal.Channels = []bootstrap.Channel{}
duplicateChannels := config
duplicateChannels.ExternalID = diff
duplicateChannels.ClientSecret = diff
duplicateChannels.ClientID = diff
cases := []struct {
desc string
config bootstrap.Config
connections []string
err error
}{
{
desc: "save a config",
config: config,
connections: channels,
err: nil,
},
{
desc: "save config with same Client ID",
config: duplicateClient,
connections: nil,
err: repoerr.ErrConflict,
},
{
desc: "save config with same external ID",
config: duplicateExternal,
connections: nil,
err: repoerr.ErrConflict,
},
{
desc: "save config with same Channels",
config: duplicateChannels,
connections: channels,
err: repoerr.ErrConflict,
},
}
for _, tc := range cases {
id, err := repo.Save(context.Background(), tc.config, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, id, tc.config.ClientID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ClientID, id))
}
}
}
func TestRetrieveByID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
id, err := repo.Save(context.Background(), c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
nonexistentConfID, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
cases := []struct {
desc string
domainID string
id string
err error
}{
{
desc: "retrieve config",
domainID: c.DomainID,
id: id,
err: nil,
},
{
desc: "retrieve config with wrong domain ID ",
domainID: "2",
id: id,
err: repoerr.ErrNotFound,
},
{
desc: "retrieve a non-existing config",
domainID: c.DomainID,
id: nonexistentConfID.String(),
err: repoerr.ErrNotFound,
},
{
desc: "retrieve a config with invalid ID",
domainID: c.DomainID,
id: "invalid",
err: repoerr.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByID(context.Background(), tc.domainID, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveAll(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
clientIDs := make([]string, numConfigs)
for i := 0; i < numConfigs; i++ {
c := config
// Use UUID to prevent conflict errors.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ExternalID = uid.String()
c.Name = fmt.Sprintf("name %d", i)
c.ClientID = uid.String()
c.ClientSecret = uid.String()
clientIDs[i] = c.ClientID
if i%2 == 0 {
c.State = bootstrap.Active
}
if i > 0 {
c.Channels = nil
}
_, err = repo.Save(context.Background(), c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
cases := []struct {
desc string
domainID string
clientID []string
offset uint64
limit uint64
filter bootstrap.Filter
size int
}{
{
desc: "retrieve all configs",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
size: numConfigs,
},
{
desc: "retrieve a subset of configs",
domainID: config.DomainID,
clientID: []string{},
offset: 5,
limit: uint64(numConfigs - 5),
size: numConfigs - 5,
},
{
desc: "retrieve with wrong domain ID ",
domainID: "2",
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
size: 0,
},
{
desc: "retrieve all active configs ",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
size: numConfigs / 2,
},
{
desc: "retrieve all with partial match filter",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
size: 1,
},
{
desc: "retrieve search by name",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
size: 1,
},
{
desc: "retrieve by valid clientIDs",
domainID: config.DomainID,
clientID: clientIDs,
offset: 0,
limit: uint64(numConfigs),
size: 10,
},
{
desc: "retrieve by non-existing clientID",
domainID: config.DomainID,
clientID: []string{"non-existing"},
offset: 0,
limit: uint64(numConfigs),
size: 0,
},
}
for _, tc := range cases {
ret := repo.RetrieveAll(context.Background(), tc.domainID, tc.clientID, tc.filter, tc.offset, tc.limit)
size := len(ret.Configs)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
}
}
func TestRetrieveByExternalID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
externalID string
err error
}{
{
desc: "retrieve with invalid external ID",
externalID: strconv.Itoa(numConfigs + 1),
err: repoerr.ErrNotFound,
},
{
desc: "retrieve with external key",
externalID: c.ExternalID,
err: nil,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByExternalID(context.Background(), tc.externalID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdate(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
c.Name = "new name"
wrongDomainID := c
wrongDomainID.DomainID = "3"
cases := []struct {
desc string
id string
config bootstrap.Config
err error
}{
{
desc: "update with wrong domainID ",
config: wrongDomainID,
err: repoerr.ErrNotFound,
},
{
desc: "update a config",
config: c,
err: nil,
},
}
for _, tc := range cases {
err := repo.Update(context.Background(), tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateCert(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
c.Name = "new name"
wrongDomainID := c
wrongDomainID.DomainID = "3"
cases := []struct {
desc string
clientID string
domainID string
cert string
certKey string
ca string
expectedConfig bootstrap.Config
err error
}{
{
desc: "update with wrong domain ID ",
clientID: "",
cert: "cert",
certKey: "certKey",
ca: "",
domainID: wrongDomainID.DomainID,
expectedConfig: bootstrap.Config{},
err: repoerr.ErrNotFound,
},
{
desc: "update a config",
clientID: c.ClientID,
cert: "cert",
certKey: "certKey",
ca: "ca",
domainID: c.DomainID,
expectedConfig: bootstrap.Config{
ClientID: c.ClientID,
ClientCert: "cert",
CACert: "ca",
ClientKey: "certKey",
DomainID: c.DomainID,
},
err: nil,
},
}
for _, tc := range cases {
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.clientID, tc.cert, tc.certKey, tc.ca)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.expectedConfig, cfg, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.expectedConfig, cfg))
}
}
func TestUpdateConnections(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Use UUID to prevent conflicts.
uid, err = uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.Channels = []bootstrap.Channel{}
c2, err := repo.Save(context.Background(), c, []string{channels[0]})
assert.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
cases := []struct {
desc string
domainID string
id string
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "update connections of non-existing config",
domainID: config.DomainID,
id: "unknown",
channels: nil,
connections: []string{channels[1]},
err: repoerr.ErrNotFound,
},
{
desc: "update connections",
domainID: config.DomainID,
id: c.ClientID,
channels: nil,
connections: []string{channels[1]},
err: nil,
},
{
desc: "update connections with existing channels",
domainID: config.DomainID,
id: c2,
channels: nil,
connections: channels,
err: nil,
},
{
desc: "update connections no channels",
domainID: config.DomainID,
id: c.ClientID,
channels: nil,
connections: nil,
err: nil,
},
}
for _, tc := range cases {
err := repo.UpdateConnections(context.Background(), tc.domainID, tc.id, tc.channels, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemove(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
id, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Removal works the same for both existing and non-existing
// (removed) config
for i := 0; i < 2; i++ {
err := repo.Remove(context.Background(), c.DomainID, id)
assert.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
_, err = repo.RetrieveByID(context.Background(), c.DomainID, id)
assert.True(t, errors.Contains(err, repoerr.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, repoerr.ErrNotFound, err))
}
}
func TestChangeState(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
domainID string
id string
state bootstrap.State
err error
}{
{
desc: "change state with wrong domain ID ",
id: saved,
domainID: "2",
err: repoerr.ErrNotFound,
},
{
desc: "change state with wrong id",
id: "wrong",
domainID: c.DomainID,
err: repoerr.ErrNotFound,
},
{
desc: "change state to Active",
id: saved,
domainID: c.DomainID,
state: bootstrap.Active,
err: nil,
},
{
desc: "change state to Inactive",
id: saved,
domainID: c.DomainID,
state: bootstrap.Inactive,
err: nil,
},
}
for _, tc := range cases {
err := repo.ChangeState(context.Background(), tc.domainID, tc.id, tc.state)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListExisting(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
var chs []bootstrap.Channel
chs = append(chs, config.Channels...)
cases := []struct {
desc string
domainID string
connections []string
existing []bootstrap.Channel
}{
{
desc: "list all existing channels",
domainID: c.DomainID,
connections: channels,
existing: chs,
},
{
desc: "list a subset of existing channels",
domainID: c.DomainID,
connections: []string{channels[0], "5"},
existing: []bootstrap.Channel{chs[0]},
},
{
desc: "list a subset of existing channels empty",
domainID: c.DomainID,
connections: []string{"5", "6"},
existing: []bootstrap.Channel{},
},
}
for _, tc := range cases {
existing, err := repo.ListExisting(context.Background(), tc.domainID, tc.connections)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: Got non-matching elements.", tc.desc))
}
}
func TestRemoveClient(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
for i := 0; i < 2; i++ {
err := repo.RemoveClient(context.Background(), saved)
assert.Nil(t, err, fmt.Sprintf("an unexpected error occurred: %s\n", err))
}
}
func TestUpdateChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
id := c.Channels[0].ID
update := bootstrap.Channel{
ID: id,
Name: "update name",
Metadata: map[string]interface{}{"update": "metadata update"},
}
err = repo.UpdateChannel(context.Background(), update)
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
var retreved bootstrap.Channel
for _, c := range cfg.Channels {
if c.ID == id {
retreved = c
break
}
}
update.DomainID = retreved.DomainID
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
}
func TestRemoveChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = repo.RemoveChannel(context.Background(), c.Channels[0].ID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.NotContains(t, cfg.Channels, c.Channels[0], fmt.Sprintf("expected to remove channel %s from %s", c.Channels[0], cfg.Channels))
}
func TestConnectClient(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.State = bootstrap.Inactive
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
wrongID := testsutil.GenerateUUID(&testing.T{})
connectedClient := c
randomClient := c
randomClientID, _ := uuid.NewV4()
randomClient.ClientID = randomClientID.String()
emptyClient := c
emptyClient.ClientID = ""
cases := []struct {
desc string
domainID string
id string
state bootstrap.State
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "connect disconnected client",
domainID: c.DomainID,
id: saved,
state: bootstrap.Inactive,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "connect already connected client",
domainID: c.DomainID,
id: connectedClient.ClientID,
state: connectedClient.State,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "connect non-existent client",
domainID: c.DomainID,
id: wrongID,
channels: c.Channels,
connections: channels,
err: repoerr.ErrNotFound,
},
{
desc: "connect random client",
domainID: c.DomainID,
id: randomClient.ClientID,
channels: c.Channels,
connections: channels,
err: repoerr.ErrNotFound,
},
{
desc: "connect empty client",
domainID: c.DomainID,
id: emptyClient.ClientID,
channels: c.Channels,
connections: channels,
err: repoerr.ErrNotFound,
},
}
for _, tc := range cases {
for i, ch := range tc.channels {
if i == 0 {
err = repo.ConnectClient(context.Background(), ch.ID, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
} else {
_ = repo.ConnectClient(context.Background(), ch.ID, tc.id)
}
}
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
}
}
func TestDisconnectClient(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.State = bootstrap.Inactive
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
wrongID := testsutil.GenerateUUID(&testing.T{})
connectedClient := c
randomClient := c
randomClientID, _ := uuid.NewV4()
randomClient.ClientID = randomClientID.String()
emptyClient := c
emptyClient.ClientID = ""
cases := []struct {
desc string
domainID string
id string
state bootstrap.State
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "disconnect connected client",
domainID: c.DomainID,
id: connectedClient.ClientID,
state: connectedClient.State,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect already disconnected client",
domainID: c.DomainID,
id: saved,
state: bootstrap.Inactive,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect invalid client",
domainID: c.DomainID,
id: wrongID,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect random client",
domainID: c.DomainID,
id: randomClient.ClientID,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect empty client",
domainID: c.DomainID,
id: emptyClient.ClientID,
channels: c.Channels,
connections: channels,
err: nil,
},
}
for _, tc := range cases {
for _, ch := range tc.channels {
err = repo.DisconnectClient(context.Background(), ch.ID, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
}
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected to be inactive when a connection is removed from %s", cfg))
}
}
func deleteChannels(ctx context.Context, repo bootstrap.ConfigRepository) error {
for _, ch := range channels {
if err := repo.RemoveChannel(ctx, ch); err != nil {
return err
}
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package postgres contains repository implementations using PostgreSQL as
// the underlying database.
package postgres
-108
View File
@@ -1,108 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import migrate "github.com/rubenv/sql-migrate"
// Migration of bootstrap service.
func Migration() *migrate.MemoryMigrationSource {
return &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "configs_1",
Up: []string{
`CREATE TABLE IF NOT EXISTS configs (
mainflux_client TEXT UNIQUE NOT NULL,
owner VARCHAR(254),
name TEXT,
mainflux_key CHAR(36) UNIQUE NOT NULL,
external_id TEXT UNIQUE NOT NULL,
external_key TEXT NOT NULL,
content TEXT,
client_cert TEXT,
client_key TEXT,
ca_cert TEXT,
state BIGINT NOT NULL,
PRIMARY KEY (mainflux_client, owner)
)`,
`CREATE TABLE IF NOT EXISTS unknown_configs (
external_id TEXT UNIQUE NOT NULL,
external_key TEXT NOT NULL,
PRIMARY KEY (external_id, external_key)
)`,
`CREATE TABLE IF NOT EXISTS channels (
mainflux_channel TEXT UNIQUE NOT NULL,
owner VARCHAR(254),
name TEXT,
metadata JSON,
PRIMARY KEY (mainflux_channel, owner)
)`,
`CREATE TABLE IF NOT EXISTS connections (
channel_id TEXT,
channel_owner VARCHAR(256),
config_id TEXT,
config_owner VARCHAR(256),
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_client, owner) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
)`,
},
Down: []string{
"DROP TABLE connections",
"DROP TABLE configs",
"DROP TABLE channels",
"DROP TABLE unknown_configs",
},
},
{
Id: "configs_2",
Up: []string{
"DROP TABLE IF EXISTS unknown_configs",
},
Down: []string{
"CREATE TABLE IF NOT EXISTS unknown_configs",
},
},
{
Id: "configs_3",
Up: []string{
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS parent_id VARCHAR(36)`,
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS description VARCHAR(1024)`,
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS created_at TIMESTAMP`,
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP`,
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_by VARCHAR(254)`,
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0)`,
},
},
{
Id: "configs_4",
Up: []string{
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_client TO magistrala_client`,
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_key TO magistrala_secret`,
`ALTER TABLE IF EXISTS channels RENAME COLUMN mainflux_channel TO magistrala_channel`,
},
},
{
Id: "configs_5",
Up: []string{
`ALTER TABLE IF EXISTS configs RENAME COLUMN owner TO domain_id`,
`ALTER TABLE IF EXISTS channels RENAME COLUMN owner TO domain_id`,
`ALTER TABLE IF EXISTS configs ADD CONSTRAINT configs_name_domain_id_key UNIQUE (name, domain_id)`,
},
},
{
Id: "configs_6",
Up: []string{
`ALTER TABLE IF EXISTS connections DROP CONSTRAINT IF EXISTS connections_pkey`,
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS channel_owner`,
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS config_owner`,
`ALTER TABLE IF EXISTS connections ADD COLUMN IF NOT EXISTS domain_id VARCHAR(256) NOT NULL`,
`ALTER TABLE IF EXISTS connections ADD CONSTRAINT connections_pkey PRIMARY KEY (channel_id, config_id, domain_id)`,
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (channel_id, domain_id) REFERENCES channels (magistrala_channel, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (config_id, domain_id) REFERENCES configs (magistrala_client, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
},
},
},
}
}
-86
View File
@@ -1,86 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"fmt"
"log"
"os"
"testing"
"github.com/absmach/supermq/bootstrap/postgres"
smqlog "github.com/absmach/supermq/logger"
pgclient "github.com/absmach/supermq/pkg/postgres"
"github.com/jmoiron/sqlx"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
var (
testLog, _ = smqlog.New(os.Stdout, "info")
db *sqlx.DB
)
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
}
container, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "postgres",
Tag: "16.2-alpine",
Env: []string{
"POSTGRES_USER=test",
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
"listen_addresses = '*'",
},
}, func(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
})
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
port := container.GetPort("5432/tcp")
if err := pool.Retry(func() error {
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
db, err = sqlx.Open("pgx", url)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
}
dbConfig := pgclient.Config{
Host: "localhost",
Port: port,
User: "test",
Pass: "test",
Name: "test",
SSLMode: "disable",
SSLCert: "",
SSLKey: "",
SSLRootCert: "",
}
if db, err = pgclient.Setup(dbConfig, *postgres.Migration()); err != nil {
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
}
code := m.Run()
// Defers will not be run when using os.Exit
db.Close()
if err := pool.Purge(container); err != nil {
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
}
os.Exit(code)
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"io"
"net/http"
)
// bootstrapRes represent SuperMQ Response to the Bootatrap request.
// This is used as a response from ConfigReader and can easily be
// replace with any other response format.
type bootstrapRes struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Channels []channelRes `json:"channels"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
func (res bootstrapRes) Code() int {
return http.StatusOK
}
func (res bootstrapRes) Headers() map[string]string {
return map[string]string{}
}
func (res bootstrapRes) Empty() bool {
return false
}
type reader struct {
encKey []byte
}
// NewConfigReader return new reader which is used to generate response
// from the config.
func NewConfigReader(encKey []byte) ConfigReader {
return reader{encKey: encKey}
}
func (r reader) ReadConfig(cfg Config, secure bool) (interface{}, error) {
var channels []channelRes
for _, ch := range cfg.Channels {
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
}
res := bootstrapRes{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Channels: channels,
Content: cfg.Content,
ClientCert: cfg.ClientCert,
ClientKey: cfg.ClientKey,
CACert: cfg.CACert,
}
if secure {
b, err := json.Marshal(res)
if err != nil {
return nil, err
}
return r.encrypt(b)
}
return res, nil
}
func (r reader) encrypt(in []byte) ([]byte, error) {
block, err := aes.NewCipher(r.encKey)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(in))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
return ciphertext, nil
}
-126
View File
@@ -1,126 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap_test
import (
"crypto/aes"
"crypto/cipher"
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/absmach/supermq"
"github.com/absmach/supermq/bootstrap"
"github.com/absmach/supermq/pkg/errors"
"github.com/stretchr/testify/assert"
)
type readChan struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
type readResp struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Channels []readChan `json:"channels"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
func dec(in []byte) ([]byte, error) {
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
if len(in) < aes.BlockSize {
return nil, errors.ErrMalformedEntity
}
iv := in[:aes.BlockSize]
in = in[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(in, in)
return in, nil
}
func TestReadConfig(t *testing.T) {
cfg := bootstrap.Config{
ClientID: "smq_id",
ClientCert: "client_cert",
ClientKey: "client_key",
CACert: "ca_cert",
ClientSecret: "smq_key",
Channels: []bootstrap.Channel{
{
ID: "smq_id",
Name: "smq_name",
Metadata: map[string]interface{}{"key": "value}"},
},
},
Content: "content",
}
ret := readResp{
ClientID: "smq_id",
ClientSecret: "smq_key",
Channels: []readChan{
{
ID: "smq_id",
Name: "smq_name",
Metadata: map[string]interface{}{"key": "value}"},
},
},
Content: "content",
ClientCert: "client_cert",
ClientKey: "client_key",
CACert: "ca_cert",
}
bin, err := json.Marshal(ret)
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
reader := bootstrap.NewConfigReader(encKey)
cases := []struct {
desc string
config bootstrap.Config
enc []byte
secret bool
err error
}{
{
desc: "read a config",
config: cfg,
enc: bin,
secret: false,
},
{
desc: "read encrypted config",
config: cfg,
enc: bin,
secret: true,
},
}
for _, tc := range cases {
res, err := reader.ReadConfig(tc.config, tc.secret)
assert.Nil(t, err, fmt.Sprintf("Reading config to succeed: %s.\n", err))
if tc.secret {
d, err := dec(res.([]byte))
assert.Nil(t, err, fmt.Sprintf("Decrypting expected to succeed: %s.\n", err))
assert.Equal(t, tc.enc, d, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, d))
continue
}
b, err := json.Marshal(res)
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
assert.Equal(t, tc.enc, b, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, b))
resp, ok := res.(supermq.Response)
assert.True(t, ok, "If not encrypted, reader should return response.")
assert.False(t, resp.Empty(), fmt.Sprintf("Response should not be empty %s.", err))
assert.Equal(t, http.StatusOK, resp.Code(), "Default config response code should be 200.")
}
}
-505
View File
@@ -1,505 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"github.com/absmach/supermq"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/policies"
mgsdk "github.com/absmach/supermq/pkg/sdk"
)
var (
// ErrClients indicates failure to communicate with SuperMQ Clients service.
// It can be due to networking error or invalid/unauthenticated request.
ErrClients = errors.New("failed to receive response from Clients service")
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
ErrExternalKey = errors.New("failed to get bootstrap configuration for given external key")
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
ErrExternalKeySecure = errors.New("failed to get bootstrap configuration for given encrypted external key")
// ErrBootstrap indicates error in getting bootstrap configuration.
ErrBootstrap = errors.New("failed to read bootstrap configuration")
// ErrAddBootstrap indicates error in adding bootstrap configuration.
ErrAddBootstrap = errors.New("failed to add bootstrap configuration")
// ErrNotInSameDomain indicates entities are not in the same domain.
errNotInSameDomain = errors.New("entities are not in the same domain")
errUpdateConnections = errors.New("failed to update connections")
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
errChangeState = errors.New("failed to change state of bootstrap configuration")
errUpdateChannel = errors.New("failed to update channel")
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
errRemoveChannel = errors.New("failed to remove channel")
errCreateClient = errors.New("failed to create client")
errConnectClient = errors.New("failed to connect client")
errDisconnectClient = errors.New("failed to disconnect client")
errCheckChannels = errors.New("failed to check if channels exists")
errConnectionChannels = errors.New("failed to check channels connections")
errClientNotFound = errors.New("failed to find client")
errUpdateCert = errors.New("failed to update cert")
)
var _ Service = (*bootstrapService)(nil)
// Service specifies an API that must be fulfilled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
//
//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines"
type Service interface {
// Add adds new Client Config to the user identified by the provided token.
Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error)
// View returns Client Config with given ID belonging to the user identified by the given token.
View(ctx context.Context, session smqauthn.Session, id string) (Config, error)
// Update updates editable fields of the provided Config.
Update(ctx context.Context, session smqauthn.Session, cfg Config) error
// UpdateCert updates an existing Config certificate and token.
// A non-nil error is returned to indicate operation failure.
UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error)
// UpdateConnections updates list of Channels related to given Config.
UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error
// List returns subset of Configs with given search params that belong to the
// user identified by the given token.
List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error)
// Remove removes Config with specified token that belongs to the user identified by the given token.
Remove(ctx context.Context, session smqauthn.Session, id string) error
// Bootstrap returns Config to the Client with provided external ID using external key.
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
// ChangeState changes state of the Client with given client ID and domain ID.
ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
// handlers for events. That's why these methods surpass ownership check.
// UpdateChannelHandler updates Channel with data received from an event.
UpdateChannelHandler(ctx context.Context, channel Channel) error
// RemoveConfigHandler removes Configuration with id received from an event.
RemoveConfigHandler(ctx context.Context, id string) error
// RemoveChannelHandler removes Channel with id received from an event.
RemoveChannelHandler(ctx context.Context, id string) error
// ConnectClientHandler changes state of the Config to active when connect event occurs.
ConnectClientHandler(ctx context.Context, channelID, clientID string) error
// DisconnectClientHandler changes state of the Config to inactive when disconnect event occurs.
DisconnectClientHandler(ctx context.Context, channelID, clientID string) error
}
// ConfigReader is used to parse Config into format which will be encoded
// as a JSON and consumed from the client side. The purpose of this interface
// is to provide convenient way to generate custom configuration response
// based on the specific Config which will be consumed by the client.
//
//go:generate mockery --name ConfigReader --output=./mocks --filename config_reader.go --quiet --note "Copyright (c) Abstract Machines"
type ConfigReader interface {
ReadConfig(Config, bool) (interface{}, error)
}
type bootstrapService struct {
policies policies.Service
configs ConfigRepository
sdk mgsdk.SDK
encKey []byte
idProvider supermq.IDProvider
}
// New returns new Bootstrap service.
func New(policyService policies.Service, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp supermq.IDProvider) Service {
return &bootstrapService{
configs: configs,
sdk: sdk,
policies: policyService,
encKey: encKey,
idProvider: idp,
}
}
func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
toConnect := bs.toIDList(cfg.Channels)
// Check if channels exist. This is the way to prevent fetching channels that already exist.
existing, err := bs.configs.ListExisting(ctx, session.DomainID, toConnect)
if err != nil {
return Config{}, errors.Wrap(errCheckChannels, err)
}
cfg.Channels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), session.DomainID, token)
if err != nil {
return Config{}, errors.Wrap(errConnectionChannels, err)
}
id := cfg.ClientID
mgClient, err := bs.client(session.DomainID, id, token)
if err != nil {
return Config{}, errors.Wrap(errClientNotFound, err)
}
for _, channel := range cfg.Channels {
if channel.DomainID != mgClient.DomainID {
return Config{}, errors.Wrap(svcerr.ErrMalformedEntity, errNotInSameDomain)
}
}
cfg.ClientID = mgClient.ID
cfg.DomainID = session.DomainID
cfg.State = Inactive
cfg.ClientSecret = mgClient.Credentials.Secret
saved, err := bs.configs.Save(ctx, cfg, toConnect)
if err != nil {
// If id is empty, then a new client has been created function - bs.client(id, token)
// So, on bootstrap config save error , delete the newly created client.
if id == "" {
if errT := bs.sdk.DeleteClient(cfg.ClientID, cfg.DomainID, token); errT != nil {
err = errors.Wrap(err, errT)
}
}
return Config{}, errors.Wrap(ErrAddBootstrap, err)
}
cfg.ClientID = saved
cfg.Channels = append(cfg.Channels, existing...)
return cfg, nil
}
func (bs bootstrapService) View(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
if err != nil {
return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return cfg, nil
}
func (bs bootstrapService) Update(ctx context.Context, session smqauthn.Session, cfg Config) error {
cfg.DomainID = session.DomainID
if err := bs.configs.Update(ctx, cfg); err != nil {
return errors.Wrap(errUpdateConnections, err)
}
return nil
}
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error) {
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, clientID, clientCert, clientKey, caCert)
if err != nil {
return Config{}, errors.Wrap(errUpdateCert, err)
}
return cfg, nil
}
func (bs bootstrapService) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
add, remove := bs.updateList(cfg, connections)
// Check if channels exist. This is the way to prevent fetching channels that already exist.
existing, err := bs.configs.ListExisting(ctx, session.DomainID, connections)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), session.DomainID, token)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
cfg.Channels = channels
var connect, disconnect []string
if cfg.State == Active {
connect = add
disconnect = remove
}
for _, c := range disconnect {
if err := bs.sdk.DisconnectClients(c, []string{id}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
if errors.Contains(err, repoerr.ErrNotFound) {
continue
}
return ErrClients
}
}
for _, c := range connect {
conIDs := mgsdk.Connection{
ChannelIDs: []string{c},
ClientIDs: []string{id},
Types: []string{"Publish", "Subscribe"},
}
if err := bs.sdk.Connect(conIDs, session.DomainID, token); err != nil {
return ErrClients
}
}
if err := bs.configs.UpdateConnections(ctx, session.DomainID, id, channels, connections); err != nil {
return errors.Wrap(errUpdateConnections, err)
}
return nil
}
func (bs bootstrapService) listClientIDs(ctx context.Context, userID string) ([]string, error) {
tids, err := bs.policies.ListAllObjects(ctx, policies.Policy{
SubjectType: policies.UserType,
Subject: userID,
Permission: policies.ViewPermission,
ObjectType: policies.ClientType,
})
if err != nil {
return nil, errors.Wrap(svcerr.ErrNotFound, err)
}
return tids.Policies, nil
}
func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
if session.SuperAdmin {
return bs.configs.RetrieveAll(ctx, session.DomainID, []string{}, filter, offset, limit), nil
}
// Handle non-admin users
clientIDs, err := bs.listClientIDs(ctx, session.DomainUserID)
if err != nil {
return ConfigsPage{}, errors.Wrap(svcerr.ErrNotFound, err)
}
if len(clientIDs) == 0 {
return ConfigsPage{
Total: 0,
Offset: offset,
Limit: limit,
Configs: []Config{},
}, nil
}
return bs.configs.RetrieveAll(ctx, session.DomainID, clientIDs, filter, offset, limit), nil
}
func (bs bootstrapService) Remove(ctx context.Context, session smqauthn.Session, id string) error {
if err := bs.configs.Remove(ctx, session.DomainID, id); err != nil {
return errors.Wrap(errRemoveBootstrap, err)
}
return nil
}
func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
cfg, err := bs.configs.RetrieveByExternalID(ctx, externalID)
if err != nil {
return cfg, errors.Wrap(ErrBootstrap, err)
}
if secure {
dec, err := bs.dec(externalKey)
if err != nil {
return Config{}, errors.Wrap(ErrExternalKeySecure, err)
}
externalKey = dec
}
if cfg.ExternalKey != externalKey {
return Config{}, ErrExternalKey
}
return cfg, nil
}
func (bs bootstrapService) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error {
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
if err != nil {
return errors.Wrap(errChangeState, err)
}
if cfg.State == state {
return nil
}
switch state {
case Active:
for _, c := range cfg.Channels {
if err := bs.sdk.ConnectClients(c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
// Ignore conflict errors as they indicate the connection already exists.
if errors.Contains(err, svcerr.ErrConflict) {
continue
}
return ErrClients
}
}
case Inactive:
for _, c := range cfg.Channels {
if err := bs.sdk.DisconnectClients(c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
if errors.Contains(err, repoerr.ErrNotFound) {
continue
}
return ErrClients
}
}
}
if err := bs.configs.ChangeState(ctx, session.DomainID, id, state); err != nil {
return errors.Wrap(errChangeState, err)
}
return nil
}
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
if err := bs.configs.UpdateChannel(ctx, channel); err != nil {
return errors.Wrap(errUpdateChannel, err)
}
return nil
}
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
if err := bs.configs.RemoveClient(ctx, id); err != nil {
return errors.Wrap(errRemoveConfig, err)
}
return nil
}
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
if err := bs.configs.RemoveChannel(ctx, id); err != nil {
return errors.Wrap(errRemoveChannel, err)
}
return nil
}
func (bs bootstrapService) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := bs.configs.ConnectClient(ctx, channelID, clientID); err != nil {
return errors.Wrap(errConnectClient, err)
}
return nil
}
func (bs bootstrapService) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := bs.configs.DisconnectClient(ctx, channelID, clientID); err != nil {
return errors.Wrap(errDisconnectClient, err)
}
return nil
}
// Method client retrieves SuperMQ Client creating one if an empty ID is passed.
func (bs bootstrapService) client(domainID, id, token string) (mgsdk.Client, error) {
// If Client ID is not provided, then create new client.
if id == "" {
id, err := bs.idProvider.ID()
if err != nil {
return mgsdk.Client{}, errors.Wrap(errCreateClient, err)
}
client, sdkErr := bs.sdk.CreateClient(mgsdk.Client{ID: id, Name: "Bootstrapped Client " + id}, domainID, token)
if sdkErr != nil {
return mgsdk.Client{}, errors.Wrap(errCreateClient, sdkErr)
}
return client, nil
}
// If Client ID is provided, then retrieve client
client, sdkErr := bs.sdk.Client(id, domainID, token)
if sdkErr != nil {
return mgsdk.Client{}, errors.Wrap(ErrClients, sdkErr)
}
return client, nil
}
func (bs bootstrapService) connectionChannels(channels, existing []string, domainID, token string) ([]Channel, error) {
add := make(map[string]bool, len(channels))
for _, ch := range channels {
add[ch] = true
}
for _, ch := range existing {
if add[ch] {
delete(add, ch)
}
}
var ret []Channel
for id := range add {
ch, err := bs.sdk.Channel(id, domainID, token)
if err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
ret = append(ret, Channel{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
DomainID: ch.DomainID,
})
}
return ret, nil
}
// Method updateList accepts config and channel IDs and returns three lists:
// 1) IDs of Channels to be added
// 2) IDs of Channels to be removed
// 3) IDs of common Channels for these two configs.
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
disconnect := make(map[string]bool, len(cfg.Channels))
for _, c := range cfg.Channels {
disconnect[c.ID] = true
}
for _, c := range connections {
if disconnect[c] {
// Don't disconnect common elements.
delete(disconnect, c)
continue
}
// Connect new elements.
add = append(add, c)
}
for v := range disconnect {
remove = append(remove, v)
}
return
}
func (bs bootstrapService) toIDList(channels []Channel) []string {
var ret []string
for _, ch := range channels {
ret = append(ret, ch.ID)
}
return ret
}
func (bs bootstrapService) dec(in string) (string, error) {
ciphertext, err := hex.DecodeString(in)
if err != nil {
return "", err
}
block, err := aes.NewCipher(bs.encKey)
if err != nil {
return "", err
}
if len(ciphertext) < aes.BlockSize {
return "", err
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return string(ciphertext), nil
}
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import "strconv"
const (
// Inactive Client is created, but not able to exchange messages using SuperMQ.
Inactive State = iota
// Active Client is created, configured, and whitelisted.
Active
)
// State represents corresponding SuperMQ Client state. The possible Config States
// as well as description of what that State represents are given in the table:
// | State | What it means |
// |----------+--------------------------------------------------------------------------------|
// | Inactive | Client is created, but isn't able to communicate over SuperMQ |
// | Active | Client is able to communicate using SuperMQ |.
type State int
// String returns string representation of State.
func (s State) String() string {
return strconv.Itoa(int(s))
}
-12
View File
@@ -1,12 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package tracing provides tracing instrumentation for SuperMQ Users service.
//
// This package provides tracing middleware for SuperMQ Users service.
// It can be used to trace incoming requests and add tracing capabilities to
// SuperMQ Users service.
//
// For more details about tracing instrumentation for SuperMQ messaging refer
// to the documentation at https://docs.supermq.abstractmachines.fr/tracing/.
package tracing
-182
View File
@@ -1,182 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package tracing
import (
"context"
"github.com/absmach/supermq/bootstrap"
smqauthn "github.com/absmach/supermq/pkg/authn"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var _ bootstrap.Service = (*tracingMiddleware)(nil)
type tracingMiddleware struct {
tracer trace.Tracer
svc bootstrap.Service
}
// New returns a new bootstrap service with tracing capabilities.
func New(svc bootstrap.Service, tracer trace.Tracer) bootstrap.Service {
return &tracingMiddleware{tracer, svc}
}
// Add traces the "Add" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_register_user", trace.WithAttributes(
attribute.String("client_id", cfg.ClientID),
attribute.String("domain_id ", cfg.DomainID),
attribute.String("name", cfg.Name),
attribute.String("external_id", cfg.ExternalID),
attribute.String("content", cfg.Content),
attribute.String("state", cfg.State.String()),
))
defer span.End()
return tm.svc.Add(ctx, session, token, cfg)
}
// View traces the "View" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_view_user", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.View(ctx, session, id)
}
// Update traces the "Update" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
ctx, span := tm.tracer.Start(ctx, "svc_update_user", trace.WithAttributes(
attribute.String("name", cfg.Name),
attribute.String("content", cfg.Content),
attribute.String("client_id", cfg.ClientID),
attribute.String("domain_id ", cfg.DomainID),
))
defer span.End()
return tm.svc.Update(ctx, session, cfg)
}
// UpdateCert traces the "UpdateCert" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_update_cert", trace.WithAttributes(
attribute.String("client_id", clientID),
))
defer span.End()
return tm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
}
// UpdateConnections traces the "UpdateConnections" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
ctx, span := tm.tracer.Start(ctx, "svc_update_connections", trace.WithAttributes(
attribute.String("id", id),
attribute.StringSlice("connections", connections),
))
defer span.End()
return tm.svc.UpdateConnections(ctx, session, token, id, connections)
}
// List traces the "List" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
ctx, span := tm.tracer.Start(ctx, "svc_list_users", trace.WithAttributes(
attribute.Int64("offset", int64(offset)),
attribute.Int64("limit", int64(limit)),
))
defer span.End()
return tm.svc.List(ctx, session, filter, offset, limit)
}
// Remove traces the "Remove" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
ctx, span := tm.tracer.Start(ctx, "svc_remove_user", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.Remove(ctx, session, id)
}
// Bootstrap traces the "Bootstrap" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_bootstrap_user", trace.WithAttributes(
attribute.String("external_key", externalKey),
attribute.String("external_id", externalID),
attribute.Bool("secure", secure),
))
defer span.End()
return tm.svc.Bootstrap(ctx, externalKey, externalID, secure)
}
// ChangeState traces the "ChangeState" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
ctx, span := tm.tracer.Start(ctx, "svc_change_state", trace.WithAttributes(
attribute.String("id", id),
attribute.String("state", state.String()),
))
defer span.End()
return tm.svc.ChangeState(ctx, session, token, id, state)
}
// UpdateChannelHandler traces the "UpdateChannelHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
ctx, span := tm.tracer.Start(ctx, "svc_update_channel_handler", trace.WithAttributes(
attribute.String("id", channel.ID),
attribute.String("name", channel.Name),
attribute.String("description", channel.Description),
))
defer span.End()
return tm.svc.UpdateChannelHandler(ctx, channel)
}
// RemoveConfigHandler traces the "RemoveConfigHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
ctx, span := tm.tracer.Start(ctx, "svc_remove_config_handler", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.RemoveConfigHandler(ctx, id)
}
// RemoveChannelHandler traces the "RemoveChannelHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
ctx, span := tm.tracer.Start(ctx, "svc_remove_channel_handler", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.RemoveChannelHandler(ctx, id)
}
// ConnectClientHandler traces the "ConnectClientHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_connect_client_handler", trace.WithAttributes(
attribute.String("channel_id", channelID),
attribute.String("client_id", clientID),
))
defer span.End()
return tm.svc.ConnectClientHandler(ctx, channelID, clientID)
}
// DisconnectClientHandler traces the "DisconnectClientHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_disconnect_client_handler", trace.WithAttributes(
attribute.String("channel_id", channelID),
attribute.String("client_id", clientID),
))
defer span.End()
return tm.svc.DisconnectClientHandler(ctx, channelID, clientID)
}
+1 -1
View File
@@ -67,7 +67,7 @@ The service is configured using the environment variables presented in the follo
## Deployment
The service is distributed as Docker container. Check the [`certs`](https://github.com/absmach/supermq/blob/main/docker/addons/bootstrap/docker-compose.yml) service section in docker-compose file to see how the service is deployed.
The service is distributed as Docker container. Check the [`certs`](https://github.com/absmach/supermq/blob/main/docker/addons/certs/docker-compose.yml) service section in docker-compose file to see how the service is deployed.
Running this service outside of container requires working instance of the auth service, clients service, postgres database, vault and Jaeger server.
To start the service outside of the container, execute the following shell script:
+1 -1
View File
@@ -20,7 +20,7 @@ type loggingMiddleware struct {
svc certs.Service
}
// LoggingMiddleware adds logging facilities to the bootstrap service.
// LoggingMiddleware adds logging facilities to the certs service.
func LoggingMiddleware(svc certs.Service, logger *slog.Logger) certs.Service {
return &loggingMiddleware{logger, svc}
}
-76
View File
@@ -84,50 +84,6 @@ supermq-cli clients create '{"name":"myClient"}' <user_token>
supermq-cli clients create '{"name":"myClient", "metadata": {"key1":"value1"}}' <user_token>
```
#### Bulk Provision Clients
```bash
supermq-cli provision clients <file> <user_token>
```
- `file` - A CSV or JSON file containing client names (must have extension `.csv` or `.json`)
- `user_token` - A valid user auth token for the current system
An example CSV file might be:
```csv
client1,
client2,
client3,
```
in which the first column is the client's name.
A comparable JSON file would be
```json
[
{
"name": "<client1_name>",
"status": "enabled"
},
{
"name": "<client2_name>",
"status": "disabled"
},
{
"name": "<client3_name>",
"status": "enabled",
"credentials": {
"identity": "<client3_identity>",
"secret": "<client3_secret>"
}
}
]
```
With JSON you can be able to specify more fields of the channels you want to create
#### Update Client
```bash
@@ -322,38 +278,6 @@ supermq-cli messages send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"
supermq-cli messages read <channel_id> <user_token> -R <reader_url>
```
### Bootstrap
#### Add configuration
```bash
supermq-cli bootstrap create '{"external_id": "myExtID", "external_key": "myExtKey", "name": "myName", "content": "myContent"}' <user_token> -b <bootstrap-url>
```
#### View configuration
```bash
supermq-cli bootstrap get <client_id> <user_token> -b <bootstrap-url>
```
#### Update configuration
```bash
supermq-cli bootstrap update '{"client_id":"<client_id>", "name": "newName", "content": "newContent"}' <user_token> -b <bootstrap-url>
```
#### Remove configuration
```bash
supermq-cli bootstrap remove <client_id> <user_token> -b <bootstrap-url>
```
#### Bootstrap configuration
```bash
supermq-cli bootstrap bootstrap <external_id> <external_key> -b <bootstrap-url>
```
### Groups
#### Create Group
-216
View File
@@ -1,216 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
var cmdBootstrap = []cobra.Command{
{
Use: "create <JSON_config> <domain_id> <user_auth_token>",
Short: "Create config",
Long: `Create new Client Bootstrap Config to the user identified by the provided key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
var cfg smqsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
id, err := sdk.AddBootstrap(cfg, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logCreatedCmd(*cmd, id)
},
},
{
Use: "get [all | <client_id>] <domain_id> <user_auth_token>",
Short: "Get config",
Long: `Get Client Config with given ID belonging to the user identified by the given key.
all - lists all config
<client_id> - view config of <client_id>`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
pageMetadata := smqsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
State: State,
Name: Name,
}
if args[0] == "all" {
l, err := sdk.Bootstraps(pageMetadata, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, l)
return
}
c, err := sdk.ViewBootstrap(args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "update [config <JSON_config> | connection <id> <channel_ids> | certs <id> <client_cert> <client_key> <ca> ] <domain_id> <user_auth_token>",
Short: "Update config",
Long: `Updates editable fields of the provided Config.
config <JSON_config> - Updates editable fields of the provided Config.
connection <id> <channel_ids> - Updates connections performs update of the channel list corresponding Client is connected to.
channel_ids - '["channel_id1", ...]'
certs <id> <client_cert> <client_key> <ca> - Update bootstrap config certificates.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == "config" {
var cfg smqsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[1]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.UpdateBootstrap(cfg, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
return
}
if args[0] == "connection" {
var ids []string
if err := json.Unmarshal([]byte(args[2]), &ids); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.UpdateBootstrapConnection(args[1], ids, args[3], args[4]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
return
}
if args[0] == "certs" {
cfg, err := sdk.UpdateBootstrapCerts(args[0], args[1], args[2], args[3], args[4], args[5])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cfg)
return
}
logUsageCmd(*cmd, cmd.Use)
},
},
{
Use: "remove <client_id> <domain_id> <user_auth_token>",
Short: "Remove config",
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.RemoveBootstrap(args[0], args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
{
Use: "bootstrap [<external_id> <external_key> | secure <external_id> <external_key> <crypto_key> ]",
Short: "Bootstrap config",
Long: `Returns Config to the Client with provided external ID using external key.
secure - Retrieves a configuration with given external ID and encrypted external key.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == "secure" {
c, err := sdk.BootstrapSecure(args[1], args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
return
}
c, err := sdk.Bootstrap(args[0], args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "whitelist <JSON_config> <domain_id> <user_auth_token>",
Short: "Whitelist config",
Long: `Whitelist updates client state config with given id from the authenticated user`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
var cfg smqsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.Whitelist(cfg.ClientID, cfg.State, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
}
// NewBootstrapCmd returns bootstrap command.
func NewBootstrapCmd() *cobra.Command {
cmd := cobra.Command{
Use: "bootstrap [create | get | update | remove | bootstrap | whitelist]",
Short: "Bootstrap management",
Long: `Bootstrap management: create, get, update, delete or whitelist Bootstrap config`,
}
for i := range cmdBootstrap {
cmd.AddCommand(&cmdBootstrap[i])
}
return &cmd
}
-622
View File
@@ -1,622 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/absmach/supermq/cli"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
mgsdk "github.com/absmach/supermq/pkg/sdk"
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var bootConfig = mgsdk.BootstrapConfig{
ClientID: client.ID,
Channels: []string{channel.ID},
Name: "Test Bootstrap",
ExternalID: "09:6:0:sb:sa",
ExternalKey: "key",
}
func TestCreateBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]}", client.ID, "Test Bootstrap", channel.ID)
invalidJson := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]", client.ID, "Test Bootdtrap", channel.ID)
cases := []struct {
desc string
args []string
logType outputLog
response string
sdkErr errors.SDKError
errLogMessage string
id string
}{
{
desc: "create bootstrap config successfully",
args: []string{
jsonConfig,
domainID,
validToken,
},
logType: createLog,
id: client.ID,
response: fmt.Sprintf("\ncreated: %s\n\n", client.ID),
},
{
desc: "create bootstrap config with invald args",
args: []string{
jsonConfig,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "create bootstrap config with invald json",
args: []string{
invalidJson,
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "create bootstrap config with invald token",
args: []string{
jsonConfig,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("AddBootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.id, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{createCmd}, tc.args...)...)
switch tc.logType {
case createLog:
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestGetBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
var boot mgsdk.BootstrapConfig
var page mgsdk.BootstrapPage
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
page mgsdk.BootstrapPage
boot mgsdk.BootstrapConfig
logType outputLog
errLogMessage string
}{
{
desc: "get all bootstrap config successfully",
args: []string{
all,
domainID,
token,
},
page: mgsdk.BootstrapPage{
PageRes: mgsdk.PageRes{
Total: 1,
Offset: 0,
Limit: 10,
},
Configs: []mgsdk.BootstrapConfig{bootConfig},
},
logType: entityLog,
},
{
desc: "get bootstrap config with id",
args: []string{
channel.ID,
domainID,
token,
},
logType: entityLog,
boot: bootConfig,
},
{
desc: "get bootstrap config with invalid args",
args: []string{
all,
domainID,
token,
extraArg,
},
logType: usageLog,
},
{
desc: "get all bootstrap config with invalid token",
args: []string{
all,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "get bootstrap config with invalid id",
args: []string{
invalidID,
domainID,
token,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ViewBootstrap", tc.args[0], tc.args[1], tc.args[2]).Return(tc.boot, tc.sdkErr)
sdkCall1 := sdkMock.On("Bootstraps", mock.Anything, tc.args[1], tc.args[2]).Return(tc.page, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
if tc.args[0] == all {
err := json.Unmarshal([]byte(out), &page)
assert.Nil(t, err)
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
} else {
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.boot, boot))
}
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
func TestRemoveBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "remove bootstrap config successfully",
args: []string{
client.ID,
domainID,
token,
},
logType: okLog,
},
{
desc: "remove bootstrap config with invalid args",
args: []string{
client.ID,
domainID,
token,
extraArg,
},
logType: usageLog,
},
{
desc: "remove bootstrap config with invalid client id",
args: []string{
invalidID,
domainID,
token,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
{
desc: "remove bootstrap config with invalid token",
args: []string{
client.ID,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("RemoveBootstrap", tc.args[0], tc.args[1], tc.args[2]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{rmCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestUpdateBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
config := "config"
connection := "connection"
newConfigJson := "{\"name\" : \"New Bootstrap\"}"
chanIDsJson := fmt.Sprintf("[\"%s\"]", channel.ID)
cases := []struct {
desc string
args []string
boot mgsdk.BootstrapConfig
sdkErr errors.SDKError
errLogMessage string
logType outputLog
}{
{
desc: "update bootstrap config successfully",
args: []string{
config,
newConfigJson,
domainID,
token,
},
logType: okLog,
},
{
desc: "update bootstrap config with invalid token",
args: []string{
config,
newConfigJson,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap connections successfully",
args: []string{
connection,
client.ID,
chanIDsJson,
domainID,
token,
},
logType: okLog,
},
{
desc: "update bootstrap connections with invalid json",
args: []string{
connection,
client.ID,
fmt.Sprintf("[\"%s\"", client.ID),
domainID,
token,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "update bootstrap connections with invalid token",
args: []string{
connection,
client.ID,
chanIDsJson,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap certs successfully",
args: []string{
"certs",
client.ID,
"client cert",
"client key",
"ca",
domainID,
token,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "update bootstrap certs with invalid token",
args: []string{
"certs",
client.ID,
"client cert",
"client key",
"ca",
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap config with invalid args",
args: []string{
newConfigJson,
domainID,
token,
},
logType: usageLog,
},
{
desc: "update bootstrap config with invalid json",
args: []string{
config,
"{\"name\" : \"New Bootstrap\"",
domainID,
token,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "update bootstrap with invalid args",
args: []string{
extraArg,
extraArg,
extraArg,
extraArg,
extraArg,
},
logType: usageLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var boot mgsdk.BootstrapConfig
sdkCall := sdkMock.On("UpdateBootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
sdkCall1 := sdkMock.On("UpdateBootstrapConnection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
sdkCall2 := sdkMock.On("UpdateBootstrapCerts", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{updCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
sdkCall1.Unset()
sdkCall2.Unset()
})
}
}
func TestWhitelistConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d}", client.ID, 1)
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
}{
{
desc: "whitelist config successfully",
args: []string{
jsonConfig,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "whitelist config with invalid args",
args: []string{
jsonConfig,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "whitelist config with invalid json",
args: []string{
fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d", client.ID, 1),
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "whitelist config with invalid token",
args: []string{
jsonConfig,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("Whitelist", mock.Anything, mock.Anything, tc.args[1], tc.args[2]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{whitelistCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
})
}
}
func TestBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
var boot mgsdk.BootstrapConfig
crptoKey := "v7aT0HGxJxt2gULzr3RHwf4WIf6DusPp"
invalidKey := "invalid key"
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
boot mgsdk.BootstrapConfig
}{
{
desc: "bootstrap secure config successfully",
args: []string{
"secure",
bootConfig.ExternalID,
bootConfig.ExternalKey,
crptoKey,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "bootstrap config successfully",
args: []string{
bootConfig.ExternalID,
bootConfig.ExternalKey,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "bootstrap secure config with invalid args",
args: []string{
crptoKey,
},
logType: usageLog,
},
{
desc: "bootstrap secure config with invalid key",
args: []string{
"secure",
bootConfig.ExternalID,
invalidKey,
crptoKey,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
{
desc: "bootstrap config with invalid key",
args: []string{
bootConfig.ExternalID,
invalidKey,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("BootstrapSecure", mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
sdkCall1 := sdkMock.On("Bootstrap", mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{bootStrapCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
-6
View File
@@ -59,12 +59,6 @@ const (
readCmd = "read"
)
// Bootstrap commands
const (
whitelistCmd = "whitelist"
bootStrapCmd = "bootstrap"
)
// Invitations commands
const (
acceptCmd = "accept"
-8
View File
@@ -22,7 +22,6 @@ const (
defUsersURL string = defURL + ":9002"
defCLientsURL string = defURL + ":9000"
defReaderURL string = defURL + ":9011"
defBootstrapURL string = defURL + ":9013"
defDomainsURL string = defURL + ":8189"
defCertsURL string = defURL + ":9019"
defInvitationsURL string = defURL + ":9020"
@@ -41,7 +40,6 @@ type remotes struct {
ReaderURL string `toml:"reader_url"`
DomainsURL string `toml:"domains_url"`
HTTPAdapterURL string `toml:"http_adapter_url"`
BootstrapURL string `toml:"bootstrap_url"`
CertsURL string `toml:"certs_url"`
InvitationsURL string `toml:"invitations_url"`
JournalURL string `toml:"journal_url"`
@@ -112,7 +110,6 @@ func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) {
ReaderURL: defReaderURL,
DomainsURL: defDomainsURL,
HTTPAdapterURL: defHTTPURL,
BootstrapURL: defBootstrapURL,
CertsURL: defCertsURL,
InvitationsURL: defInvitationsURL,
JournalURL: defJournalURL,
@@ -191,10 +188,6 @@ func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) {
sdkConf.HTTPAdapterURL = config.Remotes.HTTPAdapterURL
}
if sdkConf.BootstrapURL == "" && config.Remotes.BootstrapURL != "" {
sdkConf.BootstrapURL = config.Remotes.BootstrapURL
}
if sdkConf.CertsURL == "" && config.Remotes.CertsURL != "" {
sdkConf.CertsURL = config.Remotes.CertsURL
}
@@ -262,7 +255,6 @@ func setConfigValue(key, value string) error {
"users_url": &config.Remotes.UsersURL,
"reader_url": &config.Remotes.ReaderURL,
"http_adapter_url": &config.Remotes.HTTPAdapterURL,
"bootstrap_url": &config.Remotes.BootstrapURL,
"certs_url": &config.Remotes.CertsURL,
"tls_verification": &config.Remotes.TLSVerification,
"offset": &config.Filter.Offset,
-410
View File
@@ -1,410 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"time"
"github.com/0x6flab/namegenerator"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
const (
jsonExt = ".json"
csvExt = ".csv"
PublishType = "publish"
SubscribeType = "subscribe"
)
var (
msgFormat = `[{"bn":"provision:", "bu":"V", "t": %d, "bver":5, "n":"voltage", "u":"V", "v":%d}]`
namesgenerator = namegenerator.NewGenerator()
)
var cmdProvision = []cobra.Command{
{
Use: "clients <clients_file> <domain_id> <user_token>",
Short: "Provision clients",
Long: `Bulk create clients`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
logErrorCmd(*cmd, err)
return
}
clients, err := clientsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
clients, err = sdk.CreateClients(clients, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, clients)
},
},
{
Use: "channels <channels_file> <domain_id> <user_token>",
Short: "Provision channels",
Long: `Bulk create channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
channels, err := channelsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
var chs []smqsdk.Channel
for _, c := range channels {
c, err = sdk.CreateChannel(c, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
chs = append(chs, c)
}
channels = chs
logJSONCmd(*cmd, channels)
},
},
{
Use: "connect <connections_file> <domain_id> <user_token>",
Short: "Provision connections",
Long: `Bulk connect clients to channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
connIDs, err := connectionsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
for _, conn := range connIDs {
if err := sdk.Connect(conn, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
}
logOKCmd(*cmd)
},
},
{
Use: "test",
Short: "test",
Long: `Provisions test setup: one test user, two clients and two channels. \
Connect both clients to one of the channels, \
and only on client to other channel.`,
Run: func(cmd *cobra.Command, args []string) {
numClients := 2
numChan := 2
clients := []smqsdk.Client{}
channels := []smqsdk.Channel{}
if len(args) != 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
// Create test user
name := namesgenerator.Generate()
user := smqsdk.User{
FirstName: name,
Email: fmt.Sprintf("%s@email.com", name),
Credentials: smqsdk.Credentials{
Username: name,
Secret: "12345678",
},
Status: smqsdk.EnabledStatus,
}
user, err := sdk.CreateUser(user, "")
if err != nil {
logErrorCmd(*cmd, err)
return
}
ut, err := sdk.CreateToken(smqsdk.Login{Username: user.Credentials.Username, Password: user.Credentials.Secret})
if err != nil {
logErrorCmd(*cmd, err)
return
}
// create domain
domain := smqsdk.Domain{
Name: fmt.Sprintf("%s-domain", name),
Status: smqsdk.EnabledStatus,
}
domain, err = sdk.CreateDomain(domain, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
ut, err = sdk.CreateToken(smqsdk.Login{Username: user.Email, Password: user.Credentials.Secret})
if err != nil {
logErrorCmd(*cmd, err)
return
}
// Create clients
for i := 0; i < numClients; i++ {
t := smqsdk.Client{
Name: fmt.Sprintf("%s-client-%d", name, i),
Status: smqsdk.EnabledStatus,
}
clients = append(clients, t)
}
clients, err = sdk.CreateClients(clients, domain.ID, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
// Create channels
for i := 0; i < numChan; i++ {
c := smqsdk.Channel{
Name: fmt.Sprintf("%s-channel-%d", name, i),
Status: smqsdk.EnabledStatus,
}
c, err = sdk.CreateChannel(c, domain.ID, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
channels = append(channels, c)
}
// Connect clients to channels - first client to both channels, second only to first
conIDs := smqsdk.Connection{
ChannelIDs: []string{channels[0].ID},
ClientIDs: []string{clients[0].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
conIDs = smqsdk.Connection{
ChannelIDs: []string{channels[1].ID},
ClientIDs: []string{clients[0].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
conIDs = smqsdk.Connection{
ChannelIDs: []string{channels[0].ID},
ClientIDs: []string{clients[1].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
// send message to test connectivity
if err := sdk.SendMessage(channels[0].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[0].Credentials.Secret); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.SendMessage(channels[0].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[1].Credentials.Secret); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.SendMessage(channels[1].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[0].Credentials.Secret); err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, user, ut, clients, channels)
},
},
}
// NewProvisionCmd returns provision command.
func NewProvisionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "provision [clients | channels | connect | test]",
Short: "Provision clients and channels from a config file",
Long: `Provision clients and channels: use json or csv file to bulk provision clients and channels`,
}
for i := range cmdProvision {
cmd.AddCommand(&cmdProvision[i])
}
return &cmd
}
func clientsFromFile(path string) ([]smqsdk.Client, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Client{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Client{}, err
}
defer file.Close()
clients := []smqsdk.Client{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Client{}, err
}
if len(l) < 1 {
return []smqsdk.Client{}, errors.New("empty line found in file")
}
client := smqsdk.Client{
Name: l[0],
}
clients = append(clients, client)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&clients)
if err != nil {
return []smqsdk.Client{}, err
}
default:
return []smqsdk.Client{}, err
}
return clients, nil
}
func channelsFromFile(path string) ([]smqsdk.Channel, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Channel{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Channel{}, err
}
defer file.Close()
channels := []smqsdk.Channel{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Channel{}, err
}
if len(l) < 1 {
return []smqsdk.Channel{}, errors.New("empty line found in file")
}
channel := smqsdk.Channel{
Name: l[0],
}
channels = append(channels, channel)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&channels)
if err != nil {
return []smqsdk.Channel{}, err
}
default:
return []smqsdk.Channel{}, err
}
return channels, nil
}
func connectionsFromFile(path string) ([]smqsdk.Connection, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Connection{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Connection{}, err
}
defer file.Close()
connections := []smqsdk.Connection{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Connection{}, err
}
if len(l) < 1 {
return []smqsdk.Connection{}, errors.New("empty line found in file")
}
connections = append(connections, smqsdk.Connection{
ClientIDs: []string{l[0]},
ChannelIDs: []string{l[1]},
Types: []string{PublishType, SubscribeType},
})
}
case jsonExt:
err := json.NewDecoder(file).Decode(&connections)
if err != nil {
return []smqsdk.Connection{}, err
}
default:
return []smqsdk.Connection{}, err
}
return connections, nil
}
-8
View File
@@ -92,14 +92,6 @@ func setFlags(rootCmd *cobra.Command) *cobra.Command {
"User status query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.State,
"state",
"z",
"",
"Bootstrap state query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Topic,
"topic",
-273
View File
@@ -1,273 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package main contains bootstrap main function to start the bootstrap service.
package main
import (
"context"
"fmt"
"log"
"log/slog"
"net/url"
"os"
chclient "github.com/absmach/callhome/pkg/client"
"github.com/absmach/supermq"
"github.com/absmach/supermq/bootstrap"
httpapi "github.com/absmach/supermq/bootstrap/api"
"github.com/absmach/supermq/bootstrap/events/consumer"
"github.com/absmach/supermq/bootstrap/events/producer"
"github.com/absmach/supermq/bootstrap/middleware"
bootstrappg "github.com/absmach/supermq/bootstrap/postgres"
"github.com/absmach/supermq/bootstrap/tracing"
smqlog "github.com/absmach/supermq/logger"
authsvcAuthn "github.com/absmach/supermq/pkg/authn/authsvc"
smqauthz "github.com/absmach/supermq/pkg/authz"
authsvcAuthz "github.com/absmach/supermq/pkg/authz/authsvc"
domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient"
"github.com/absmach/supermq/pkg/events"
"github.com/absmach/supermq/pkg/events/store"
"github.com/absmach/supermq/pkg/grpcclient"
"github.com/absmach/supermq/pkg/jaeger"
"github.com/absmach/supermq/pkg/policies"
"github.com/absmach/supermq/pkg/policies/spicedb"
pgclient "github.com/absmach/supermq/pkg/postgres"
"github.com/absmach/supermq/pkg/prometheus"
mgsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/absmach/supermq/pkg/server"
httpserver "github.com/absmach/supermq/pkg/server/http"
"github.com/absmach/supermq/pkg/uuid"
"github.com/authzed/authzed-go/v1"
"github.com/authzed/grpcutil"
"github.com/caarlos0/env/v11"
"github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
svcName = "bootstrap"
envPrefixDB = "SMQ_BOOTSTRAP_DB_"
envPrefixHTTP = "SMQ_BOOTSTRAP_HTTP_"
envPrefixAuth = "SMQ_AUTH_GRPC_"
envPrefixDomains = "SMQ_DOMAINS_GRPC_"
defDB = "bootstrap"
defSvcHTTPPort = "9013"
stream = "events.supermq.clients"
streamID = "supermq.bootstrap"
)
type config struct {
LogLevel string `env:"SMQ_BOOTSTRAP_LOG_LEVEL" envDefault:"info"`
EncKey string `env:"SMQ_BOOTSTRAP_ENCRYPT_KEY" envDefault:"12345678910111213141516171819202"`
ESConsumerName string `env:"SMQ_BOOTSTRAP_EVENT_CONSUMER" envDefault:"bootstrap"`
ClientsURL string `env:"SMQ_CLIENTS_URL" envDefault:"http://localhost:9000"`
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
InstanceID string `env:"SMQ_BOOTSTRAP_INSTANCE_ID" envDefault:""`
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"`
SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"`
SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"`
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
g, ctx := errgroup.WithContext(ctx)
cfg := config{}
if err := env.Parse(&cfg); err != nil {
log.Fatalf("failed to load %s configuration : %s", svcName, err)
}
logger, err := smqlog.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf("failed to init logger: %s", err.Error())
}
var exitCode int
defer smqlog.ExitWithError(&exitCode)
if cfg.InstanceID == "" {
if cfg.InstanceID, err = uuid.New().ID(); err != nil {
logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err))
exitCode = 1
return
}
}
// Create new postgres client
dbConfig := pgclient.Config{Name: defDB}
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
logger.Error(err.Error())
}
db, err := pgclient.Setup(dbConfig, *bootstrappg.Migration())
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
defer db.Close()
policySvc, err := newPolicyService(cfg, logger)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
logger.Info("Policy client successfully connected to spicedb gRPC server")
tp, err := jaeger.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio)
if err != nil {
logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err))
exitCode = 1
return
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
logger.Error(fmt.Sprintf("error shutting down tracer provider: %v", err))
}
}()
tracer := tp.Tracer(svcName)
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 := authsvcAuthn.NewAuthentication(ctx, grpcCfg)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
defer authnClient.Close()
domsGrpcCfg := grpcclient.Config{}
if err := env.ParseWithOptions(&domsGrpcCfg, env.Options{Prefix: envPrefixDomains}); err != nil {
logger.Error(fmt.Sprintf("failed to load domains gRPC client configuration : %s", err))
exitCode = 1
return
}
domainsAuthz, _, domainsHandler, err := domainsAuthz.NewAuthorization(ctx, domsGrpcCfg)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
defer domainsHandler.Close()
authz, authzClient, err := authsvcAuthz.NewAuthorization(ctx, grpcCfg, domainsAuthz)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
defer authzClient.Close()
logger.Info("AuthZ successfully connected to auth gRPC server " + authzClient.Secure())
// Create new service
svc, err := newService(ctx, authz, policySvc, db, tracer, logger, cfg, dbConfig)
if err != nil {
logger.Error(fmt.Sprintf("failed to create %s service: %s", svcName, err))
exitCode = 1
return
}
if err = subscribeToClientsES(ctx, svc, cfg, logger); err != nil {
logger.Error(fmt.Sprintf("failed to subscribe to clients event store: %s", err))
exitCode = 1
return
}
logger.Info("Subscribed to Event Store")
httpServerConfig := server.Config{Port: defSvcHTTPPort}
if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil {
logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err))
exitCode = 1
return
}
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, bootstrap.NewConfigReader([]byte(cfg.EncKey)), logger, cfg.InstanceID), logger)
if cfg.SendTelemetry {
chc := chclient.New(svcName, supermq.Version, logger, cancel)
go chc.CallHome(ctx)
}
// Start servers
g.Go(func() error {
return hs.Start()
})
g.Go(func() error {
return server.StopSignalHandler(ctx, cancel, logger, svcName, hs)
})
if err := g.Wait(); err != nil {
logger.Error(fmt.Sprintf("Bootstrap service terminated: %s", err))
}
}
func newService(ctx context.Context, authz smqauthz.Authorization, policySvc policies.Service, db *sqlx.DB, tracer trace.Tracer, logger *slog.Logger, cfg config, dbConfig pgclient.Config) (bootstrap.Service, error) {
database := pgclient.NewDatabase(db, dbConfig, tracer)
repoConfig := bootstrappg.NewConfigRepository(database, logger)
config := mgsdk.Config{
ClientsURL: cfg.ClientsURL,
}
sdk := mgsdk.NewSDK(config)
idp := uuid.New()
svc := bootstrap.New(policySvc, repoConfig, sdk, []byte(cfg.EncKey), idp)
publisher, err := store.NewPublisher(ctx, cfg.ESURL, streamID)
if err != nil {
return nil, err
}
svc = middleware.AuthorizationMiddleware(svc, authz)
svc = producer.NewEventStoreMiddleware(svc, publisher)
svc = middleware.LoggingMiddleware(svc, logger)
counter, latency := prometheus.MakeMetrics(svcName, "api")
svc = middleware.MetricsMiddleware(svc, counter, latency)
svc = tracing.New(svc, tracer)
return svc, nil
}
func subscribeToClientsES(ctx context.Context, svc bootstrap.Service, cfg config, logger *slog.Logger) error {
subscriber, err := store.NewSubscriber(ctx, cfg.ESURL, logger)
if err != nil {
return err
}
subConfig := events.SubscriberConfig{
Stream: stream,
Consumer: cfg.ESConsumerName,
Handler: consumer.NewEventHandler(svc),
}
return subscriber.Subscribe(ctx, subConfig)
}
func newPolicyService(cfg config, logger *slog.Logger) (policies.Service, error) {
client, err := authzed.NewClientWithExperimentalAPIs(
fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey),
)
if err != nil {
return nil, err
}
policySvc := spicedb.NewPolicyService(client, logger)
return policySvc, nil
}
-20
View File
@@ -41,8 +41,6 @@ func main() {
groupsCmd := cli.NewGroupsCmd()
channelsCmd := cli.NewChannelsCmd()
messagesCmd := cli.NewMessagesCmd()
provisionCmd := cli.NewProvisionCmd()
bootstrapCmd := cli.NewBootstrapCmd()
certsCmd := cli.NewCertsCmd()
subscriptionsCmd := cli.NewSubscriptionCmd()
configCmd := cli.NewConfigCmd()
@@ -57,8 +55,6 @@ func main() {
rootCmd.AddCommand(clientsCmd)
rootCmd.AddCommand(channelsCmd)
rootCmd.AddCommand(messagesCmd)
rootCmd.AddCommand(provisionCmd)
rootCmd.AddCommand(bootstrapCmd)
rootCmd.AddCommand(certsCmd)
rootCmd.AddCommand(subscriptionsCmd)
rootCmd.AddCommand(configCmd)
@@ -66,14 +62,6 @@ func main() {
rootCmd.AddCommand(journalCmd)
// Root Flags
rootCmd.PersistentFlags().StringVarP(
&sdkConf.BootstrapURL,
"bootstrap-url",
"b",
sdkConf.BootstrapURL,
"Bootstrap service URL",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.CertsURL,
"certs-url",
@@ -234,14 +222,6 @@ func main() {
"User status query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.State,
"state",
"z",
"",
"Bootstrap state query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Topic,
"topic",
-190
View File
@@ -1,190 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package main contains provision main function to start the provision service.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"reflect"
chclient "github.com/absmach/callhome/pkg/client"
"github.com/absmach/supermq"
"github.com/absmach/supermq/channels"
"github.com/absmach/supermq/clients"
smqlog "github.com/absmach/supermq/logger"
"github.com/absmach/supermq/pkg/errors"
mgsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/absmach/supermq/pkg/server"
httpserver "github.com/absmach/supermq/pkg/server/http"
"github.com/absmach/supermq/pkg/uuid"
"github.com/absmach/supermq/provision"
httpapi "github.com/absmach/supermq/provision/api"
"github.com/caarlos0/env/v11"
"golang.org/x/sync/errgroup"
)
const (
svcName = "provision"
contentType = "application/json"
)
var (
errMissingConfigFile = errors.New("missing config file setting")
errFailLoadingConfigFile = errors.New("failed to load config from file")
errFailedToReadBootstrapContent = errors.New("failed to read bootstrap content from envs")
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
g, ctx := errgroup.WithContext(ctx)
cfg, err := loadConfig()
if err != nil {
log.Fatalf("failed to load %s configuration : %s", svcName, err)
}
logger, err := smqlog.New(os.Stdout, cfg.Server.LogLevel)
if err != nil {
log.Fatalf("failed to init logger: %s", err.Error())
}
var exitCode int
defer smqlog.ExitWithError(&exitCode)
if cfg.InstanceID == "" {
if cfg.InstanceID, err = uuid.New().ID(); err != nil {
logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err))
exitCode = 1
return
}
}
if cfgFromFile, err := loadConfigFromFile(cfg.File); err != nil {
logger.Warn(fmt.Sprintf("Continue with settings from env, failed to load from: %s: %s", cfg.File, err))
} else {
// Merge environment variables and file settings.
mergeConfigs(&cfgFromFile, &cfg)
cfg = cfgFromFile
logger.Info("Continue with settings from file: " + cfg.File)
}
SDKCfg := mgsdk.Config{
UsersURL: cfg.Server.UsersURL,
ClientsURL: cfg.Server.ClientsURL,
BootstrapURL: cfg.Server.MgBSURL,
CertsURL: cfg.Server.MgCertsURL,
MsgContentType: contentType,
TLSVerification: cfg.Server.TLS,
}
SDK := mgsdk.NewSDK(SDKCfg)
svc := provision.New(cfg, SDK, logger)
svc = httpapi.NewLoggingMiddleware(svc, logger)
httpServerConfig := server.Config{Host: "", Port: cfg.Server.HTTPPort, KeyFile: cfg.Server.ServerKey, CertFile: cfg.Server.ServerCert}
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, logger, cfg.InstanceID), logger)
if cfg.SendTelemetry {
chc := chclient.New(svcName, supermq.Version, logger, cancel)
go chc.CallHome(ctx)
}
g.Go(func() error {
return hs.Start()
})
g.Go(func() error {
return server.StopSignalHandler(ctx, cancel, logger, svcName, hs)
})
if err := g.Wait(); err != nil {
logger.Error(fmt.Sprintf("Provision service terminated: %s", err))
}
}
func loadConfigFromFile(file string) (provision.Config, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
return provision.Config{}, errors.Wrap(errMissingConfigFile, err)
}
c, err := provision.Read(file)
if err != nil {
return provision.Config{}, errors.Wrap(errFailLoadingConfigFile, err)
}
return c, nil
}
func loadConfig() (provision.Config, error) {
cfg := provision.Config{}
if err := env.Parse(&cfg); err != nil {
return provision.Config{}, err
}
if cfg.Bootstrap.AutoWhiteList && !cfg.Bootstrap.Provision {
return provision.Config{}, errors.New("Can't auto whitelist if auto config save is off")
}
var content map[string]interface{}
if cfg.BSContent != "" {
if err := json.Unmarshal([]byte(cfg.BSContent), &content); err != nil {
return provision.Config{}, errFailedToReadBootstrapContent
}
}
cfg.Bootstrap.Content = content
// This is default conf for provision if there is no config file
cfg.Channels = []channels.Channel{
{
Name: "control-channel",
Metadata: map[string]interface{}{"type": "control"},
}, {
Name: "data-channel",
Metadata: map[string]interface{}{"type": "data"},
},
}
cfg.Clients = []clients.Client{
{
Name: "client",
Metadata: map[string]interface{}{"external_id": "xxxxxx"},
},
}
return cfg, nil
}
func mergeConfigs(dst, src interface{}) interface{} {
d := reflect.ValueOf(dst).Elem()
s := reflect.ValueOf(src).Elem()
for i := 0; i < d.NumField(); i++ {
dField := d.Field(i)
sField := s.Field(i)
switch dField.Kind() {
case reflect.Struct:
dst := dField.Addr().Interface()
src := sField.Addr().Interface()
m := mergeConfigs(dst, src)
val := reflect.ValueOf(m).Elem().Interface()
dField.Set(reflect.ValueOf(val))
case reflect.Slice:
case reflect.Bool:
if dField.Interface() == false {
dField.Set(reflect.ValueOf(sField.Interface()))
}
case reflect.Int:
if dField.Interface() == 0 {
dField.Set(reflect.ValueOf(sField.Interface()))
}
case reflect.String:
if dField.Interface() == "" {
dField.Set(reflect.ValueOf(sField.Interface()))
}
}
}
return dst
}
-1
View File
@@ -11,7 +11,6 @@ user_token = ""
[remotes]
journal_url = "http://localhost:9021"
bootstrap_url = "http://localhost:9013"
certs_url = "http://localhost:9019"
domains_url = "http://localhost:8189"
host_url = "http://localhost"
-45
View File
@@ -136,7 +136,6 @@ SMQ_DOMAINS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}
SMQ_DOMAINS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}
SMQ_DOMAINS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
### SpiceDB Datastore config
SMQ_SPICEDB_DB_USER=supermq
SMQ_SPICEDB_DB_PASS=supermq
@@ -176,7 +175,6 @@ SMQ_CLIENTS_URL=http://clients:9006
SMQ_USERS_URL=http://users:9002
SMQ_INVITATIONS_URL=http://invitations:9020
SMQ_DOMAINS_URL=http://domains:9003
SMQ_BOOTSTRAP_URL=http://bootstrap:9013
SMQ_UI_HOST_URL=http://localhost:9095
SMQ_UI_VERIFICATION_TLS=false
SMQ_UI_CONTENT_TYPE=application/senml+json
@@ -334,7 +332,6 @@ SMQ_CHANNELS_DB_SSL_KEY=
SMQ_CHANNELS_DB_SSL_ROOT_CERT=
SMQ_CHANNELS_INSTANCE_ID=
#### Channels Client Config
SMQ_CHANNELS_URL=http://channels:9005
SMQ_CHANNELS_GRPC_URL=channels:7005
@@ -381,48 +378,6 @@ SMQ_WS_ADAPTER_HTTP_SERVER_KEY=
SMQ_WS_ADAPTER_INSTANCE_ID=
## Addons Services
### Bootstrap
SMQ_BOOTSTRAP_LOG_LEVEL=debug
SMQ_BOOTSTRAP_ENCRYPT_KEY=v7aT0HGxJxt2gULzr3RHwf4WIf6DusPp
SMQ_BOOTSTRAP_EVENT_CONSUMER=bootstrap
SMQ_BOOTSTRAP_HTTP_HOST=bootstrap
SMQ_BOOTSTRAP_HTTP_PORT=9013
SMQ_BOOTSTRAP_HTTP_SERVER_CERT=
SMQ_BOOTSTRAP_HTTP_SERVER_KEY=
SMQ_BOOTSTRAP_DB_HOST=bootstrap-db
SMQ_BOOTSTRAP_DB_PORT=5432
SMQ_BOOTSTRAP_DB_USER=supermq
SMQ_BOOTSTRAP_DB_PASS=supermq
SMQ_BOOTSTRAP_DB_NAME=bootstrap
SMQ_BOOTSTRAP_DB_SSL_MODE=disable
SMQ_BOOTSTRAP_DB_SSL_CERT=
SMQ_BOOTSTRAP_DB_SSL_KEY=
SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT=
SMQ_BOOTSTRAP_INSTANCE_ID=
### Provision
SMQ_PROVISION_CONFIG_FILE=/configs/config.toml
SMQ_PROVISION_LOG_LEVEL=debug
SMQ_PROVISION_HTTP_PORT=9016
SMQ_PROVISION_ENV_CLIENTS_TLS=false
SMQ_PROVISION_SERVER_CERT=
SMQ_PROVISION_SERVER_KEY=
SMQ_PROVISION_USERS_LOCATION=http://users:9002
SMQ_PROVISION_CLIENTS_LOCATION=http://clients:9006
SMQ_PROVISION_USER=
SMQ_PROVISION_USERNAME=
SMQ_PROVISION_PASS=
SMQ_PROVISION_API_KEY=
SMQ_PROVISION_CERTS_SVC_URL=http://certs:9019
SMQ_PROVISION_X509_PROVISIONING=false
SMQ_PROVISION_BS_SVC_URL=http://bootstrap:9013
SMQ_PROVISION_BS_CONFIG_PROVISIONING=true
SMQ_PROVISION_BS_AUTO_WHITELIST=true
SMQ_PROVISION_BS_CONTENT=
SMQ_PROVISION_CERTS_HOURS_VALID=2400h
SMQ_PROVISION_CERTS_RSA_BITS=2048
SMQ_PROVISION_INSTANCE_ID=
### Vault
SMQ_VAULT_HOST=vault
SMQ_VAULT_PORT=8200
@@ -1,90 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
# This docker-compose file contains optional bootstrap services. Since it's optional, this file is
# dependent of docker-compose file from <project_root>/docker. In order to run this services, execute command:
# docker compose -f docker/docker-compose.yml -f docker/addons/bootstrap/docker-compose.yml up
# from project root.
networks:
supermq-base-net:
volumes:
supermq-bootstrap-db-volume:
services:
bootstrap-db:
image: postgres:16.2-alpine
container_name: supermq-bootstrap-db
restart: on-failure
environment:
POSTGRES_USER: ${SMQ_BOOTSTRAP_DB_USER}
POSTGRES_PASSWORD: ${SMQ_BOOTSTRAP_DB_PASS}
POSTGRES_DB: ${SMQ_BOOTSTRAP_DB_NAME}
networks:
- supermq-base-net
volumes:
- supermq-bootstrap-db-volume:/var/lib/postgresql/data
bootstrap:
image: supermq/bootstrap:${SMQ_RELEASE_TAG}
container_name: supermq-bootstrap
depends_on:
- bootstrap-db
restart: on-failure
ports:
- ${SMQ_BOOTSTRAP_HTTP_PORT}:${SMQ_BOOTSTRAP_HTTP_PORT}
environment:
SMQ_BOOTSTRAP_LOG_LEVEL: ${SMQ_BOOTSTRAP_LOG_LEVEL}
SMQ_BOOTSTRAP_ENCRYPT_KEY: ${SMQ_BOOTSTRAP_ENCRYPT_KEY}
SMQ_BOOTSTRAP_EVENT_CONSUMER: ${SMQ_BOOTSTRAP_EVENT_CONSUMER}
SMQ_ES_URL: ${SMQ_ES_URL}
SMQ_BOOTSTRAP_HTTP_HOST: ${SMQ_BOOTSTRAP_HTTP_HOST}
SMQ_BOOTSTRAP_HTTP_PORT: ${SMQ_BOOTSTRAP_HTTP_PORT}
SMQ_BOOTSTRAP_HTTP_SERVER_CERT: ${SMQ_BOOTSTRAP_HTTP_SERVER_CERT}
SMQ_BOOTSTRAP_HTTP_SERVER_KEY: ${SMQ_BOOTSTRAP_HTTP_SERVER_KEY}
SMQ_BOOTSTRAP_DB_HOST: ${SMQ_BOOTSTRAP_DB_HOST}
SMQ_BOOTSTRAP_DB_PORT: ${SMQ_BOOTSTRAP_DB_PORT}
SMQ_BOOTSTRAP_DB_USER: ${SMQ_BOOTSTRAP_DB_USER}
SMQ_BOOTSTRAP_DB_PASS: ${SMQ_BOOTSTRAP_DB_PASS}
SMQ_BOOTSTRAP_DB_NAME: ${SMQ_BOOTSTRAP_DB_NAME}
SMQ_BOOTSTRAP_DB_SSL_MODE: ${SMQ_BOOTSTRAP_DB_SSL_MODE}
SMQ_BOOTSTRAP_DB_SSL_CERT: ${SMQ_BOOTSTRAP_DB_SSL_CERT}
SMQ_BOOTSTRAP_DB_SSL_KEY: ${SMQ_BOOTSTRAP_DB_SSL_KEY}
SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT: ${SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT}
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_CLIENTS_URL: ${SMQ_CLIENTS_URL}
SMQ_JAEGER_URL: ${SMQ_JAEGER_URL}
SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
SMQ_BOOTSTRAP_INSTANCE_ID: ${SMQ_BOOTSTRAP_INSTANCE_ID}
SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY}
SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST}
SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT}
SMQ_DOMAINS_GRPC_URL: ${SMQ_DOMAINS_GRPC_URL}
SMQ_DOMAINS_GRPC_TIMEOUT: ${SMQ_DOMAINS_GRPC_TIMEOUT}
SMQ_DOMAINS_GRPC_CLIENT_CERT: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt}
SMQ_DOMAINS_GRPC_CLIENT_KEY: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key}
SMQ_DOMAINS_GRPC_SERVER_CA_CERTS: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt}
networks:
- supermq-base-net
volumes:
- type: bind
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${SMQ_AUTH_GRPC_CLIENT_CERT:-./ssl/certs/dummy/client_cert}
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
bind:
create_host_path: true
- type: bind
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${SMQ_AUTH_GRPC_CLIENT_KEY:-./ssl/certs/dummy/client_key}
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
bind:
create_host_path: true
- type: bind
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-./ssl/certs/dummy/server_ca}
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
bind:
create_host_path: true
@@ -1,74 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
[bootstrap]
[bootstrap.content]
[bootstrap.content.agent.edgex]
url = "http://localhost:48090/api/v1/"
[bootstrap.content.agent.log]
level = "info"
[bootstrap.content.agent.mqtt]
mtls = false
qos = 0
retain = false
skip_tls_ver = true
url = "localhost:1883"
[bootstrap.content.agent.server]
nats_url = "localhost:4222"
port = "9000"
[bootstrap.content.agent.heartbeat]
interval = "30s"
[bootstrap.content.agent.terminal]
session_timeout = "30s"
[bootstrap.content.export.exp]
log_level = "debug"
nats = "nats://localhost:4222"
port = "8172"
cache_url = "localhost:6379"
cache_pass = ""
cache_db = "0"
[bootstrap.content.export.mqtt]
ca_path = "ca.crt"
cert_path = "client.crt"
channel = ""
host = "tcp://localhost:1883"
mtls = false
password = ""
priv_key_path = "client.key"
qos = 0
retain = false
skip_tls_ver = false
username = ""
[[bootstrap.content.export.routes]]
mqtt_topic = ""
nats_topic = ">"
subtopic = ""
type = "plain"
workers = 10
[[clients]]
name = "client"
[clients.metadata]
external_id = "xxxxxx"
[[channels]]
name = "control-channel"
[channels.metadata]
type = "control"
[[channels]]
name = "data-channel"
[channels.metadata]
type = "data"
@@ -1,46 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
# This docker-compose file contains optional provision services. Since it's optional, this file is
# dependent of docker-compose file from <project_root>/docker. In order to run this services, execute command:
# docker compose -f docker/docker-compose.yml -f docker/addons/provision/docker-compose.yml up
# from project root.
networks:
supermq-base-net:
services:
provision:
image: supermq/provision:${SMQ_RELEASE_TAG}
container_name: supermq-provision
restart: on-failure
networks:
- supermq-base-net
ports:
- ${SMQ_PROVISION_HTTP_PORT}:${SMQ_PROVISION_HTTP_PORT}
environment:
SMQ_PROVISION_LOG_LEVEL: ${SMQ_PROVISION_LOG_LEVEL}
SMQ_PROVISION_HTTP_PORT: ${SMQ_PROVISION_HTTP_PORT}
SMQ_PROVISION_CONFIG_FILE: ${SMQ_PROVISION_CONFIG_FILE}
SMQ_PROVISION_ENV_CLIENTS_TLS: ${SMQ_PROVISION_ENV_CLIENTS_TLS}
SMQ_PROVISION_SERVER_CERT: ${SMQ_PROVISION_SERVER_CERT}
SMQ_PROVISION_SERVER_KEY: ${SMQ_PROVISION_SERVER_KEY}
SMQ_PROVISION_USERS_LOCATION: ${SMQ_PROVISION_USERS_LOCATION}
SMQ_PROVISION_CLIENTS_LOCATION: ${SMQ_PROVISION_CLIENTS_LOCATION}
SMQ_PROVISION_USER: ${SMQ_PROVISION_USER}
SMQ_PROVISION_USERNAME: ${SMQ_PROVISION_USERNAME}
SMQ_PROVISION_PASS: ${SMQ_PROVISION_PASS}
SMQ_PROVISION_API_KEY: ${SMQ_PROVISION_API_KEY}
SMQ_PROVISION_CERTS_SVC_URL: ${SMQ_PROVISION_CERTS_SVC_URL}
SMQ_PROVISION_X509_PROVISIONING: ${SMQ_PROVISION_X509_PROVISIONING}
SMQ_PROVISION_BS_SVC_URL: ${SMQ_PROVISION_BS_SVC_URL}
SMQ_PROVISION_BS_CONFIG_PROVISIONING: ${SMQ_PROVISION_BS_CONFIG_PROVISIONING}
SMQ_PROVISION_BS_AUTO_WHITELIST: ${SMQ_PROVISION_BS_AUTO_WHITELIST}
SMQ_PROVISION_BS_CONTENT: ${SMQ_PROVISION_BS_CONTENT}
SMQ_PROVISION_CERTS_HOURS_VALID: ${SMQ_PROVISION_CERTS_HOURS_VALID}
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
SMQ_PROVISION_INSTANCE_ID: ${SMQ_PROVISION_INSTANCE_ID}
volumes:
- ./configs:/configs
- ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key
- ../../ssl/certs/ca.crt:/etc/ssl/certs/ca.crt
-322
View File
@@ -1,322 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package sdk
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
)
const (
configsEndpoint = "clients/configs"
bootstrapEndpoint = "clients/bootstrap"
whitelistEndpoint = "clients/state"
bootstrapCertsEndpoint = "clients/configs/certs"
bootstrapConnEndpoint = "clients/configs/connections"
secureEndpoint = "secure"
)
// BootstrapConfig represents Configuration entity. It wraps information about external entity
// as well as info about corresponding SuperMQ entities.
// MGClient represents corresponding SuperMQ Client ID.
// MGKey is key of corresponding SuperMQ Client.
// MGChannels is a list of SuperMQ Channels corresponding SuperMQ Client connects to.
type BootstrapConfig struct {
Channels interface{} `json:"channels,omitempty"`
ExternalID string `json:"external_id,omitempty"`
ExternalKey string `json:"external_key,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
Name string `json:"name,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
Content string `json:"content,omitempty"`
State int `json:"state,omitempty"`
}
func (ts *BootstrapConfig) UnmarshalJSON(data []byte) error {
var rawData map[string]json.RawMessage
if err := json.Unmarshal(data, &rawData); err != nil {
return err
}
if channelData, ok := rawData["channels"]; ok {
var stringData []string
if err := json.Unmarshal(channelData, &stringData); err == nil {
ts.Channels = stringData
} else {
var channels []Channel
if err := json.Unmarshal(channelData, &channels); err == nil {
ts.Channels = channels
} else {
return fmt.Errorf("unsupported channel data type")
}
}
}
if err := json.Unmarshal(data, &struct {
ExternalID *string `json:"external_id,omitempty"`
ExternalKey *string `json:"external_key,omitempty"`
ClientID *string `json:"client_id,omitempty"`
ClientSecret *string `json:"client_secret,omitempty"`
Name *string `json:"name,omitempty"`
ClientCert *string `json:"client_cert,omitempty"`
ClientKey *string `json:"client_key,omitempty"`
CACert *string `json:"ca_cert,omitempty"`
Content *string `json:"content,omitempty"`
State *int `json:"state,omitempty"`
}{
ExternalID: &ts.ExternalID,
ExternalKey: &ts.ExternalKey,
ClientID: &ts.ClientID,
ClientSecret: &ts.ClientSecret,
Name: &ts.Name,
ClientCert: &ts.ClientCert,
ClientKey: &ts.ClientKey,
CACert: &ts.CACert,
Content: &ts.Content,
State: &ts.State,
}); err != nil {
return err
}
return nil
}
func (sdk mgSDK) AddBootstrap(cfg BootstrapConfig, domainID, token string) (string, errors.SDKError) {
data, err := json.Marshal(cfg)
if err != nil {
return "", errors.NewSDKError(err)
}
url := fmt.Sprintf("%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint)
headers, _, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusOK, http.StatusCreated)
if sdkerr != nil {
return "", sdkerr
}
id := strings.TrimPrefix(headers.Get("Location"), "/clients/configs/")
return id, nil
}
func (sdk mgSDK) Bootstraps(pm PageMetadata, domainID, token string) (BootstrapPage, errors.SDKError) {
endpoint := fmt.Sprintf("%s/%s", domainID, configsEndpoint)
url, err := sdk.withQueryParams(sdk.bootstrapURL, endpoint, pm)
if err != nil {
return BootstrapPage{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return BootstrapPage{}, sdkerr
}
var bb BootstrapPage
if err = json.Unmarshal(body, &bb); err != nil {
return BootstrapPage{}, errors.NewSDKError(err)
}
return bb, nil
}
func (sdk mgSDK) Whitelist(clientID string, state int, domainID, token string) errors.SDKError {
if clientID == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
data, err := json.Marshal(BootstrapConfig{State: state})
if err != nil {
return errors.NewSDKError(err)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, whitelistEndpoint, clientID)
_, _, sdkerr := sdk.processRequest(http.MethodPut, url, token, data, nil, http.StatusCreated, http.StatusOK)
return sdkerr
}
func (sdk mgSDK) ViewBootstrap(id, domainID, token string) (BootstrapConfig, errors.SDKError) {
if id == "" {
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, id)
_, body, err := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
if err != nil {
return BootstrapConfig{}, err
}
var bc BootstrapConfig
if err := json.Unmarshal(body, &bc); err != nil {
return BootstrapConfig{}, errors.NewSDKError(err)
}
return bc, nil
}
func (sdk mgSDK) UpdateBootstrap(cfg BootstrapConfig, domainID, token string) errors.SDKError {
if cfg.ClientID == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, cfg.ClientID)
data, err := json.Marshal(cfg)
if err != nil {
return errors.NewSDKError(err)
}
_, _, sdkerr := sdk.processRequest(http.MethodPut, url, token, data, nil, http.StatusOK)
return sdkerr
}
func (sdk mgSDK) UpdateBootstrapCerts(id, clientCert, clientKey, ca, domainID, token string) (BootstrapConfig, errors.SDKError) {
if id == "" {
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapCertsEndpoint, id)
request := BootstrapConfig{
ClientCert: clientCert,
ClientKey: clientKey,
CACert: ca,
}
data, err := json.Marshal(request)
if err != nil {
return BootstrapConfig{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(http.MethodPatch, url, token, data, nil, http.StatusOK)
if sdkerr != nil {
return BootstrapConfig{}, sdkerr
}
var bc BootstrapConfig
if err := json.Unmarshal(body, &bc); err != nil {
return BootstrapConfig{}, errors.NewSDKError(err)
}
return bc, nil
}
func (sdk mgSDK) UpdateBootstrapConnection(id string, channels []string, domainID, token string) errors.SDKError {
if id == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapConnEndpoint, id)
request := map[string][]string{
"channels": channels,
}
data, err := json.Marshal(request)
if err != nil {
return errors.NewSDKError(err)
}
_, _, sdkerr := sdk.processRequest(http.MethodPut, url, token, data, nil, http.StatusOK)
return sdkerr
}
func (sdk mgSDK) RemoveBootstrap(id, domainID, token string) errors.SDKError {
if id == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, id)
_, _, err := sdk.processRequest(http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return err
}
func (sdk mgSDK) Bootstrap(externalID, externalKey string) (BootstrapConfig, errors.SDKError) {
if externalID == "" {
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s", sdk.bootstrapURL, bootstrapEndpoint, externalID)
_, body, err := sdk.processRequest(http.MethodGet, url, ClientPrefix+externalKey, nil, nil, http.StatusOK)
if err != nil {
return BootstrapConfig{}, err
}
var bc BootstrapConfig
if err := json.Unmarshal(body, &bc); err != nil {
return BootstrapConfig{}, errors.NewSDKError(err)
}
return bc, nil
}
func (sdk mgSDK) BootstrapSecure(externalID, externalKey, cryptoKey string) (BootstrapConfig, errors.SDKError) {
if externalID == "" {
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, bootstrapEndpoint, secureEndpoint, externalID)
encExtKey, err := bootstrapEncrypt([]byte(externalKey), cryptoKey)
if err != nil {
return BootstrapConfig{}, errors.NewSDKError(err)
}
_, body, sdkErr := sdk.processRequest(http.MethodGet, url, ClientPrefix+encExtKey, nil, nil, http.StatusOK)
if sdkErr != nil {
return BootstrapConfig{}, sdkErr
}
decBody, decErr := bootstrapDecrypt(body, cryptoKey)
if decErr != nil {
return BootstrapConfig{}, errors.NewSDKError(decErr)
}
var bc BootstrapConfig
if err := json.Unmarshal(decBody, &bc); err != nil {
return BootstrapConfig{}, errors.NewSDKError(err)
}
return bc, nil
}
func bootstrapEncrypt(in []byte, cryptoKey string) (string, error) {
block, err := aes.NewCipher([]byte(cryptoKey))
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(in))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
return hex.EncodeToString(ciphertext), nil
}
func bootstrapDecrypt(in []byte, cryptoKey string) ([]byte, error) {
ciphertext := in
block, err := aes.NewCipher([]byte(cryptoKey))
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, err
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return ciphertext, nil
}
File diff suppressed because it is too large Load Diff
-2
View File
@@ -36,8 +36,6 @@ func (sdk mgSDK) Health(service string) (HealthInfo, errors.SDKError) {
url = fmt.Sprintf("%s/health", sdk.clientsURL)
case "users":
url = fmt.Sprintf("%s/health", sdk.usersURL)
case "bootstrap":
url = fmt.Sprintf("%s/health", sdk.bootstrapURL)
case "certs":
url = fmt.Sprintf("%s/health", sdk.certsURL)
case "reader":
-25
View File
@@ -9,11 +9,8 @@ import (
"testing"
"github.com/absmach/supermq"
"github.com/absmach/supermq/bootstrap/api"
bmocks "github.com/absmach/supermq/bootstrap/mocks"
chmocks "github.com/absmach/supermq/channels/mocks"
climocks "github.com/absmach/supermq/clients/mocks"
smqlog "github.com/absmach/supermq/logger"
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
"github.com/absmach/supermq/pkg/errors"
sdk "github.com/absmach/supermq/pkg/sdk"
@@ -32,9 +29,6 @@ func TestHealth(t *testing.T) {
certsTs, _, _ := setupCerts()
defer certsTs.Close()
bootstrapTs := setupMinimalBootstrap()
defer bootstrapTs.Close()
readerTs := setupMinimalReader()
defer readerTs.Close()
@@ -45,7 +39,6 @@ func TestHealth(t *testing.T) {
ClientsURL: clientsTs.URL,
UsersURL: usersTs.URL,
CertsURL: certsTs.URL,
BootstrapURL: bootstrapTs.URL,
ReaderURL: readerTs.URL,
HTTPAdapterURL: httpAdapterTs.URL,
MsgContentType: contentType,
@@ -85,14 +78,6 @@ func TestHealth(t *testing.T) {
description: "certs service",
status: "pass",
},
{
desc: "get bootstrap service health check",
service: "bootstrap",
empty: false,
err: nil,
description: "bootstrap service",
status: "pass",
},
{
desc: "get reader service health check",
service: "reader",
@@ -123,16 +108,6 @@ func TestHealth(t *testing.T) {
}
}
func setupMinimalBootstrap() *httptest.Server {
bsvc := new(bmocks.Service)
reader := new(bmocks.ConfigReader)
logger := smqlog.NewMock()
authn := new(authnmocks.Authentication)
mux := api.MakeHandler(bsvc, authn, reader, logger, "")
return httptest.NewServer(mux)
}
func setupMinimalReader() *httptest.Server {
repo := new(readersmocks.MessageRepository)
channels := new(chmocks.ChannelsServiceClient)
-564
View File
@@ -75,66 +75,6 @@ func (_c *SDK_AcceptInvitation_Call) RunAndReturn(run func(string, string) error
return _c
}
// AddBootstrap provides a mock function with given fields: cfg, domainID, token
func (_m *SDK) AddBootstrap(cfg sdk.BootstrapConfig, domainID string, token string) (string, errors.SDKError) {
ret := _m.Called(cfg, domainID, token)
if len(ret) == 0 {
panic("no return value specified for AddBootstrap")
}
var r0 string
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(sdk.BootstrapConfig, string, string) (string, errors.SDKError)); ok {
return rf(cfg, domainID, token)
}
if rf, ok := ret.Get(0).(func(sdk.BootstrapConfig, string, string) string); ok {
r0 = rf(cfg, domainID, token)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(sdk.BootstrapConfig, string, string) errors.SDKError); ok {
r1 = rf(cfg, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_AddBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddBootstrap'
type SDK_AddBootstrap_Call struct {
*mock.Call
}
// AddBootstrap is a helper method to define mock.On call
// - cfg sdk.BootstrapConfig
// - domainID string
// - token string
func (_e *SDK_Expecter) AddBootstrap(cfg interface{}, domainID interface{}, token interface{}) *SDK_AddBootstrap_Call {
return &SDK_AddBootstrap_Call{Call: _e.mock.On("AddBootstrap", cfg, domainID, token)}
}
func (_c *SDK_AddBootstrap_Call) Run(run func(cfg sdk.BootstrapConfig, domainID string, token string)) *SDK_AddBootstrap_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(sdk.BootstrapConfig), args[1].(string), args[2].(string))
})
return _c
}
func (_c *SDK_AddBootstrap_Call) Return(_a0 string, _a1 errors.SDKError) *SDK_AddBootstrap_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SDK_AddBootstrap_Call) RunAndReturn(run func(sdk.BootstrapConfig, string, string) (string, errors.SDKError)) *SDK_AddBootstrap_Call {
_c.Call.Return(run)
return _c
}
// AddChildren provides a mock function with given fields: id, domainID, groupIDs, token
func (_m *SDK) AddChildren(id string, domainID string, groupIDs []string, token string) errors.SDKError {
ret := _m.Called(id, domainID, groupIDs, token)
@@ -750,185 +690,6 @@ func (_c *SDK_AvailableGroupRoleActions_Call) RunAndReturn(run func(string, stri
return _c
}
// Bootstrap provides a mock function with given fields: externalID, externalKey
func (_m *SDK) Bootstrap(externalID string, externalKey string) (sdk.BootstrapConfig, errors.SDKError) {
ret := _m.Called(externalID, externalKey)
if len(ret) == 0 {
panic("no return value specified for Bootstrap")
}
var r0 sdk.BootstrapConfig
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
return rf(externalID, externalKey)
}
if rf, ok := ret.Get(0).(func(string, string) sdk.BootstrapConfig); ok {
r0 = rf(externalID, externalKey)
} else {
r0 = ret.Get(0).(sdk.BootstrapConfig)
}
if rf, ok := ret.Get(1).(func(string, string) errors.SDKError); ok {
r1 = rf(externalID, externalKey)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_Bootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bootstrap'
type SDK_Bootstrap_Call struct {
*mock.Call
}
// Bootstrap is a helper method to define mock.On call
// - externalID string
// - externalKey string
func (_e *SDK_Expecter) Bootstrap(externalID interface{}, externalKey interface{}) *SDK_Bootstrap_Call {
return &SDK_Bootstrap_Call{Call: _e.mock.On("Bootstrap", externalID, externalKey)}
}
func (_c *SDK_Bootstrap_Call) Run(run func(externalID string, externalKey string)) *SDK_Bootstrap_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *SDK_Bootstrap_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_Bootstrap_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SDK_Bootstrap_Call) RunAndReturn(run func(string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_Bootstrap_Call {
_c.Call.Return(run)
return _c
}
// BootstrapSecure provides a mock function with given fields: externalID, externalKey, cryptoKey
func (_m *SDK) BootstrapSecure(externalID string, externalKey string, cryptoKey string) (sdk.BootstrapConfig, errors.SDKError) {
ret := _m.Called(externalID, externalKey, cryptoKey)
if len(ret) == 0 {
panic("no return value specified for BootstrapSecure")
}
var r0 sdk.BootstrapConfig
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
return rf(externalID, externalKey, cryptoKey)
}
if rf, ok := ret.Get(0).(func(string, string, string) sdk.BootstrapConfig); ok {
r0 = rf(externalID, externalKey, cryptoKey)
} else {
r0 = ret.Get(0).(sdk.BootstrapConfig)
}
if rf, ok := ret.Get(1).(func(string, string, string) errors.SDKError); ok {
r1 = rf(externalID, externalKey, cryptoKey)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_BootstrapSecure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BootstrapSecure'
type SDK_BootstrapSecure_Call struct {
*mock.Call
}
// BootstrapSecure is a helper method to define mock.On call
// - externalID string
// - externalKey string
// - cryptoKey string
func (_e *SDK_Expecter) BootstrapSecure(externalID interface{}, externalKey interface{}, cryptoKey interface{}) *SDK_BootstrapSecure_Call {
return &SDK_BootstrapSecure_Call{Call: _e.mock.On("BootstrapSecure", externalID, externalKey, cryptoKey)}
}
func (_c *SDK_BootstrapSecure_Call) Run(run func(externalID string, externalKey string, cryptoKey string)) *SDK_BootstrapSecure_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string))
})
return _c
}
func (_c *SDK_BootstrapSecure_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_BootstrapSecure_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SDK_BootstrapSecure_Call) RunAndReturn(run func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_BootstrapSecure_Call {
_c.Call.Return(run)
return _c
}
// Bootstraps provides a mock function with given fields: pm, domainID, token
func (_m *SDK) Bootstraps(pm sdk.PageMetadata, domainID string, token string) (sdk.BootstrapPage, errors.SDKError) {
ret := _m.Called(pm, domainID, token)
if len(ret) == 0 {
panic("no return value specified for Bootstraps")
}
var r0 sdk.BootstrapPage
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string, string) (sdk.BootstrapPage, errors.SDKError)); ok {
return rf(pm, domainID, token)
}
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string, string) sdk.BootstrapPage); ok {
r0 = rf(pm, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapPage)
}
if rf, ok := ret.Get(1).(func(sdk.PageMetadata, string, string) errors.SDKError); ok {
r1 = rf(pm, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_Bootstraps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bootstraps'
type SDK_Bootstraps_Call struct {
*mock.Call
}
// Bootstraps is a helper method to define mock.On call
// - pm sdk.PageMetadata
// - domainID string
// - token string
func (_e *SDK_Expecter) Bootstraps(pm interface{}, domainID interface{}, token interface{}) *SDK_Bootstraps_Call {
return &SDK_Bootstraps_Call{Call: _e.mock.On("Bootstraps", pm, domainID, token)}
}
func (_c *SDK_Bootstraps_Call) Run(run func(pm sdk.PageMetadata, domainID string, token string)) *SDK_Bootstraps_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(sdk.PageMetadata), args[1].(string), args[2].(string))
})
return _c
}
func (_c *SDK_Bootstraps_Call) Return(_a0 sdk.BootstrapPage, _a1 errors.SDKError) *SDK_Bootstraps_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SDK_Bootstraps_Call) RunAndReturn(run func(sdk.PageMetadata, string, string) (sdk.BootstrapPage, errors.SDKError)) *SDK_Bootstraps_Call {
_c.Call.Return(run)
return _c
}
// Channel provides a mock function with given fields: id, domainID, token
func (_m *SDK) Channel(id string, domainID string, token string) (sdk.Channel, errors.SDKError) {
ret := _m.Called(id, domainID, token)
@@ -5442,56 +5203,6 @@ func (_c *SDK_RemoveAllGroupRoleMembers_Call) RunAndReturn(run func(string, stri
return _c
}
// RemoveBootstrap provides a mock function with given fields: id, domainID, token
func (_m *SDK) RemoveBootstrap(id string, domainID string, token string) errors.SDKError {
ret := _m.Called(id, domainID, token)
if len(ret) == 0 {
panic("no return value specified for RemoveBootstrap")
}
var r0 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string, string) errors.SDKError); ok {
r0 = rf(id, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_RemoveBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveBootstrap'
type SDK_RemoveBootstrap_Call struct {
*mock.Call
}
// RemoveBootstrap is a helper method to define mock.On call
// - id string
// - domainID string
// - token string
func (_e *SDK_Expecter) RemoveBootstrap(id interface{}, domainID interface{}, token interface{}) *SDK_RemoveBootstrap_Call {
return &SDK_RemoveBootstrap_Call{Call: _e.mock.On("RemoveBootstrap", id, domainID, token)}
}
func (_c *SDK_RemoveBootstrap_Call) Run(run func(id string, domainID string, token string)) *SDK_RemoveBootstrap_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string))
})
return _c
}
func (_c *SDK_RemoveBootstrap_Call) Return(_a0 errors.SDKError) *SDK_RemoveBootstrap_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *SDK_RemoveBootstrap_Call) RunAndReturn(run func(string, string, string) errors.SDKError) *SDK_RemoveBootstrap_Call {
_c.Call.Return(run)
return _c
}
// RemoveChannelParent provides a mock function with given fields: id, domainID, groupID, token
func (_m *SDK) RemoveChannelParent(id string, domainID string, groupID string, token string) errors.SDKError {
ret := _m.Called(id, domainID, groupID, token)
@@ -6521,170 +6232,6 @@ func (_c *SDK_SetGroupParent_Call) RunAndReturn(run func(string, string, string,
return _c
}
// UpdateBootstrap provides a mock function with given fields: cfg, domainID, token
func (_m *SDK) UpdateBootstrap(cfg sdk.BootstrapConfig, domainID string, token string) errors.SDKError {
ret := _m.Called(cfg, domainID, token)
if len(ret) == 0 {
panic("no return value specified for UpdateBootstrap")
}
var r0 errors.SDKError
if rf, ok := ret.Get(0).(func(sdk.BootstrapConfig, string, string) errors.SDKError); ok {
r0 = rf(cfg, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_UpdateBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrap'
type SDK_UpdateBootstrap_Call struct {
*mock.Call
}
// UpdateBootstrap is a helper method to define mock.On call
// - cfg sdk.BootstrapConfig
// - domainID string
// - token string
func (_e *SDK_Expecter) UpdateBootstrap(cfg interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrap_Call {
return &SDK_UpdateBootstrap_Call{Call: _e.mock.On("UpdateBootstrap", cfg, domainID, token)}
}
func (_c *SDK_UpdateBootstrap_Call) Run(run func(cfg sdk.BootstrapConfig, domainID string, token string)) *SDK_UpdateBootstrap_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(sdk.BootstrapConfig), args[1].(string), args[2].(string))
})
return _c
}
func (_c *SDK_UpdateBootstrap_Call) Return(_a0 errors.SDKError) *SDK_UpdateBootstrap_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *SDK_UpdateBootstrap_Call) RunAndReturn(run func(sdk.BootstrapConfig, string, string) errors.SDKError) *SDK_UpdateBootstrap_Call {
_c.Call.Return(run)
return _c
}
// UpdateBootstrapCerts provides a mock function with given fields: id, clientCert, clientKey, ca, domainID, token
func (_m *SDK) UpdateBootstrapCerts(id string, clientCert string, clientKey string, ca string, domainID string, token string) (sdk.BootstrapConfig, errors.SDKError) {
ret := _m.Called(id, clientCert, clientKey, ca, domainID, token)
if len(ret) == 0 {
panic("no return value specified for UpdateBootstrapCerts")
}
var r0 sdk.BootstrapConfig
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
return rf(id, clientCert, clientKey, ca, domainID, token)
}
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) sdk.BootstrapConfig); ok {
r0 = rf(id, clientCert, clientKey, ca, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapConfig)
}
if rf, ok := ret.Get(1).(func(string, string, string, string, string, string) errors.SDKError); ok {
r1 = rf(id, clientCert, clientKey, ca, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_UpdateBootstrapCerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrapCerts'
type SDK_UpdateBootstrapCerts_Call struct {
*mock.Call
}
// UpdateBootstrapCerts is a helper method to define mock.On call
// - id string
// - clientCert string
// - clientKey string
// - ca string
// - domainID string
// - token string
func (_e *SDK_Expecter) UpdateBootstrapCerts(id interface{}, clientCert interface{}, clientKey interface{}, ca interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrapCerts_Call {
return &SDK_UpdateBootstrapCerts_Call{Call: _e.mock.On("UpdateBootstrapCerts", id, clientCert, clientKey, ca, domainID, token)}
}
func (_c *SDK_UpdateBootstrapCerts_Call) Run(run func(id string, clientCert string, clientKey string, ca string, domainID string, token string)) *SDK_UpdateBootstrapCerts_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string))
})
return _c
}
func (_c *SDK_UpdateBootstrapCerts_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_UpdateBootstrapCerts_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SDK_UpdateBootstrapCerts_Call) RunAndReturn(run func(string, string, string, string, string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_UpdateBootstrapCerts_Call {
_c.Call.Return(run)
return _c
}
// UpdateBootstrapConnection provides a mock function with given fields: id, channels, domainID, token
func (_m *SDK) UpdateBootstrapConnection(id string, channels []string, domainID string, token string) errors.SDKError {
ret := _m.Called(id, channels, domainID, token)
if len(ret) == 0 {
panic("no return value specified for UpdateBootstrapConnection")
}
var r0 errors.SDKError
if rf, ok := ret.Get(0).(func(string, []string, string, string) errors.SDKError); ok {
r0 = rf(id, channels, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_UpdateBootstrapConnection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrapConnection'
type SDK_UpdateBootstrapConnection_Call struct {
*mock.Call
}
// UpdateBootstrapConnection is a helper method to define mock.On call
// - id string
// - channels []string
// - domainID string
// - token string
func (_e *SDK_Expecter) UpdateBootstrapConnection(id interface{}, channels interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrapConnection_Call {
return &SDK_UpdateBootstrapConnection_Call{Call: _e.mock.On("UpdateBootstrapConnection", id, channels, domainID, token)}
}
func (_c *SDK_UpdateBootstrapConnection_Call) Run(run func(id string, channels []string, domainID string, token string)) *SDK_UpdateBootstrapConnection_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].([]string), args[2].(string), args[3].(string))
})
return _c
}
func (_c *SDK_UpdateBootstrapConnection_Call) Return(_a0 errors.SDKError) *SDK_UpdateBootstrapConnection_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *SDK_UpdateBootstrapConnection_Call) RunAndReturn(run func(string, []string, string, string) errors.SDKError) *SDK_UpdateBootstrapConnection_Call {
_c.Call.Return(run)
return _c
}
// UpdateChannel provides a mock function with given fields: channel, domainID, token
func (_m *SDK) UpdateChannel(channel sdk.Channel, domainID string, token string) (sdk.Channel, errors.SDKError) {
ret := _m.Called(channel, domainID, token)
@@ -7880,66 +7427,6 @@ func (_c *SDK_Users_Call) RunAndReturn(run func(sdk.PageMetadata, string) (sdk.U
return _c
}
// ViewBootstrap provides a mock function with given fields: id, domainID, token
func (_m *SDK) ViewBootstrap(id string, domainID string, token string) (sdk.BootstrapConfig, errors.SDKError) {
ret := _m.Called(id, domainID, token)
if len(ret) == 0 {
panic("no return value specified for ViewBootstrap")
}
var r0 sdk.BootstrapConfig
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
return rf(id, domainID, token)
}
if rf, ok := ret.Get(0).(func(string, string, string) sdk.BootstrapConfig); ok {
r0 = rf(id, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapConfig)
}
if rf, ok := ret.Get(1).(func(string, string, string) errors.SDKError); ok {
r1 = rf(id, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_ViewBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewBootstrap'
type SDK_ViewBootstrap_Call struct {
*mock.Call
}
// ViewBootstrap is a helper method to define mock.On call
// - id string
// - domainID string
// - token string
func (_e *SDK_Expecter) ViewBootstrap(id interface{}, domainID interface{}, token interface{}) *SDK_ViewBootstrap_Call {
return &SDK_ViewBootstrap_Call{Call: _e.mock.On("ViewBootstrap", id, domainID, token)}
}
func (_c *SDK_ViewBootstrap_Call) Run(run func(id string, domainID string, token string)) *SDK_ViewBootstrap_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string))
})
return _c
}
func (_c *SDK_ViewBootstrap_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_ViewBootstrap_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SDK_ViewBootstrap_Call) RunAndReturn(run func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_ViewBootstrap_Call {
_c.Call.Return(run)
return _c
}
// ViewCert provides a mock function with given fields: certID, domainID, token
func (_m *SDK) ViewCert(certID string, domainID string, token string) (sdk.Cert, errors.SDKError) {
ret := _m.Called(certID, domainID, token)
@@ -8119,57 +7606,6 @@ func (_c *SDK_ViewSubscription_Call) RunAndReturn(run func(string, string) (sdk.
return _c
}
// Whitelist provides a mock function with given fields: clientID, state, domainID, token
func (_m *SDK) Whitelist(clientID string, state int, domainID string, token string) errors.SDKError {
ret := _m.Called(clientID, state, domainID, token)
if len(ret) == 0 {
panic("no return value specified for Whitelist")
}
var r0 errors.SDKError
if rf, ok := ret.Get(0).(func(string, int, string, string) errors.SDKError); ok {
r0 = rf(clientID, state, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_Whitelist_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Whitelist'
type SDK_Whitelist_Call struct {
*mock.Call
}
// Whitelist is a helper method to define mock.On call
// - clientID string
// - state int
// - domainID string
// - token string
func (_e *SDK_Expecter) Whitelist(clientID interface{}, state interface{}, domainID interface{}, token interface{}) *SDK_Whitelist_Call {
return &SDK_Whitelist_Call{Call: _e.mock.On("Whitelist", clientID, state, domainID, token)}
}
func (_c *SDK_Whitelist_Call) Run(run func(clientID string, state int, domainID string, token string)) *SDK_Whitelist_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(int), args[2].(string), args[3].(string))
})
return _c
}
func (_c *SDK_Whitelist_Call) Return(_a0 errors.SDKError) *SDK_Whitelist_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *SDK_Whitelist_Call) RunAndReturn(run func(string, int, string, string) errors.SDKError) *SDK_Whitelist_Call {
_c.Call.Return(run)
return _c
}
// NewSDK creates a new instance of SDK. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewSDK(t interface {
-6
View File
@@ -67,12 +67,6 @@ type revokeCertsRes struct {
RevocationTime time.Time `json:"revocation_time"`
}
// bootstrapsPage contains list of bootstrap configs in a page with proper metadata.
type BootstrapPage struct {
Configs []BootstrapConfig `json:"configs"`
PageRes
}
type CertSerials struct {
Certs []Cert `json:"certs"`
PageRes
-91
View File
@@ -1069,94 +1069,6 @@ type SDK interface {
// fmt.Println(health)
Health(service string) (HealthInfo, errors.SDKError)
// AddBootstrap add bootstrap configuration
//
// example:
// cfg := sdk.BootstrapConfig{
// ClientID: "clientID",
// Name: "bootstrap",
// ExternalID: "externalID",
// ExternalKey: "externalKey",
// Channels: []string{"channel1", "channel2"},
// }
// id, _ := sdk.AddBootstrap(cfg, "domainID", "token")
// fmt.Println(id)
AddBootstrap(cfg BootstrapConfig, domainID, token string) (string, errors.SDKError)
// View returns Client Config with given ID belonging to the user identified by the given token.
//
// example:
// bootstrap, _ := sdk.ViewBootstrap("id", "domainID", "token")
// fmt.Println(bootstrap)
ViewBootstrap(id, domainID, token string) (BootstrapConfig, errors.SDKError)
// Update updates editable fields of the provided Config.
//
// example:
// cfg := sdk.BootstrapConfig{
// ClientID: "clientID",
// Name: "bootstrap",
// ExternalID: "externalID",
// ExternalKey: "externalKey",
// Channels: []string{"channel1", "channel2"},
// }
// err := sdk.UpdateBootstrap(cfg, "domainID", "token")
// fmt.Println(err)
UpdateBootstrap(cfg BootstrapConfig, domainID, token string) errors.SDKError
// Update bootstrap config certificates.
//
// example:
// err := sdk.UpdateBootstrapCerts("id", "clientCert", "clientKey", "ca", "domainID", "token")
// fmt.Println(err)
UpdateBootstrapCerts(id string, clientCert, clientKey, ca string, domainID, token string) (BootstrapConfig, errors.SDKError)
// UpdateBootstrapConnection updates connections performs update of the channel list corresponding Client is connected to.
//
// example:
// err := sdk.UpdateBootstrapConnection("id", []string{"channel1", "channel2"}, "domainID", "token")
// fmt.Println(err)
UpdateBootstrapConnection(id string, channels []string, domainID, token string) errors.SDKError
// Remove removes Config with specified token that belongs to the user identified by the given token.
//
// example:
// err := sdk.RemoveBootstrap("id", "domainID", "token")
// fmt.Println(err)
RemoveBootstrap(id, domainID, token string) errors.SDKError
// Bootstrap returns Config to the Client with provided external ID using external key.
//
// example:
// bootstrap, _ := sdk.Bootstrap("externalID", "externalKey")
// fmt.Println(bootstrap)
Bootstrap(externalID, externalKey string) (BootstrapConfig, errors.SDKError)
// BootstrapSecure retrieves a configuration with given external ID and encrypted external key.
//
// example:
// bootstrap, _ := sdk.BootstrapSecure("externalID", "externalKey", "cryptoKey")
// fmt.Println(bootstrap)
BootstrapSecure(externalID, externalKey, cryptoKey string) (BootstrapConfig, errors.SDKError)
// Bootstraps retrieves a list of managed configs.
//
// example:
// pm := sdk.PageMetadata{
// Offset: 0,
// Limit: 10,
// }
// bootstraps, _ := sdk.Bootstraps(pm, "domainID", "token")
// fmt.Println(bootstraps)
Bootstraps(pm PageMetadata, domainID, token string) (BootstrapPage, errors.SDKError)
// Whitelist updates Client state Config with given ID belonging to the user identified by the given token.
//
// example:
// err := sdk.Whitelist("clientID", 1, "domainID", "token")
// fmt.Println(err)
Whitelist(clientID string, state int, domainID, token string) errors.SDKError
// IssueCert issues a certificate for a client required for mTLS.
//
// example:
@@ -1456,7 +1368,6 @@ type SDK interface {
}
type mgSDK struct {
bootstrapURL string
certsURL string
httpAdapterURL string
readerURL string
@@ -1476,7 +1387,6 @@ type mgSDK struct {
// Config contains sdk configuration parameters.
type Config struct {
BootstrapURL string
CertsURL string
HTTPAdapterURL string
ReaderURL string
@@ -1497,7 +1407,6 @@ type Config struct {
// NewSDK returns new supermq SDK instance.
func NewSDK(conf Config) SDK {
return &mgSDK{
bootstrapURL: conf.BootstrapURL,
certsURL: conf.CertsURL,
httpAdapterURL: conf.HTTPAdapterURL,
readerURL: conf.ReaderURL,
-194
View File
@@ -1,194 +0,0 @@
# Provision service
Provision service provides an HTTP API to interact with [SuperMQ][supermq].
Provision service is used to setup initial applications configuration i.e. clients, channels, connections and certificates that will be required for the specific use case especially useful for gateway provision.
For gateways to communicate with [SuperMQ][supermq] configuration is required (mqtt host, client, channels, certificates...). To get the configuration gateway will send a request to [Bootstrap][bootstrap] service providing `<external_id>` and `<external_key>` in request. To make a request to [Bootstrap][bootstrap] service you can use [Agent][agent] service on a gateway.
To create bootstrap configuration you can use [Bootstrap][bootstrap] or `Provision` service. [SuperMQ UI][mgxui] uses [Bootstrap][bootstrap] service for creating gateway configurations. `Provision` service should provide an easy way of provisioning your gateways i.e creating bootstrap configuration and as many clients and channels that your setup requires.
Also you may use provision service to create certificates for each client. Each service running on gateway may require more than one client and channel for communication. Let's say that you are using services [Agent][agent] and [Export][export] on a gateway you will need two channels for `Agent` (`data` and `control`) and one for `Export` and one client. Additionally if you enabled mtls each service will need its own client and certificate for access to [SuperMQ][supermq]. Your setup could require any number of clients and channels this kind of setup we can call `provision layout`.
Provision service provides a way of specifying this `provision layout` and creating a setup according to that layout by serving requests on `/mapping` endpoint. Provision layout is configured in [config.toml](configs/config.toml).
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
| ------------------------------------ | -------------------------------------------------- | ----------------------- |
| SMQ_PROVISION_LOG_LEVEL | Service log level | debug |
| SMQ_PROVISION_USER | User (email) for accessing SuperMQ | <user@example.com> |
| SMQ_PROVISION_PASS | SuperMQ password | user123 |
| SMQ_PROVISION_API_KEY | SuperMQ authentication token | |
| SMQ_PROVISION_CONFIG_FILE | Provision config file | config.toml |
| SMQ_PROVISION_HTTP_PORT | Provision service listening port | 9016 |
| SMQ_PROVISION_ENV_CLIENTS_TLS | SuperMQ SDK TLS verification | false |
| SMQ_PROVISION_SERVER_CERT | SuperMQ gRPC secure server cert | |
| SMQ_PROVISION_SERVER_KEY | SuperMQ gRPC secure server key | |
| SMQ_PROVISION_USERS_LOCATION | Users service URL | <http://users:9002> |
| SMQ_PROVISION_CLIENTS_LOCATION | Clients service URL | <http://clients:9000> |
| SMQ_PROVISION_BS_SVC_URL | SuperMQ Bootstrap service URL | <http://bootstrap:9013> |
| SMQ_PROVISION_CERTS_SVC_URL | Certificates service URL | <http://certs:9019> |
| SMQ_PROVISION_X509_PROVISIONING | Should X509 client cert be provisioned | false |
| SMQ_PROVISION_BS_CONFIG_PROVISIONING | Should client config be saved in Bootstrap service | true |
| SMQ_PROVISION_BS_AUTO_WHITELIST | Should client be auto whitelisted | true |
| SMQ_PROVISION_BS_CONTENT | Bootstrap service configs content, JSON format | {} |
| SMQ_PROVISION_CERTS_RSA_BITS | Certificate RSA bits parameter | 4096 |
| SMQ_PROVISION_CERTS_HOURS_VALID | Number of hours that certificate is valid | "2400h" |
| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true |
By default, call to `/mapping` endpoint will create one client and two channels (`control` and `data`) and connect it. If there is a requirement for different provision layout we can use [config](docker/configs/config.toml) file in addition to environment variables.
For the purposes of running provision as an add-on in docker composition environment variables seems more suitable. Environment variables are set in [.env](.env).
Configuration can be specified in [config.toml](configs/config.toml). Config file can specify all the settings that environment variables can configure and in addition
`/mapping` endpoint provision layout can be configured.
In `config.toml` we can enlist array of clients and channels that we want to create and make connections between them which we call provision layout.
Metadata can be whatever suits your needs except that at least one client needs to have `external_id` (which is populated with value from [request](#example)). Client that has `external_id` will be used for creating bootstrap configuration which can be fetched with [Agent][agent].
For channels metadata `type` is reserved for `control` and `data` which we use with [Agent][agent].
Example of provision layout below
```toml
[[clients]]
name = "client"
[clients.metadata]
external_id = "xxxxxx"
[[channels]]
name = "control-channel"
[channels.metadata]
type = "control"
[[channels]]
name = "data-channel"
[channels.metadata]
type = "data"
[[channels]]
name = "export-channel"
[channels.metadata]
type = "data"
```
## Authentication
In order to create necessary entities provision service needs to authenticate against SuperMQ. To provide authentication credentials to the provision service you can pass it in an environment variable or in a config file as SuperMQ user and password or as API token that can be issued on `/users/tokens/issue`.
Additionally users or API token can be passed in Authorization header, this authentication takes precedence over others.
- `username`, `password` - (`SMQ_PROVISION_USER`, `SMQ_PROVISION_PASSWORD` in [.env](../.env), `mg_user`, `mg_pass` in [config.toml](../docker/addons/provision/configs/config.toml))
- API Key - (`SMQ_PROVISION_API_KEY` in [.env](../.env) or [config.toml](../docker/addons/provision/configs/config.toml))
- `Authorization: Bearer Token` - request authorization header containing either users token.
## Running
Provision service can be run as a standalone or in docker composition as addon to the core docker composition.
Standalone:
```bash
SMQ_PROVISION_BS_SVC_URL=http://localhost:9013 \
SMQ_PROVISION_CLIENTS_LOCATION=http://localhost:9000 \
SMQ_PROVISION_USERS_LOCATION=http://localhost:9002 \
SMQ_PROVISION_CONFIG_FILE=docker/addons/provision/configs/config.toml \
build/supermq-provision
```
Docker composition:
```bash
docker compose -f docker/addons/provision/docker-compose.yml up
```
For the case that credentials or API token is passed in configuration file or environment variables, call to `/mapping` endpoint doesn't require `Authentication` header:
```bash
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/mapping -H 'Content-Type: application/json' -d '{"external_id": "33:52:77:99:43", "external_key": "223334fw2"}'
```
In the case that provision service is not deployed with credentials or API key or you want to use user other than one being set in environment (or config file):
```bash
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/mapping -H "Authorization: Bearer <token|api_key>" -H 'Content-Type: application/json' -d '{"external_id": "<external_id>", "external_key": "<external_key>"}'
```
Or if you want to specify a name for client different than in `config.toml` you can specify post data as:
```json
{
"name": "<name>",
"external_id": "<external_id>",
"external_key": "<external_key>"
}
```
Response contains created clients, channels and certificates if any:
```json
{
"clients": [
{
"id": "c22b0c0f-8c03-40da-a06b-37ed3a72c8d1",
"name": "client",
"key": "007cce56-e0eb-40d6-b2b9-ed348a97d1eb",
"metadata": {
"external_id": "33:52:79:C3:43"
}
}
],
"channels": [
{
"id": "064c680e-181b-4b58-975e-6983313a5170",
"name": "control-channel",
"metadata": {
"type": "control"
}
},
{
"id": "579da92d-6078-4801-a18a-dd1cfa2aa44f",
"name": "data-channel",
"metadata": {
"type": "data"
}
}
],
"whitelisted": {
"c22b0c0f-8c03-40da-a06b-37ed3a72c8d1": true
}
}
```
## Certificates
Provision service has `/certs` endpoint that can be used to generate certificates for clients when mTLS is required:
- `users_token` - users authentication token or API token
- `client_id` - id of the client for which certificate is going to be generated
```bash
curl -s -X POST http://localhost:8190/certs -H "Authorization: Bearer <users_token>" -H 'Content-Type: application/json' -d '{"client_id": "<client_id>", "ttl":"2400h" }'
```
```json
{
"client_cert": "-----BEGIN CERTIFICATE-----\nMIIEmDCCA4CgAwIBAgIQCZ0NOq2oKLo+XftbAu0TfzANBgkqhkiG9w0BAQsFADBX\nMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoMCE1haW5mbHV4MQwwCgYDVQQL\nDANJb1QxIDAeBgkqhkiG9w0BCQEWEWluZm9AbWFpbmZsdXguY29tMB4XDTIwMDYw\nNTEyMzc1M1oXDTIwMDkxMzEyMzc1M1owVTERMA8GA1UEChMITWFpbmZsdXgxETAP\nBgNVBAsTCG1haW5mbHV4MS0wKwYDVQQDEyQyYmZlYmZmMC05ODZhLTQ3ZTAtOGQ3\nYS00YTRiN2UyYjU3OGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCn\nWvTuOIdhqOLEREcEJqfQAtDoYu3rUDijOffXuWFZgNqfZTGmoD5ZqJXxwbZ4tCST\npdSteHtyr7JXnPJQN1dsslU+q3haKjFoZRc39/7u4/8XCTwlqbMl9YVcwqS+FLkM\niLSyyqzryP7Y8H8cidTKg56p5JALaEKfzZS6Km3G+CCinR6hNNW9ckWsy29a0/9E\nMAUtM+Lsk5OjsHzOnWruuqHsCx4ODI5aJQaMC1qntkbXkht0WDiwAt9SDQ3uLWru\nAoSJDK9a6EgR3a0Jf7ZiVPiwlZNjrB/I5OQyFDGqcmSAl2rdJqPkmaDXKKFyL1cG\nMIyHv62QzJoMdRoXu20lxyGxAvEjQNVHux4LA3dbf/85nEVTI2uP8crMf2Jnzbg5\n9zF+iTMJGpUlatCyK2RJS/mvHbbUIf5Ro3VbcPHbgFroJ7qMFz0Fc5kYY8IdwXjG\nlyG9MobKEO2CfBGRjPmCuTQq2HcuOy7F6KfQf3HToI8MmC5hBtCmTNbV8I3GIjWA\n/xJQLm2pVZ41QhrnNGtuqAYoe3Zt6OldxGRcoAj7KlIpYcPZ55PJ6mWcV6dB9Fnl\n5mYOwQL8jtfybbGWvqJldhTxUqm7/EbAaF0Qjmh4oOHMl2xADrmYzJHvf0llwr6g\noRQuzqxPi0aW3tkFNsm63NX1Ab5BXFQhMSj5+82blwIDAQABo2IwYDAOBgNVHQ8B\nAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQH\nBAUBAgMEBjAfBgNVHSMEGDAWgBRs4xR91qEjNRGmw391xS7x6Tc+8jANBgkqhkiG\n9w0BAQsFAAOCAQEAphLT8PjawRRWswU1B5oWnnqeTllnvGB88sjDPLAG0UiBlDLX\nwoPiBVPWuYV+MMJuaREgheYF1Ahx4Jrfy9stFDU7B99ON1T58oM1aKEq4rKc+/Ke\nyxrAFTonclC0LNaaOvpZZjsPFWr2muTQO8XHiS8icw3BLxEzoF+5aJ8ihtxRtfKL\nUvtHDqC6IPAbSUcvqyjrFh3RrTUAyGOzW12IEWSXP9DLwoiLPwJ6kCVoXdG/asjz\nUpk/jj7AUn9oJNF8nUbyhdOnmeJ2z0x1ylgYrIAxvGzm8zs+NEVN67CrBYKwstlN\nvw7DRQsCvGJjZzWj28VV3FGLtXFgu52bFZNBww==\n-----END CERTIFICATE-----\n",
"client_cert_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIJJwIBAAKCAgEAp1r07jiHYajixERHBCan0ALQ6GLt61A4ozn317lhWYDan2Ux\npqA+WaiV8cG2eLQkk6XUrXh7cq+yV5zyUDdXbLJVPqt4WioxaGUXN/f+7uP/Fwk8\nJamzJfWFXMKkvhS5DIi0ssqs68j+2PB/HInUyoOeqeSQC2hCn82Uuiptxvggop0e\noTTVvXJFrMtvWtP/RDAFLTPi7JOTo7B8zp1q7rqh7AseDgyOWiUGjAtap7ZG15Ib\ndFg4sALfUg0N7i1q7gKEiQyvWuhIEd2tCX+2YlT4sJWTY6wfyOTkMhQxqnJkgJdq\n3Saj5Jmg1yihci9XBjCMh7+tkMyaDHUaF7ttJcchsQLxI0DVR7seCwN3W3//OZxF\nUyNrj/HKzH9iZ824OfcxfokzCRqVJWrQsitkSUv5rx221CH+UaN1W3Dx24Ba6Ce6\njBc9BXOZGGPCHcF4xpchvTKGyhDtgnwRkYz5grk0Kth3Ljsuxein0H9x06CPDJgu\nYQbQpkzW1fCNxiI1gP8SUC5tqVWeNUIa5zRrbqgGKHt2bejpXcRkXKAI+ypSKWHD\n2eeTyeplnFenQfRZ5eZmDsEC/I7X8m2xlr6iZXYU8VKpu/xGwGhdEI5oeKDhzJds\nQA65mMyR739JZcK+oKEULs6sT4tGlt7ZBTbJutzV9QG+QVxUITEo+fvNm5cCAwEA\nAQKCAgAmCIfNc89gpG8Ux6eUC+zrWxh7F7CWX97fSZdH0XuMSbplqyvDgHtrCOM6\n1BlSCS6e13skCVOU1tUjECoJjOoza7vvyCxL4XblEMRcFeI8DFi2tYST0qNCJzAt\nypaCFFeRv6fBUkpGM6GnT9Czfad8drkiRy1tSj6J7sC0JlxYcZ+JFUgWvtksesHW\n6UzfSXqj1n32reoOdeOBueRDWIcqxgNyj3w/GR9o4S1BunrZzpT+/Nd8c2g+qAh0\nrz7ROEUq3iucseNQN6XZWZWvqPScGE+EYhni9wUqNMqfjvNSlzi7+K1yoQtyMm/Z\nNgSq3JNcdsAZQbiCRd1ko2BQsGm3ZBnbsAJ1Dxcn+i9nF5DT/ddWjUWin6LYWuUM\n/0Bqfv3etlrFuP6yxc8bPEMX0ucJg4yVxdkDrm1tYlJ+ANEQoOlZqhngvjz0f8uO\nOtEcDLmiG5VG6Yl72UtWIw+ALnKc5U7ib43Qve0bDAKR5zlHODcRetN9BCMvpekY\nOA4hohkllTP25xmMzLokBqY9n38zEt74kJOp67VKMvhoF7QkrLOfKWCRJjFL7/9I\nHDa6jb31INA9Wu+p/2LIa6I1SUYnMvCUqISgF2hBG9Q9S9TZvKnYUvfurhFS9jZv\n18sxW7IFYWmQyioo+gsAmfKLolJtLl9hCmTfYi7oqCh/EtZdIQKCAQEA0Umkp0Uu\nimVilLjgYGTWLcg8T3NWaELQzb2HYRXSzEq/M8GOtEr7TR7noJBm8fcgl55HEnPl\ni4cEJrr+VprzGbdMtXjHbCD+I945GA6vv3khg7mbqS9a1Uw6gjrQEZgZQU+/IVCu\n9Pbvx8Af32xaBWuN2cFzC7Z6iB815LPc2O5qyZ3+3nEUPah+Z+a9WEeTR6M0hy5c\nkkaRqhehugHDgqMRWGt8GfsFOmaR13kvfFfKadPRPkaGkftCSKBMWjrU4uX7aulm\nD7k4VDbnXIBMhI039+0znSkhZdcV1zk6qwBYn9TtZ11PTlspFPjtPxqS5M6IGflw\nsXkZGv4rZ5CkiQKCAQEAzLVdw2qw/8rWGsCV39EKp7hXLvp7+FuodPvX1L55lWB0\nvmSOldGcNvb2ZsK3RNvgteb8VfKRgaY6waeN5Qm1UXazsOX4F+GThPGHstdNuzkt\nJofRQQHQVR3npZbCngSkSZdahQ9SjiLIDKn8baPN8I8HfpJ4oHLUvkayavbch1kJ\nYWUfGtVKxHGX5m/nnxLdgbJEx9Q+3Qa7DDHuxTqsEqhkk0R0Ganred34HjpDNMs6\nV95HFNolW3yKfuHETKA1bLhej+XdMa11Ts5hBVGCMnnT07WcGhxtyK2dSa656SyT\ngT9+Hd1VWZ/KPpAkQmH9boOr2ihE+oAXiZ4D1t53HwKCAQAD0cA7fTu4Mtl1tVoC\n6FQwSbMwD/7HsFB3MLpDv041hDexDhs4lxW29pVrjLcUO1pQ6gaKA6twvGoK+uah\nVfqRwZKYzTd2dbOtm+SW183FRMSjzsNUdxTFR7rZnZEmgQwU8Quf5AUNW2RM1Oi/\n/w41gxz3mFwtHotl6IvnPJEPNGqme0enb5Da/zQvWTqjXcsGR6gxv1rZIIiP/hZp\nepbCz48FehCtuLMDudN3hzKipkd/Xuo2pLrX9ynigWpjSyePbHsGHHRMXSj2AHqA\naab71EftMlr6x0FgxmgToWu8qyjy4cPjWwSTfX5mb5SEzktX+ZzqPG8eDgOzRmgs\nX6thAoIBADL3kQG/hZQaL1Z3zpjsFggOKH7E1KrQP0/pCCKqzeC4JDjnFm0MxCUX\nNd/96N1XFUqU2QyZGUs7VPO0QOrekOtYb4LCrxNbEXyPGicX3f2YTbqDJEFYL0OR\n74PV1ly7cR/1dA8e8oH6/O3SQMwXdYXIRqhn1Wq1TGyXc4KYNe3o6CH8qFLo+fWR\nBq3T/MopS0coWGGcYY5sR5PQts8aPY9jp67W40UkfkFYV5dHEEaLttn7uJzjd1ug\n1Waj1VjypnqMKNcQ9xKQSl21mohVc+IXXPsgA16o51iIiVm4DAeXFp6ebUsIOWDY\nHOWYw75XYV7rn5TwY8Qusi2MTw5nUycCggEAB/45U0LW7ZGpks/aF/BeGaSWiLIG\nodBWUjRQ4w+Le/pTC8Ci9fiidxuCDH6TQbsUTGKOk7GsfncWHTQJogaMyO26IJ1N\nmYGgK2JJvs7PKyIkocPDVD/Yh0gIzQIE92ZdyXUT21pIYKDUB9e3p0fy/+E0pyeI\nsmsV8oaLr4tZRY1cMogI+pvtUUferbLQmZHhFd9X3m3RslR43Dl1qpYQyzE3x/a3\nWA2NJZbJhh+LiAKzqk7swXOqrTrmXuzLcjMG+T/3lizrbLLuKjQrf+eehlpw0db0\nHVVvkMLOP5ZH/ImkmvOZJY7xxup89VV7LD7TfMKwXafOrjMDdvTAYPtgxw==\n-----END RSA PRIVATE KEY-----\n"
}
```
[supermq]: https://github.com/absmach/supermq
[bootstrap]: https://github.com/absmach/supermq/tree/main/bootstrap
[export]: https://github.com/absmach/export
[agent]: https://github.com/absmach/agent
[mgxui]: https://github.com/absmach/supermq/ui
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
-54
View File
@@ -1,54 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/provision"
"github.com/go-kit/kit/endpoint"
)
func doProvision(svc provision.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(provisionReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
res, err := svc.Provision(req.domainID, req.token, req.Name, req.ExternalID, req.ExternalKey)
if err != nil {
return nil, err
}
provisionResponse := provisionRes{
Clients: res.Clients,
Channels: res.Channels,
ClientCert: res.ClientCert,
ClientKey: res.ClientKey,
CACert: res.CACert,
Whitelisted: res.Whitelisted,
}
return provisionResponse, nil
}
}
func getMapping(svc provision.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(mappingReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
res, err := svc.Mapping(req.token)
if err != nil {
return nil, err
}
return mappingRes{Data: res}, nil
}
}
-223
View File
@@ -1,223 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api_test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/internal/testsutil"
smqlog "github.com/absmach/supermq/logger"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/provision"
"github.com/absmach/supermq/provision/api"
"github.com/absmach/supermq/provision/mocks"
"github.com/stretchr/testify/assert"
)
var (
validToken = "valid"
validContenType = "application/json"
validID = testsutil.GenerateUUID(&testing.T{})
)
type testRequest struct {
client *http.Client
method string
url string
token string
contentType string
body io.Reader
}
func (tr testRequest) make() (*http.Response, error) {
req, err := http.NewRequest(tr.method, tr.url, tr.body)
if err != nil {
return nil, err
}
if tr.token != "" {
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
}
if tr.contentType != "" {
req.Header.Set("Content-Type", tr.contentType)
}
return tr.client.Do(req)
}
func newProvisionServer() (*httptest.Server, *mocks.Service) {
svc := new(mocks.Service)
logger := smqlog.NewMock()
mux := api.MakeHandler(svc, logger, "test")
return httptest.NewServer(mux), svc
}
func TestProvision(t *testing.T) {
is, svc := newProvisionServer()
cases := []struct {
desc string
token string
domainID string
data string
contentType string
status int
svcErr error
}{
{
desc: "valid request",
token: validToken,
domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusCreated,
contentType: validContenType,
svcErr: nil,
},
{
desc: "request with empty external id",
token: validToken,
domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_key": "%s"}`, validID),
status: http.StatusBadRequest,
contentType: validContenType,
svcErr: nil,
},
{
desc: "request with empty external key",
token: validToken,
domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_id": "%s"}`, validID),
status: http.StatusBadRequest,
contentType: validContenType,
svcErr: nil,
},
{
desc: "empty token",
token: "",
domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusCreated,
contentType: validContenType,
svcErr: nil,
},
{
desc: "invalid content type",
token: validToken,
domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusUnsupportedMediaType,
contentType: "text/plain",
svcErr: nil,
},
{
desc: "invalid request",
token: validToken,
domainID: validID,
data: `data`,
status: http.StatusBadRequest,
contentType: validContenType,
svcErr: nil,
},
{
desc: "service error",
token: validToken,
domainID: validID,
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
status: http.StatusForbidden,
contentType: validContenType,
svcErr: svcerr.ErrAuthorization,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repocall := svc.On("Provision", validID, tc.token, "test", validID, validID).Return(provision.Result{}, tc.svcErr)
req := testRequest{
client: is.Client(),
method: http.MethodPost,
url: is.URL + fmt.Sprintf("/%s/mapping", 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)
repocall.Unset()
})
}
}
func TestMapping(t *testing.T) {
is, svc := newProvisionServer()
cases := []struct {
desc string
token string
domainID string
contentType string
status int
svcErr error
}{
{
desc: "valid request",
token: validToken,
domainID: validID,
status: http.StatusOK,
contentType: validContenType,
svcErr: nil,
},
{
desc: "empty token",
token: "",
domainID: validID,
status: http.StatusUnauthorized,
contentType: validContenType,
svcErr: nil,
},
{
desc: "invalid content type",
token: validToken,
domainID: validID,
status: http.StatusUnsupportedMediaType,
contentType: "text/plain",
svcErr: nil,
},
{
desc: "service error",
token: validToken,
domainID: validID,
status: http.StatusForbidden,
contentType: validContenType,
svcErr: svcerr.ErrAuthorization,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repocall := svc.On("Mapping", tc.token).Return(map[string]interface{}{}, tc.svcErr)
req := testRequest{
client: is.Client(),
method: http.MethodGet,
url: is.URL + fmt.Sprintf("/%s/mapping", tc.domainID),
token: tc.token,
contentType: tc.contentType,
}
resp, err := req.make()
assert.Nil(t, err, tc.desc)
assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
repocall.Unset()
})
}
}
-77
View File
@@ -1,77 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
//go:build !test
package api
import (
"log/slog"
"time"
"github.com/absmach/supermq/provision"
)
var _ provision.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger *slog.Logger
svc provision.Service
}
// NewLoggingMiddleware adds logging facilities to the core service.
func NewLoggingMiddleware(svc provision.Service, logger *slog.Logger) provision.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Provision(domainID, token, name, externalID, externalKey string) (res provision.Result, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("name", name),
slog.String("external_id", externalID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Provision failed", args...)
return
}
lm.logger.Info("Provision completed successfully", args...)
}(time.Now())
return lm.svc.Provision(domainID, token, name, externalID, externalKey)
}
func (lm *loggingMiddleware) Cert(domainID, token, clientID, duration string) (cert, key string, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", clientID),
slog.String("ttl", duration),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Client certificate failed to create successfully", args...)
return
}
lm.logger.Info("Client certificate created successfully", args...)
}(time.Now())
return lm.svc.Cert(domainID, token, clientID, duration)
}
func (lm *loggingMiddleware) Mapping(token string) (res map[string]interface{}, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Mapping failed", args...)
return
}
lm.logger.Info("Mapping completed successfully", args...)
}(time.Now())
return lm.svc.Mapping(token)
}
-48
View File
@@ -1,48 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import apiutil "github.com/absmach/supermq/api/http/util"
type provisionReq struct {
token string
domainID string
Name string `json:"name"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
}
func (req provisionReq) validate() error {
if req.ExternalID == "" {
return apiutil.ErrMissingID
}
if req.domainID == "" {
return apiutil.ErrMissingDomainID
}
if req.ExternalKey == "" {
return apiutil.ErrBearerKey
}
if req.Name == "" {
return apiutil.ErrMissingName
}
return nil
}
type mappingReq struct {
token string
domainID string
}
func (req mappingReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.domainID == "" {
return apiutil.ErrMissingDomainID
}
return nil
}
-110
View File
@@ -1,110 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"fmt"
"testing"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestProvisioReq(t *testing.T) {
cases := []struct {
desc string
req provisionReq
err error
}{
{
desc: "valid request",
req: provisionReq{
token: "token",
domainID: testsutil.GenerateUUID(t),
Name: "name",
ExternalID: testsutil.GenerateUUID(t),
ExternalKey: testsutil.GenerateUUID(t),
},
err: nil,
},
{
desc: "empty external id",
req: provisionReq{
token: "token",
domainID: testsutil.GenerateUUID(t),
Name: "name",
ExternalID: "",
ExternalKey: testsutil.GenerateUUID(t),
},
err: apiutil.ErrMissingID,
},
{
desc: "empty domain id",
req: provisionReq{
token: "token",
domainID: "",
Name: "name",
ExternalID: testsutil.GenerateUUID(t),
ExternalKey: testsutil.GenerateUUID(t),
},
err: apiutil.ErrMissingDomainID,
},
{
desc: "empty external key",
req: provisionReq{
token: "token",
domainID: testsutil.GenerateUUID(t),
Name: "name",
ExternalID: testsutil.GenerateUUID(t),
ExternalKey: "",
},
err: apiutil.ErrBearerKey,
},
}
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))
}
}
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))
}
}
-55
View File
@@ -1,55 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"encoding/json"
"net/http"
"github.com/absmach/supermq"
sdk "github.com/absmach/supermq/pkg/sdk"
)
var _ supermq.Response = (*provisionRes)(nil)
type provisionRes struct {
Clients []sdk.Client `json:"clients"`
Channels []sdk.Channel `json:"channels"`
ClientCert map[string]string `json:"client_cert,omitempty"`
ClientKey map[string]string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
Whitelisted map[string]bool `json:"whitelisted,omitempty"`
}
func (res provisionRes) Code() int {
return http.StatusCreated
}
func (res provisionRes) Headers() map[string]string {
return map[string]string{}
}
func (res provisionRes) Empty() bool {
return false
}
type mappingRes struct {
Data interface{}
}
func (res mappingRes) Code() int {
return http.StatusOK
}
func (res mappingRes) Headers() map[string]string {
return map[string]string{}
}
func (res mappingRes) Empty() bool {
return false
}
func (res mappingRes) MarshalJSON() ([]byte, error) {
return json.Marshal(res.Data)
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"github.com/absmach/supermq"
api "github.com/absmach/supermq/api/http"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/provision"
"github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
contentType = "application/json"
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
}
r := chi.NewRouter()
r.Route("/{domainID}", func(r chi.Router) {
r.Route("/mapping", func(r chi.Router) {
r.Post("/", kithttp.NewServer(
doProvision(svc),
decodeProvisionRequest,
api.EncodeResponse,
opts...,
).ServeHTTP)
r.Get("/", kithttp.NewServer(
getMapping(svc),
decodeMappingRequest,
api.EncodeResponse,
opts...,
).ServeHTTP)
})
})
r.Handle("/metrics", promhttp.Handler())
r.Get("/health", supermq.Health("provision", instanceID))
return r
}
func decodeProvisionRequest(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := provisionReq{
token: apiutil.ExtractBearerToken(r),
domainID: chi.URLParam(r, "domainID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
}
return req, nil
}
func decodeMappingRequest(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := mappingReq{
token: apiutil.ExtractBearerToken(r),
domainID: chi.URLParam(r, "domainID"),
}
return req, nil
}
-104
View File
@@ -1,104 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package provision
import (
"fmt"
"os"
"github.com/absmach/supermq/channels"
"github.com/absmach/supermq/clients"
"github.com/absmach/supermq/pkg/errors"
"github.com/pelletier/go-toml"
)
var errFailedToReadConfig = errors.New("failed to read config file")
// ServiceConf represents service config.
type ServiceConf struct {
Port string `toml:"port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"`
LogLevel string `toml:"log_level" env:"SMQ_PROVISION_LOG_LEVEL" envDefault:"info"`
TLS bool `toml:"tls" env:"SMQ_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"`
ServerCert string `toml:"server_cert" env:"SMQ_PROVISION_SERVER_CERT" envDefault:""`
ServerKey string `toml:"server_key" env:"SMQ_PROVISION_SERVER_KEY" envDefault:""`
ClientsURL string `toml:"clients_url" env:"SMQ_PROVISION_CLIENTS_LOCATION" envDefault:"http://localhost"`
UsersURL string `toml:"users_url" env:"SMQ_PROVISION_USERS_LOCATION" envDefault:"http://localhost"`
HTTPPort string `toml:"http_port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"`
MgEmail string `toml:"smq_email" env:"SMQ_PROVISION_EMAIL" envDefault:"test@example.com"`
MgUsername string `toml:"smq_username" env:"SMQ_PROVISION_USERNAME" envDefault:"user"`
MgPass string `toml:"smq_pass" env:"SMQ_PROVISION_PASS" envDefault:"test"`
MgDomainID string `toml:"smq_domain_id" env:"SMQ_PROVISION_DOMAIN_ID" envDefault:""`
MgAPIKey string `toml:"smq_api_key" env:"SMQ_PROVISION_API_KEY" envDefault:""`
MgBSURL string `toml:"smq_bs_url" env:"SMQ_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"`
MgCertsURL string `toml:"smq_certs_url" env:"SMQ_PROVISION_CERTS_SVC_URL" envDefault:"http://localhost:9019"`
}
// Bootstrap represetns the Bootstrap config.
type Bootstrap struct {
X509Provision bool `toml:"x509_provision" env:"SMQ_PROVISION_X509_PROVISIONING" envDefault:"false"`
Provision bool `toml:"provision" env:"SMQ_PROVISION_BS_CONFIG_PROVISIONING" envDefault:"true"`
AutoWhiteList bool `toml:"autowhite_list" env:"SMQ_PROVISION_BS_AUTO_WHITELIST" envDefault:"true"`
Content map[string]interface{} `toml:"content"`
}
// Gateway represetns the Gateway config.
type Gateway struct {
Type string `toml:"type" json:"type"`
ExternalID string `toml:"external_id" json:"external_id"`
ExternalKey string `toml:"external_key" json:"external_key"`
CtrlChannelID string `toml:"ctrl_channel_id" json:"ctrl_channel_id"`
DataChannelID string `toml:"data_channel_id" json:"data_channel_id"`
ExportChannelID string `toml:"export_channel_id" json:"export_channel_id"`
CfgID string `toml:"cfg_id" json:"cfg_id"`
}
// Cert represetns the certificate config.
type Cert struct {
TTL string `json:"ttl" toml:"ttl" env:"SMQ_PROVISION_CERTS_HOURS_VALID" envDefault:"2400h"`
}
// Config struct of Provision.
type Config struct {
File string `toml:"file" env:"SMQ_PROVISION_CONFIG_FILE" envDefault:"config.toml"`
Server ServiceConf `toml:"server" mapstructure:"server"`
Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"`
Clients []clients.Client `toml:"clients" mapstructure:"clients"`
Channels []channels.Channel `toml:"channels" mapstructure:"channels"`
Cert Cert `toml:"cert" mapstructure:"cert"`
BSContent string `env:"SMQ_PROVISION_BS_CONTENT" envDefault:""`
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
InstanceID string `env:"SMQ_MQTT_ADAPTER_INSTANCE_ID" envDefault:""`
}
// Save - store config in a file.
func Save(c Config, file string) error {
if file == "" {
return errors.ErrEmptyPath
}
b, err := toml.Marshal(c)
if err != nil {
return errors.Wrap(errFailedToReadConfig, err)
}
if err := os.WriteFile(file, b, 0o644); err != nil {
return fmt.Errorf("Error writing toml: %w", err)
}
return nil
}
// Read - retrieve config from a file.
func Read(file string) (Config, error) {
data, err := os.ReadFile(file)
if err != nil {
return Config{}, errors.Wrap(errFailedToReadConfig, err)
}
var c Config
if err := toml.Unmarshal(data, &c); err != nil {
return Config{}, fmt.Errorf("Error unmarshaling toml: %w", err)
}
return c, nil
}
-223
View File
@@ -1,223 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package provision_test
import (
"fmt"
"os"
"testing"
"github.com/absmach/supermq/channels"
"github.com/absmach/supermq/clients"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/provision"
"github.com/pelletier/go-toml"
"github.com/stretchr/testify/assert"
)
var (
validConfig = provision.Config{
Server: provision.ServiceConf{
Port: "9016",
LogLevel: "info",
TLS: false,
},
Bootstrap: provision.Bootstrap{
X509Provision: true,
Provision: true,
AutoWhiteList: true,
Content: map[string]interface{}{
"test": "test",
},
},
Clients: []clients.Client{
{
ID: "1234567890",
Name: "test",
Tags: []string{"test"},
Metadata: map[string]interface{}{
"test": "test",
},
Permissions: []string{"test"},
},
},
Channels: []channels.Channel{
{
ID: "1234567890",
Name: "test",
Tags: []string{"test"},
Metadata: map[string]interface{}{
"test": "test",
},
Permissions: []string{"test"},
},
},
Cert: provision.Cert{},
SendTelemetry: true,
InstanceID: "1234567890",
}
validConfigFile = "./config.toml"
invalidConfig = provision.Config{
Bootstrap: provision.Bootstrap{
Content: map[string]interface{}{
"invalid": make(chan int),
},
},
}
invalidConfigFile = "./invalid.toml"
)
func createInvalidConfigFile() error {
config := map[string]interface{}{
"invalid": "invalid",
}
b, err := toml.Marshal(config)
if err != nil {
return err
}
f, err := os.Create(invalidConfigFile)
if err != nil {
return err
}
if _, err = f.Write(b); err != nil {
return err
}
return nil
}
func createValidConfigFile() error {
b, err := toml.Marshal(validConfig)
if err != nil {
return err
}
f, err := os.Create(validConfigFile)
if err != nil {
return err
}
if _, err = f.Write(b); err != nil {
return err
}
return nil
}
func TestSave(t *testing.T) {
cases := []struct {
desc string
cfg provision.Config
file string
err error
}{
{
desc: "save valid config",
cfg: validConfig,
file: validConfigFile,
err: nil,
},
{
desc: "save valid config with empty file name",
cfg: validConfig,
file: "",
err: errors.ErrEmptyPath,
},
{
desc: "save empty config with valid config file",
cfg: provision.Config{},
file: validConfigFile,
err: nil,
},
{
desc: "save empty config with empty file name",
cfg: provision.Config{},
file: "",
err: errors.ErrEmptyPath,
},
{
desc: "save invalid config",
cfg: invalidConfig,
file: invalidConfigFile,
err: errors.New("failed to read config file"),
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
err := provision.Save(c.cfg, c.file)
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected: %v, got: %v", c.err, err))
if err == nil {
defer func() {
if c.file != "" {
err := os.Remove(c.file)
assert.NoError(t, err)
}
}()
cfg, err := provision.Read(c.file)
if c.cfg.Bootstrap.Content == nil {
c.cfg.Bootstrap.Content = map[string]interface{}{}
}
assert.Equal(t, c.err, err)
assert.Equal(t, c.cfg, cfg)
}
})
}
}
func TestRead(t *testing.T) {
err := createInvalidConfigFile()
assert.NoError(t, err)
err = createValidConfigFile()
assert.NoError(t, err)
t.Cleanup(func() {
err := os.Remove(invalidConfigFile)
assert.NoError(t, err)
err = os.Remove(validConfigFile)
assert.NoError(t, err)
})
cases := []struct {
desc string
file string
cfg provision.Config
err error
}{
{
desc: "read valid config",
file: validConfigFile,
cfg: validConfig,
err: nil,
},
{
desc: "read invalid config",
file: invalidConfigFile,
cfg: invalidConfig,
err: nil,
},
{
desc: "read empty config",
file: "",
cfg: provision.Config{},
err: errors.New("failed to read config file"),
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
cfg, err := provision.Read(c.file)
if c.desc == "read invalid config" {
c.cfg.Bootstrap.Content = nil
}
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected: %v, got: %v", c.err, err))
assert.Equal(t, c.cfg, cfg)
})
}
}
-47
View File
@@ -1,47 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
file = "config.toml"
[bootstrap]
autowhite_list = true
content = ""
provision = true
x509_provision = false
[server]
LogLevel = "info"
ca_certs = ""
http_port = "8190"
mg_api_key = ""
mg_bs_url = "http://localhost:9013"
mg_certs_url = "http://localhost:9019"
mg_pass = ""
mg_user = ""
mqtt_url = ""
port = ""
server_cert = ""
server_key = ""
clients_location = "http://localhost:9006"
tls = true
users_location = ""
[[clients]]
name = "client"
[client.metadata]
external_id = "xxxxxx"
[[channels]]
name = "control-channel"
[channels.metadata]
type = "control"
[[channels]]
name = "data-channel"
[channels.metadata]
type = "data"
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package provision contains domain concept definitions needed to support
// Provision service feature, i.e. automate provision process.
package provision
-122
View File
@@ -1,122 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
// Copyright (c) Abstract Machines
package mocks
import (
provision "github.com/absmach/supermq/provision"
mock "github.com/stretchr/testify/mock"
)
// Service is an autogenerated mock type for the Service type
type Service struct {
mock.Mock
}
// Cert provides a mock function with given fields: domainID, token, clientID, duration
func (_m *Service) Cert(domainID string, token string, clientID string, duration string) (string, string, error) {
ret := _m.Called(domainID, token, clientID, duration)
if len(ret) == 0 {
panic("no return value specified for Cert")
}
var r0 string
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(string, string, string, string) (string, string, error)); ok {
return rf(domainID, token, clientID, duration)
}
if rf, ok := ret.Get(0).(func(string, string, string, string) string); ok {
r0 = rf(domainID, token, clientID, duration)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string, string, string, string) string); ok {
r1 = rf(domainID, token, clientID, duration)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func(string, string, string, string) error); ok {
r2 = rf(domainID, token, clientID, duration)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Mapping provides a mock function with given fields: token
func (_m *Service) Mapping(token string) (map[string]interface{}, error) {
ret := _m.Called(token)
if len(ret) == 0 {
panic("no return value specified for Mapping")
}
var r0 map[string]interface{}
var r1 error
if rf, ok := ret.Get(0).(func(string) (map[string]interface{}, error)); ok {
return rf(token)
}
if rf, ok := ret.Get(0).(func(string) map[string]interface{}); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]interface{})
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Provision provides a mock function with given fields: domainID, token, name, externalID, externalKey
func (_m *Service) Provision(domainID string, token string, name string, externalID string, externalKey string) (provision.Result, error) {
ret := _m.Called(domainID, token, name, externalID, externalKey)
if len(ret) == 0 {
panic("no return value specified for Provision")
}
var r0 provision.Result
var r1 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) (provision.Result, error)); ok {
return rf(domainID, token, name, externalID, externalKey)
}
if rf, ok := ret.Get(0).(func(string, string, string, string, string) provision.Result); ok {
r0 = rf(domainID, token, name, externalID, externalKey)
} else {
r0 = ret.Get(0).(provision.Result)
}
if rf, ok := ret.Get(1).(func(string, string, string, string, string) error); ok {
r1 = rf(domainID, token, name, externalID, externalKey)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewService(t interface {
mock.TestingT
Cleanup(func())
}) *Service {
mock := &Service{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
-425
View File
@@ -1,425 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package provision
import (
"encoding/json"
"fmt"
"log/slog"
"github.com/absmach/supermq/pkg/errors"
sdk "github.com/absmach/supermq/pkg/sdk"
)
const (
externalIDKey = "external_id"
gateway = "gateway"
Active = 1
control = "control"
data = "data"
export = "export"
)
var (
ErrUnauthorized = errors.New("unauthorized access")
ErrFailedToCreateToken = errors.New("failed to create access token")
ErrEmptyClientsList = errors.New("clients list in configuration empty")
ErrClientUpdate = errors.New("failed to update client")
ErrEmptyChannelsList = errors.New("channels list in configuration is empty")
ErrFailedChannelCreation = errors.New("failed to create channel")
ErrFailedChannelRetrieval = errors.New("failed to retrieve channel")
ErrFailedClientCreation = errors.New("failed to create client")
ErrFailedClientRetrieval = errors.New("failed to retrieve client")
ErrMissingCredentials = errors.New("missing credentials")
ErrFailedBootstrapRetrieval = errors.New("failed to retrieve bootstrap")
ErrFailedCertCreation = errors.New("failed to create certificates")
ErrFailedCertView = errors.New("failed to view certificate")
ErrFailedBootstrap = errors.New("failed to create bootstrap config")
ErrFailedBootstrapValidate = errors.New("failed to validate bootstrap config creation")
ErrGatewayUpdate = errors.New("failed to updated gateway metadata")
limit uint = 10
offset uint = 0
)
var _ Service = (*provisionService)(nil)
// Service specifies Provision service API.
//
//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines"
type Service interface {
// Provision is the only method this API specifies. Depending on the configuration,
// the following actions will can be executed:
// - create a Client based on external_id (eg. MAC address)
// - create multiple Channels
// - create Bootstrap configuration
// - whitelist Client in Bootstrap configuration == connect Client to Channels
Provision(domainID, token, name, externalID, externalKey string) (Result, error)
// Mapping returns current configuration used for provision
// useful for using in ui to create configuration that matches
// one created with Provision method.
Mapping(token string) (map[string]interface{}, error)
// Certs creates certificate for clients that communicate over mTLS
// A duration string is a possibly signed sequence of decimal numbers,
// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
Cert(domainID, token, clientID, duration string) (string, string, error)
}
type provisionService struct {
logger *slog.Logger
sdk sdk.SDK
conf Config
}
// Result represent what is created with additional info.
type Result struct {
Clients []sdk.Client `json:"clients,omitempty"`
Channels []sdk.Channel `json:"channels,omitempty"`
ClientCert map[string]string `json:"client_cert,omitempty"`
ClientKey map[string]string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
Whitelisted map[string]bool `json:"whitelisted,omitempty"`
Error string `json:"error,omitempty"`
}
// New returns new provision service.
func New(cfg Config, mgsdk sdk.SDK, logger *slog.Logger) Service {
return &provisionService{
logger: logger,
conf: cfg,
sdk: mgsdk,
}
}
// Mapping retrieves current configuration.
func (ps *provisionService) Mapping(token string) (map[string]interface{}, error) {
pm := sdk.PageMetadata{
Offset: uint64(offset),
Limit: uint64(limit),
}
if _, err := ps.sdk.Users(pm, token); err != nil {
return map[string]interface{}{}, errors.Wrap(ErrUnauthorized, err)
}
return ps.conf.Bootstrap.Content, nil
}
// Provision is provision method for creating setup according to
// provision layout specified in config.toml.
func (ps *provisionService) Provision(domainID, token, name, externalID, externalKey string) (res Result, err error) {
var channels []sdk.Channel
var clients []sdk.Client
defer ps.recover(&err, &clients, &channels, &domainID, &token)
token, err = ps.createTokenIfEmpty(token)
if err != nil {
return res, errors.Wrap(ErrFailedToCreateToken, err)
}
if len(ps.conf.Clients) == 0 {
return res, ErrEmptyClientsList
}
if len(ps.conf.Channels) == 0 {
return res, ErrEmptyChannelsList
}
for _, c := range ps.conf.Clients {
// If client in configs contains metadata with external_id
// set value for it from the provision request
if _, ok := c.Metadata[externalIDKey]; ok {
c.Metadata[externalIDKey] = externalID
}
cli := sdk.Client{
Metadata: c.Metadata,
}
if name == "" {
name = c.Name
}
cli.Name = name
cli, err := ps.sdk.CreateClient(cli, domainID, token)
if err != nil {
res.Error = err.Error()
return res, errors.Wrap(ErrFailedClientCreation, err)
}
// Get newly created client (in order to get the key).
cli, err = ps.sdk.Client(cli.ID, domainID, token)
if err != nil {
e := errors.Wrap(err, fmt.Errorf("client id: %s", cli.ID))
return res, errors.Wrap(ErrFailedClientRetrieval, e)
}
clients = append(clients, cli)
}
for _, channel := range ps.conf.Channels {
ch := sdk.Channel{
Name: name + "_" + channel.Name,
Metadata: sdk.Metadata(channel.Metadata),
}
ch, err := ps.sdk.CreateChannel(ch, domainID, token)
if err != nil {
return res, errors.Wrap(ErrFailedChannelCreation, err)
}
ch, err = ps.sdk.Channel(ch.ID, domainID, token)
if err != nil {
e := errors.Wrap(err, fmt.Errorf("channel id: %s", ch.ID))
return res, errors.Wrap(ErrFailedChannelRetrieval, e)
}
channels = append(channels, ch)
}
res = Result{
Clients: clients,
Channels: channels,
Whitelisted: map[string]bool{},
ClientCert: map[string]string{},
ClientKey: map[string]string{},
}
var cert sdk.Cert
var bsConfig sdk.BootstrapConfig
for _, c := range clients {
var chanIDs []string
for _, ch := range channels {
chanIDs = append(chanIDs, ch.ID)
}
content, err := json.Marshal(ps.conf.Bootstrap.Content)
if err != nil {
return Result{}, errors.Wrap(ErrFailedBootstrap, err)
}
if ps.conf.Bootstrap.Provision && needsBootstrap(c) {
bsReq := sdk.BootstrapConfig{
ClientID: c.ID,
ExternalID: externalID,
ExternalKey: externalKey,
Channels: chanIDs,
CACert: res.CACert,
ClientCert: cert.Certificate,
ClientKey: cert.Key,
Content: string(content),
}
bsid, err := ps.sdk.AddBootstrap(bsReq, domainID, token)
if err != nil {
return Result{}, errors.Wrap(ErrFailedBootstrap, err)
}
bsConfig, err = ps.sdk.ViewBootstrap(bsid, domainID, token)
if err != nil {
return Result{}, errors.Wrap(ErrFailedBootstrapValidate, err)
}
}
if ps.conf.Bootstrap.X509Provision {
var cert sdk.Cert
cert, err = ps.sdk.IssueCert(c.ID, ps.conf.Cert.TTL, domainID, token)
if err != nil {
e := errors.Wrap(err, fmt.Errorf("client id: %s", c.ID))
return res, errors.Wrap(ErrFailedCertCreation, e)
}
cert, err := ps.sdk.ViewCert(cert.SerialNumber, domainID, token)
if err != nil {
return res, errors.Wrap(ErrFailedCertView, err)
}
res.ClientCert[c.ID] = cert.Certificate
res.ClientKey[c.ID] = cert.Key
res.CACert = ""
if needsBootstrap(c) {
if _, err = ps.sdk.UpdateBootstrapCerts(bsConfig.ClientID, cert.Certificate, cert.Key, "", domainID, token); err != nil {
return Result{}, errors.Wrap(ErrFailedCertCreation, err)
}
}
}
if ps.conf.Bootstrap.AutoWhiteList {
if err := ps.sdk.Whitelist(c.ID, Active, domainID, token); err != nil {
res.Error = err.Error()
return res, ErrClientUpdate
}
res.Whitelisted[c.ID] = true
}
}
if err = ps.updateGateway(domainID, token, bsConfig, channels); err != nil {
return res, err
}
return res, nil
}
func (ps *provisionService) Cert(domainID, token, clientID, ttl string) (string, string, error) {
token, err := ps.createTokenIfEmpty(token)
if err != nil {
return "", "", errors.Wrap(ErrFailedToCreateToken, err)
}
th, err := ps.sdk.Client(clientID, domainID, token)
if err != nil {
return "", "", errors.Wrap(ErrUnauthorized, err)
}
cert, err := ps.sdk.IssueCert(th.ID, ps.conf.Cert.TTL, domainID, token)
if err != nil {
return "", "", errors.Wrap(ErrFailedCertCreation, err)
}
cert, err = ps.sdk.ViewCert(cert.SerialNumber, domainID, token)
if err != nil {
return "", "", errors.Wrap(ErrFailedCertView, err)
}
return cert.Certificate, cert.Key, err
}
func (ps *provisionService) createTokenIfEmpty(token string) (string, error) {
if token != "" {
return token, nil
}
// If no token in request is provided
// use API key provided in config file or env
if ps.conf.Server.MgAPIKey != "" {
return ps.conf.Server.MgAPIKey, nil
}
// If no API key use username and password provided to create access token.
if ps.conf.Server.MgUsername == "" || ps.conf.Server.MgPass == "" {
return token, ErrMissingCredentials
}
u := sdk.Login{
Username: ps.conf.Server.MgUsername,
Password: ps.conf.Server.MgPass,
}
tkn, err := ps.sdk.CreateToken(u)
if err != nil {
return token, errors.Wrap(ErrFailedToCreateToken, err)
}
return tkn.AccessToken, nil
}
func (ps *provisionService) updateGateway(domainID, token string, bs sdk.BootstrapConfig, channels []sdk.Channel) error {
var gw Gateway
for _, ch := range channels {
switch ch.Metadata["type"] {
case control:
gw.CtrlChannelID = ch.ID
case data:
gw.DataChannelID = ch.ID
case export:
gw.ExportChannelID = ch.ID
}
}
gw.ExternalID = bs.ExternalID
gw.ExternalKey = bs.ExternalKey
gw.CfgID = bs.ClientID
gw.Type = gateway
c, sdkerr := ps.sdk.Client(bs.ClientID, domainID, token)
if sdkerr != nil {
return errors.Wrap(ErrGatewayUpdate, sdkerr)
}
b, err := json.Marshal(gw)
if err != nil {
return errors.Wrap(ErrGatewayUpdate, err)
}
if err := json.Unmarshal(b, &c.Metadata); err != nil {
return errors.Wrap(ErrGatewayUpdate, err)
}
if _, err := ps.sdk.UpdateClient(c, domainID, token); err != nil {
return errors.Wrap(ErrGatewayUpdate, err)
}
return nil
}
func (ps *provisionService) errLog(err error) {
if err != nil {
ps.logger.Error(fmt.Sprintf("Error recovering: %s", err))
}
}
func clean(ps *provisionService, clients []sdk.Client, channels []sdk.Channel, domainID, token string) {
for _, t := range clients {
err := ps.sdk.DeleteClient(t.ID, domainID, token)
ps.errLog(err)
}
for _, c := range channels {
err := ps.sdk.DeleteChannel(c.ID, domainID, token)
ps.errLog(err)
}
}
func (ps *provisionService) recover(e *error, ths *[]sdk.Client, chs *[]sdk.Channel, dm, tkn *string) {
if e == nil {
return
}
clients, channels, domainID, token, err := *ths, *chs, *dm, *tkn, *e
if errors.Contains(err, ErrFailedClientRetrieval) || errors.Contains(err, ErrFailedChannelCreation) {
for _, c := range clients {
err := ps.sdk.DeleteClient(c.ID, domainID, token)
ps.errLog(err)
}
return
}
if errors.Contains(err, ErrFailedBootstrap) || errors.Contains(err, ErrFailedChannelRetrieval) {
clean(ps, clients, channels, domainID, token)
return
}
if errors.Contains(err, ErrFailedBootstrapValidate) || errors.Contains(err, ErrFailedCertCreation) {
clean(ps, clients, channels, domainID, token)
for _, th := range clients {
if needsBootstrap(th) {
ps.errLog(ps.sdk.RemoveBootstrap(th.ID, domainID, token))
}
}
return
}
if errors.Contains(err, ErrFailedBootstrapValidate) || errors.Contains(err, ErrFailedCertCreation) {
clean(ps, clients, channels, domainID, token)
for _, th := range clients {
if needsBootstrap(th) {
bs, err := ps.sdk.ViewBootstrap(th.ID, domainID, token)
ps.errLog(errors.Wrap(ErrFailedBootstrapRetrieval, err))
ps.errLog(ps.sdk.RemoveBootstrap(bs.ClientID, domainID, token))
}
}
}
if errors.Contains(err, ErrClientUpdate) || errors.Contains(err, ErrGatewayUpdate) {
clean(ps, clients, channels, domainID, token)
for _, th := range clients {
if ps.conf.Bootstrap.X509Provision && needsBootstrap(th) {
_, err := ps.sdk.RevokeCert(th.ID, domainID, token)
ps.errLog(err)
}
if needsBootstrap(th) {
bs, err := ps.sdk.ViewBootstrap(th.ID, domainID, token)
ps.errLog(errors.Wrap(ErrFailedBootstrapRetrieval, err))
ps.errLog(ps.sdk.RemoveBootstrap(bs.ClientID, domainID, token))
}
}
return
}
}
func needsBootstrap(th sdk.Client) bool {
if th.Metadata == nil {
return false
}
if _, ok := th.Metadata[externalIDKey]; ok {
return true
}
return false
}
-232
View File
@@ -1,232 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package provision_test
import (
"fmt"
"testing"
"github.com/absmach/supermq/internal/testsutil"
smqlog "github.com/absmach/supermq/logger"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
svcerr "github.com/absmach/supermq/pkg/errors/service"
sdk "github.com/absmach/supermq/pkg/sdk"
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
"github.com/absmach/supermq/provision"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var validToken = "valid"
func TestMapping(t *testing.T) {
mgsdk := new(sdkmocks.SDK)
svc := provision.New(validConfig, mgsdk, smqlog.NewMock())
cases := []struct {
desc string
token string
content map[string]interface{}
sdkerr error
err error
}{
{
desc: "valid token",
token: validToken,
content: validConfig.Bootstrap.Content,
sdkerr: nil,
err: nil,
},
{
desc: "invalid token",
token: "invalid",
content: map[string]interface{}{},
sdkerr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
err: provision.ErrUnauthorized,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
pm := sdk.PageMetadata{Offset: uint64(0), Limit: uint64(10)}
repocall := mgsdk.On("Users", pm, c.token).Return(sdk.UsersPage{}, c.sdkerr)
content, err := svc.Mapping(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)
repocall.Unset()
})
}
}
func TestCert(t *testing.T) {
cases := []struct {
desc string
config provision.Config
domainID string
token string
clientID string
ttl string
serial string
cert string
key string
sdkClientErr error
sdkCertErr error
sdkTokenErr error
err error
}{
{
desc: "valid",
config: validConfig,
domainID: testsutil.GenerateUUID(t),
token: validToken,
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "cert",
key: "key",
sdkClientErr: nil,
sdkCertErr: nil,
sdkTokenErr: nil,
err: nil,
},
{
desc: "empty token with config API key",
config: provision.Config{
Server: provision.ServiceConf{MgAPIKey: "key"},
Cert: provision.Cert{TTL: "1h"},
},
domainID: testsutil.GenerateUUID(t),
token: "",
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "cert",
key: "key",
sdkClientErr: nil,
sdkCertErr: nil,
sdkTokenErr: nil,
err: nil,
},
{
desc: "empty token with username and password",
config: provision.Config{
Server: provision.ServiceConf{
MgUsername: "testUsername",
MgPass: "12345678",
MgDomainID: testsutil.GenerateUUID(t),
},
Cert: provision.Cert{TTL: "1h"},
},
domainID: testsutil.GenerateUUID(t),
token: "",
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "cert",
key: "key",
sdkClientErr: nil,
sdkCertErr: nil,
sdkTokenErr: nil,
err: nil,
},
{
desc: "empty token with username and invalid password",
config: provision.Config{
Server: provision.ServiceConf{
MgUsername: "testUsername",
MgPass: "12345678",
MgDomainID: testsutil.GenerateUUID(t),
},
Cert: provision.Cert{TTL: "1h"},
},
domainID: testsutil.GenerateUUID(t),
token: "",
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "",
key: "",
sdkClientErr: nil,
sdkCertErr: nil,
sdkTokenErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
err: provision.ErrFailedToCreateToken,
},
{
desc: "empty token with empty username and password",
config: provision.Config{
Server: provision.ServiceConf{},
Cert: provision.Cert{TTL: "1h"},
},
domainID: testsutil.GenerateUUID(t),
token: "",
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "",
key: "",
sdkClientErr: nil,
sdkCertErr: nil,
sdkTokenErr: nil,
err: provision.ErrMissingCredentials,
},
{
desc: "invalid clientID",
config: validConfig,
domainID: testsutil.GenerateUUID(t),
token: "invalid",
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "",
key: "",
sdkClientErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
sdkCertErr: nil,
sdkTokenErr: nil,
err: provision.ErrUnauthorized,
},
{
desc: "invalid clientID",
config: validConfig,
domainID: testsutil.GenerateUUID(t),
token: validToken,
clientID: "invalid",
ttl: "1h",
cert: "",
key: "",
sdkClientErr: errors.NewSDKErrorWithStatus(repoerr.ErrNotFound, 404),
sdkCertErr: nil,
sdkTokenErr: nil,
err: provision.ErrUnauthorized,
},
{
desc: "failed to issue cert",
config: validConfig,
domainID: testsutil.GenerateUUID(t),
token: validToken,
clientID: testsutil.GenerateUUID(t),
ttl: "1h",
cert: "",
key: "",
sdkClientErr: nil,
sdkTokenErr: nil,
sdkCertErr: errors.NewSDKError(repoerr.ErrCreateEntity),
err: repoerr.ErrCreateEntity,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
mgsdk := new(sdkmocks.SDK)
svc := provision.New(c.config, mgsdk, smqlog.NewMock())
mgsdk.On("Client", c.clientID, c.domainID, mock.Anything).Return(sdk.Client{ID: c.clientID}, c.sdkClientErr)
mgsdk.On("IssueCert", c.clientID, c.config.Cert.TTL, c.domainID, mock.Anything).Return(sdk.Cert{SerialNumber: c.serial}, c.sdkCertErr)
mgsdk.On("ViewCert", c.serial, mock.Anything, mock.Anything).Return(sdk.Cert{Certificate: c.cert, Key: c.key}, c.sdkCertErr)
login := sdk.Login{
Username: c.config.Server.MgUsername,
Password: c.config.Server.MgPass,
}
mgsdk.On("CreateToken", login).Return(sdk.Token{AccessToken: validToken}, c.sdkTokenErr)
cert, key, err := svc.Cert(c.domainID, c.token, c.clientID, c.ttl)
assert.Equal(t, c.cert, cert)
assert.Equal(t, c.key, key)
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected error %v, got %v", c.err, err))
})
}
}
-1
View File
@@ -66,7 +66,6 @@ func Provision(conf Config) error {
UsersURL: conf.Host,
ReaderURL: defReaderURL,
HTTPAdapterURL: fmt.Sprintf("%s/http", conf.Host),
BootstrapURL: conf.Host,
CertsURL: conf.Host,
MsgContentType: sdk.ContentType(msgContentType),
TLSVerification: false,