mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
MG-3537 - Migrate Magistrala to ATOM for identity & authorization (#3532)
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: Arvindh <arvindh91@gmail.com> Signed-off-by: dusan <borovcanindusan1@gmail.com> Signed-off-by: Rodney Osodo <socials@rodneyosodo.com> Co-authored-by: Dušan Borovčanin <dusan.borovcanin@absmach.eu> Co-authored-by: Rodney Osodo <socials@rodneyosodo.com> Co-authored-by: dusan <borovcanindusan1@gmail.com>
This commit is contained in:
@@ -10,7 +10,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "api/**"
|
||||
- "auth/api/http/**"
|
||||
- "internal/atom/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
@@ -20,9 +20,9 @@ on:
|
||||
- "bootstrap/api/**"
|
||||
- "certs/api/http/**"
|
||||
- "readers/api/http/**"
|
||||
- "re/api/**"
|
||||
- "alarms/api/**"
|
||||
- "reports/api/**"
|
||||
- "re/**"
|
||||
- "alarms/**"
|
||||
- "reports/**"
|
||||
- "apidocs/openapi/**"
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -30,7 +30,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "api/**"
|
||||
- "auth/api/http/**"
|
||||
- "internal/atom/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
@@ -40,9 +40,9 @@ on:
|
||||
- "bootstrap/api/**"
|
||||
- "certs/api/http/**"
|
||||
- "readers/api/http/**"
|
||||
- "re/api/**"
|
||||
- "alarms/api/**"
|
||||
- "reports/api/**"
|
||||
- "re/**"
|
||||
- "alarms/**"
|
||||
- "reports/**"
|
||||
- "apidocs/openapi/**"
|
||||
|
||||
concurrency:
|
||||
@@ -50,17 +50,15 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TOKENS_URL: http://localhost:9002/users/tokens/issue
|
||||
CREATE_DOMAINS_URL: http://localhost:9003/domains
|
||||
USER_IDENTITY: admin@example.com
|
||||
ATOM_LOGIN_URL: http://localhost/auth/login
|
||||
USER_IDENTITY: admin
|
||||
USER_SECRET: 12345678
|
||||
DOMAIN_NAME: demo-test
|
||||
USERS_URL: http://localhost:9002
|
||||
DOMAIN_URL: http://localhost:9003
|
||||
CLIENTS_URL: http://localhost:9006
|
||||
CHANNELS_URL: http://localhost:9005
|
||||
GROUPS_URL: http://localhost:9004
|
||||
AUTH_URL: http://localhost:9001
|
||||
USERS_URL: http://localhost
|
||||
DOMAIN_URL: http://localhost
|
||||
CLIENTS_URL: http://localhost
|
||||
CHANNELS_URL: http://localhost
|
||||
GROUPS_URL: http://localhost
|
||||
JOURNAL_URL: http://localhost:9021
|
||||
BOOTSTRAP_URL: http://localhost:9013
|
||||
CERTS_URL: http://localhost:9019
|
||||
@@ -96,28 +94,29 @@ jobs:
|
||||
- "apidocs/openapi/journal.yaml"
|
||||
- "journal/api/**"
|
||||
|
||||
auth:
|
||||
- "apidocs/openapi/auth.yaml"
|
||||
- "auth/api/http/**"
|
||||
|
||||
domains:
|
||||
- "apidocs/openapi/domains.yaml"
|
||||
- "internal/atom/**"
|
||||
- "domains/api/http/**"
|
||||
|
||||
clients:
|
||||
- "apidocs/openapi/clients.yaml"
|
||||
- "internal/atom/**"
|
||||
- "clients/api/http/**"
|
||||
|
||||
channels:
|
||||
- "apidocs/openapi/channels.yaml"
|
||||
- "internal/atom/**"
|
||||
- "channels/api/http/**"
|
||||
|
||||
groups:
|
||||
- "apidocs/openapi/groups.yaml"
|
||||
- "internal/atom/**"
|
||||
- "groups/api/http/**"
|
||||
|
||||
users:
|
||||
- "apidocs/openapi/users.yaml"
|
||||
- "internal/atom/**"
|
||||
- "users/api/**"
|
||||
|
||||
bootstrap:
|
||||
@@ -134,21 +133,30 @@ jobs:
|
||||
|
||||
re:
|
||||
- "apidocs/openapi/rules.yaml"
|
||||
- "re/api/**"
|
||||
- "re/**"
|
||||
- "cmd/re/**"
|
||||
- "internal/atom/**"
|
||||
|
||||
alarms:
|
||||
- "apidocs/openapi/alarms.yaml"
|
||||
- "alarms/api/**"
|
||||
- "alarms/**"
|
||||
- "cmd/alarms/**"
|
||||
- "internal/atom/**"
|
||||
|
||||
reports:
|
||||
- "apidocs/openapi/reports.yaml"
|
||||
- "reports/api/**"
|
||||
- "reports/**"
|
||||
- "cmd/reports/**"
|
||||
- "internal/atom/**"
|
||||
|
||||
- name: Build images
|
||||
run: make all -j $(nproc) && make dockers_dev -j $(nproc)
|
||||
|
||||
- name: Provision Atom service tokens
|
||||
run: make provision_atom_tokens
|
||||
|
||||
- name: Start containers
|
||||
run: make run_latest up args="-d" && make run_addons up args="-d"
|
||||
run: make run_latest_ci up args="-d" && make run_addons up args="-d"
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
@@ -157,24 +165,28 @@ jobs:
|
||||
|
||||
# Check if services are responding
|
||||
for i in {1..30}; do
|
||||
if curl -f -s http://localhost:9002/health > /dev/null 2>&1; then
|
||||
if curl -f -s http://localhost/health > /dev/null 2>&1; then
|
||||
echo "Services are ready!"
|
||||
break
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for services... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Services failed to become ready" >&2
|
||||
docker compose -f docker/docker-compose.yaml -f docker/docker-compose-ci.yaml --env-file docker/.env --env-file docker/.env.tokens -p "${USER_REPO:-absmach_magistrala}" ps || true
|
||||
docker logs --tail 100 magistrala-nginx || true
|
||||
docker logs --tail 100 magistrala-atom || true
|
||||
docker logs --tail 100 magistrala-atom-bootstrap || true
|
||||
exit 1
|
||||
|
||||
- name: Set access token
|
||||
run: |
|
||||
export USER_TOKEN=$(curl -sSX POST $TOKENS_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token)
|
||||
export DOMAIN_ID=$(curl -sSX POST $CREATE_DOMAINS_URL -H "Content-Type: application/json" -H "Authorization: Bearer $USER_TOKEN" -d "{\"name\":\"$DOMAIN_NAME\",\"route\":\"$DOMAIN_NAME\"}" | jq -r .id)
|
||||
export USER_TOKEN=$(curl -fsS -X POST "$ATOM_LOGIN_URL" -H "Content-Type: application/json" -d "{\"identifier\":\"$USER_IDENTITY\",\"secret\":\"$USER_SECRET\",\"kind\":\"password\"}" | jq -er .token)
|
||||
echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV
|
||||
export CLIENT_SECRET=$(magistrala-cli provision test | /usr/bin/grep -Eo '"secret": "[^"]+"' | awk 'NR % 2 == 0' | sed 's/"secret": "\(.*\)"/\1/')
|
||||
echo "CLIENT_SECRET=$CLIENT_SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Users API tests
|
||||
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('users/api/**') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/users.yaml
|
||||
@@ -183,7 +195,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples'
|
||||
|
||||
- name: Run Groups API tests
|
||||
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('groups/api/http/**') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/groups.yaml
|
||||
@@ -192,7 +204,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Clients API tests
|
||||
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('clients/api/http/**') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/clients.yaml
|
||||
@@ -201,7 +213,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Channels API tests
|
||||
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('channels/api/http/**') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/channels.yaml
|
||||
@@ -209,17 +221,8 @@ jobs:
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Auth API tests
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/auth.yaml
|
||||
base-url: ${{ env.AUTH_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Domains API tests
|
||||
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('domains/api/http/**') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/domains.yaml
|
||||
@@ -237,7 +240,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Bootstrap API tests
|
||||
if: steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('bootstrap/api/**') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/bootstrap.yaml
|
||||
@@ -246,7 +249,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Certs API tests
|
||||
if: steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
if: (steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('docker/addons/certs/docker-compose.yaml') != ''
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/certs.yaml
|
||||
@@ -292,4 +295,4 @@ jobs:
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: make run_latest down args="-v" && make run_addons down args="-v"
|
||||
run: make run_latest_ci down args="-v" && make run_addons down args="-v"
|
||||
|
||||
@@ -50,6 +50,7 @@ jobs:
|
||||
- "pkg/messaging/*.pb.go"
|
||||
|
||||
mocks:
|
||||
- "tools/config/.mockery.yaml"
|
||||
- ".github/workflows/check-generated-files.yaml"
|
||||
- "pkg/sdk/sdk.go"
|
||||
- "users/postgres/clients.go"
|
||||
|
||||
@@ -45,17 +45,9 @@ jobs:
|
||||
make all -j $(nproc)
|
||||
|
||||
compile-check:
|
||||
name: Compile Check ${{ matrix.variant.name }}
|
||||
name: Compile Check Redis Event Store
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
variant:
|
||||
- name: redis
|
||||
env: MG_ES_TYPE=es_redis
|
||||
target: fluxmq
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
@@ -66,6 +58,6 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Compile check for ${{ matrix.variant.name }}
|
||||
- name: Compile check
|
||||
run: |
|
||||
${{ matrix.variant.env }} make ${{ matrix.variant.target }}
|
||||
MG_ES_TYPE=es_redis make all -j $(nproc)
|
||||
|
||||
@@ -66,28 +66,8 @@ jobs:
|
||||
workflow:
|
||||
- ".github/workflows/tests.yaml"
|
||||
|
||||
auth:
|
||||
- "auth/**"
|
||||
- "cmd/auth/**"
|
||||
- "auth.proto"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
|
||||
bootstrap:
|
||||
- "bootstrap/**"
|
||||
- "cmd/bootstrap/**"
|
||||
- "pkg/bootstrap/**"
|
||||
- "provision/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
channels:
|
||||
- "channels/**"
|
||||
- "cmd/channels/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
- "clients/api/grpc/**"
|
||||
- "groups/api/grpc/**"
|
||||
@@ -101,10 +81,6 @@ jobs:
|
||||
|
||||
clients:
|
||||
- "clients/**"
|
||||
- "cmd/clients/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
@@ -115,18 +91,10 @@ jobs:
|
||||
|
||||
domains:
|
||||
- "domains/**"
|
||||
- "cmd/domains/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "internal/grpc/**"
|
||||
|
||||
groups:
|
||||
- "groups/**"
|
||||
- "cmd/groups/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "clients/api/grpc/**"
|
||||
@@ -140,9 +108,6 @@ jobs:
|
||||
journal:
|
||||
- "journal/**"
|
||||
- "cmd/journal/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
logger:
|
||||
@@ -165,7 +130,6 @@ jobs:
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/errors/**"
|
||||
- "pkg/groups/**"
|
||||
- "auth/**"
|
||||
- "internal/*"
|
||||
- "clients/**"
|
||||
- "users/**"
|
||||
@@ -189,10 +153,6 @@ jobs:
|
||||
|
||||
users:
|
||||
- "users/**"
|
||||
- "cmd/users/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
@@ -200,8 +160,6 @@ jobs:
|
||||
notifications:
|
||||
- "notifications/**"
|
||||
- "cmd/notifications/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "consumers/notifier.go"
|
||||
- "pkg/events/**"
|
||||
|
||||
@@ -240,11 +198,10 @@ jobs:
|
||||
|
||||
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then
|
||||
# If workflow or pkg/errors changed, test everything
|
||||
modules=("auth" "bootstrap" "channels" "cli" "clients" "domains" "groups" "internal" "journal" "logger" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers" "re" "alarms" "reports")
|
||||
modules=("auth" "channels" "cli" "clients" "domains" "groups" "internal" "journal" "logger" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers" "re" "alarms" "reports")
|
||||
else
|
||||
# Add only changed modules
|
||||
[[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth")
|
||||
[[ "${{ steps.changes.outputs.bootstrap }}" == "true" ]] && modules+=("bootstrap")
|
||||
[[ "${{ steps.changes.outputs.channels }}" == "true" ]] && modules+=("channels")
|
||||
[[ "${{ steps.changes.outputs.cli }}" == "true" ]] && modules+=("cli")
|
||||
[[ "${{ steps.changes.outputs.clients }}" == "true" ]] && modules+=("clients")
|
||||
@@ -314,9 +271,19 @@ jobs:
|
||||
pkg-transformers) dir="pkg/transformers" ;;
|
||||
pkg-ulid) dir="pkg/ulid" ;;
|
||||
pkg-uuid) dir="pkg/uuid" ;;
|
||||
channels) dir="pkg/channels" ;;
|
||||
clients) dir="pkg/clients" ;;
|
||||
domains) dir="pkg/domains" ;;
|
||||
groups) dir="pkg/groups" ;;
|
||||
*) dir="${{ matrix.module }}" ;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
echo "Skipping ${{ matrix.module }}; ./$dir is not present in this branch"
|
||||
echo "mode: atomic" > coverage-${{ matrix.module }}.out
|
||||
exit 0
|
||||
fi
|
||||
|
||||
go test -mod=readonly --race -v -count=1 -failfast -coverprofile=coverage-${{ matrix.module }}.out ./$dir/...
|
||||
|
||||
- name: Upload coverage
|
||||
|
||||
@@ -19,5 +19,11 @@ coverage
|
||||
# Ignore Openbao data directory as it contains runtime-generated data
|
||||
docker/addons/certs/openbao/
|
||||
|
||||
# Ignore generated local Atom service tokens.
|
||||
docker/.env.tokens
|
||||
|
||||
# Ignore SeaweedFS data directory as it contains runtime-generated data
|
||||
docker/data/*
|
||||
|
||||
demo-ui
|
||||
node_modules
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
override MG_DOCKER_IMAGE_NAME_PREFIX := ghcr.io/absmach/magistrala
|
||||
MG_DOCKER_VOLUME_NAME_PREFIX ?= magistrala
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = auth users clients groups channels domains notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports bootstrap provision journal fluxmq
|
||||
TEST_API_SERVICES = journal auth certs clients users channels groups domains
|
||||
SERVICES = atom-bootstrap notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader alarms reports journal fluxmq
|
||||
TEST_API_SERVICES = journal certs clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
@@ -23,6 +23,15 @@ space:= $(empty) $(empty)
|
||||
DOCKER_PROJECT ?= $(shell echo $(subst $(space),,$(USER_REPO)) | sed -E 's/[^a-zA-Z0-9]/_/g' | tr '[:upper:]' '[:lower:]')
|
||||
DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart
|
||||
DEFAULT_DOCKER_COMPOSE_COMMAND := up
|
||||
ATOM_TOKENS_ENV ?= docker/.env.tokens
|
||||
REQUIRED_ATOM_TOKEN_ENVS := MG_ATOM_TOKEN_FLUXMQ_AUTH MG_ATOM_TOKEN_FLUXMQ_NODE1 MG_ATOM_TOKEN_FLUXMQ_NODE2 MG_ATOM_TOKEN_FLUXMQ_NODE3 MG_ATOM_TOKEN_JOURNAL MG_ATOM_TOKEN_NOTIFICATIONS MG_ATOM_TOKEN_TIMESCALE_READER MG_ATOM_TOKEN_RE MG_ATOM_TOKEN_ALARMS MG_ATOM_TOKEN_REPORTS MG_ATOM_TOKEN_POSTGRES_READER
|
||||
PROVISION_ATOM_TOKENS ?= false
|
||||
PROVISION_ATOM_TOKEN_GOALS := provision-atom-tokens
|
||||
DOCKER_BASE_ENV_FILES := --env-file docker/.env
|
||||
DOCKER_ENV_FILES = $(if $(filter down,$(DOCKER_COMPOSE_COMMAND)),$(DOCKER_BASE_ENV_FILES),$(DOCKER_BASE_ENV_FILES) --env-file $(ATOM_TOKENS_ENV))
|
||||
DOCKER_PROVISION_ENV_FILES = $(DOCKER_BASE_ENV_FILES) $(if $(wildcard $(ATOM_TOKENS_ENV)),--env-file $(ATOM_TOKENS_ENV))
|
||||
HOST_UID := $(shell id -u)
|
||||
HOST_GID := $(shell id -g)
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
MOCKERY = $(GOBIN)/mockery
|
||||
MOCKERY_VERSION=3.6.4
|
||||
@@ -79,7 +88,48 @@ define make_docker_dev
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
define require_atom_tokens_env
|
||||
@if [ -z "$(filter down,$(DOCKER_COMPOSE_COMMAND))" ]; then \
|
||||
if [ ! -f "$(ATOM_TOKENS_ENV)" ]; then \
|
||||
echo "Missing $(ATOM_TOKENS_ENV). Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
missing=""; \
|
||||
for env_name in $(REQUIRED_ATOM_TOKEN_ENVS); do \
|
||||
if ! grep -q "^$${env_name}=" "$(ATOM_TOKENS_ENV)"; then \
|
||||
missing="$${missing} $${env_name}"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [ -n "$${missing}" ]; then \
|
||||
echo "Missing Atom service token(s) in $(ATOM_TOKENS_ENV):$${missing}. Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define ensure_atom_tokens_env
|
||||
@if [ "$(PROVISION_ATOM_TOKENS)" = "true" ] && [ -z "$(filter down,$(DOCKER_COMPOSE_COMMAND))" ]; then \
|
||||
$(MAKE) provision_atom_tokens; \
|
||||
elif [ -z "$(filter down,$(DOCKER_COMPOSE_COMMAND))" ]; then \
|
||||
if [ ! -f "$(ATOM_TOKENS_ENV)" ]; then \
|
||||
echo "Missing $(ATOM_TOKENS_ENV). Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
missing=""; \
|
||||
for env_name in $(REQUIRED_ATOM_TOKEN_ENVS); do \
|
||||
if ! grep -q "^$${env_name}=" "$(ATOM_TOKENS_ENV)"; then \
|
||||
missing="$${missing} $${env_name}"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [ -n "$${missing}" ]; then \
|
||||
echo "Missing Atom service token(s) in $(ATOM_TOKENS_ENV):$${missing}. Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define run_with_arch_detection
|
||||
$(call require_atom_tokens_env)
|
||||
@echo "Detecting architecture..."
|
||||
@if [ "$(DETECTED_ARCH)" = "arm64" ] || [ "$(DETECTED_ARCH)" = "aarch64" ]; then \
|
||||
echo "ARM64 architecture detected."; \
|
||||
@@ -89,12 +139,12 @@ define run_with_arch_detection
|
||||
docker tag $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest; \
|
||||
done; \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
else \
|
||||
echo "x86_64 architecture detected."; \
|
||||
git checkout $(1); \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
fi
|
||||
endef
|
||||
|
||||
@@ -125,6 +175,9 @@ DOCKER_PLATFORM ?=
|
||||
ifneq ($(filter run%,$(firstword $(MAKECMDGOALS))),)
|
||||
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
DOCKER_COMPOSE_COMMAND := $(if $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
ifneq ($(filter $(PROVISION_ATOM_TOKEN_GOALS),$(temp_args)),)
|
||||
override PROVISION_ATOM_TOKENS := true
|
||||
endif
|
||||
$(eval $(DOCKER_COMPOSE_COMMAND):;@)
|
||||
endif
|
||||
|
||||
@@ -144,7 +197,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release provision_atom_tokens provision-atom-tokens run_latest run_latest_ci run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
@@ -199,12 +252,11 @@ define test_api_service
|
||||
--phases=examples,stateful
|
||||
endef
|
||||
|
||||
test_api_users: TEST_API_URL := http://localhost:9002
|
||||
test_api_clients: TEST_API_URL := http://localhost:9006
|
||||
test_api_domains: TEST_API_URL := http://localhost:9003
|
||||
test_api_channels: TEST_API_URL := http://localhost:9005
|
||||
test_api_groups: TEST_API_URL := http://localhost:9004
|
||||
test_api_auth: TEST_API_URL := http://localhost:9001
|
||||
test_api_users: TEST_API_URL := http://localhost:9000
|
||||
test_api_clients: TEST_API_URL := http://localhost:9000
|
||||
test_api_domains: TEST_API_URL := http://localhost:9000
|
||||
test_api_channels: TEST_API_URL := http://localhost:9000
|
||||
test_api_groups: TEST_API_URL := http://localhost:9000
|
||||
test_api_certs: TEST_API_URL := http://localhost:9019
|
||||
test_api_journal: TEST_API_URL := http://localhost:9021
|
||||
|
||||
@@ -262,7 +314,15 @@ rundev:
|
||||
cd scripts && ./run.sh
|
||||
|
||||
grpc_mtls_certs:
|
||||
$(MAKE) -C docker/ssl auth_grpc_certs clients_grpc_certs
|
||||
$(MAKE) -C docker/ssl clients_grpc_certs
|
||||
|
||||
provision_atom_tokens:
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_PROVISION_ENV_FILES) -p $(DOCKER_PROJECT) up -d --wait --wait-timeout 120 atom
|
||||
$(MAKE) docker_atom-bootstrap
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_PROVISION_ENV_FILES) -p $(DOCKER_PROJECT) run --rm --no-deps --user "$(HOST_UID):$(HOST_GID)" -v "$(PWD)/docker:/host/docker" atom-bootstrap provision-tokens --output /host/docker/.env.tokens
|
||||
|
||||
provision-atom-tokens:
|
||||
@:
|
||||
|
||||
check_tls:
|
||||
ifeq ($(GRPC_TLS),true)
|
||||
@@ -284,14 +344,20 @@ check_certs: check_mtls check_tls
|
||||
ifeq ($(GRPC_MTLS_CERT_FILES_EXISTS),0)
|
||||
ifeq ($(filter true,$(GRPC_MTLS) $(GRPC_TLS)),true)
|
||||
ifeq ($(filter $(DEFAULT_DOCKER_COMPOSE_COMMAND),$(DOCKER_COMPOSE_COMMAND)),$(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
$(MAKE) -C docker/ssl auth_grpc_certs clients_grpc_certs
|
||||
$(MAKE) -C docker/ssl clients_grpc_certs
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
run_latest: check_certs
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
$(call ensure_atom_tokens_env)
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_latest_ci: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-ci.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_tls:
|
||||
@test -n "$(host)" || (echo "Usage: make run_tls host=example.com [email=admin@example.com] [letsencrypt=false] [staging=true] [force=true]" && exit 2)
|
||||
@@ -305,17 +371,20 @@ run_tls:
|
||||
./docker/setup-tls.sh
|
||||
|
||||
run_stable: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(version)/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_addons: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) up -d atom jaeger
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) $(DOCKER_ENV_FILES) $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
done
|
||||
|
||||
run_live: check_certs
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
$(call require_atom_tokens_env)
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
@@ -46,7 +46,7 @@ It is extremely flexible and lets you build systems the way you want — from si
|
||||
|
||||
At the same time, it avoids the typical complexity of many IoT platforms, where you need to learn an entirely new set of concepts before you can even get started.
|
||||
|
||||
Magistrala is built around a small number of core concepts:
|
||||
Magistrala is built around a small number of main concepts:
|
||||
- users
|
||||
- clients (devices)
|
||||
- channels
|
||||
@@ -141,6 +141,138 @@ Magistrala provides a complete set of building blocks for IoT systems — from d
|
||||
- Documentation focused on getting you running quickly
|
||||
---
|
||||
|
||||
## Atom Integration Model
|
||||
|
||||
Magistrala uses **Atom** as the backend for identity, authorization, and the catalog.
|
||||
|
||||
Atom is the source of truth for:
|
||||
- domains
|
||||
- users
|
||||
- clients
|
||||
- channels
|
||||
- groups
|
||||
- roles
|
||||
- access policies
|
||||
|
||||
Magistrala services such as rules, alarms, and reports remain Magistrala services, but they use Atom for identity and authorization.
|
||||
|
||||
### Core Entity Mapping
|
||||
|
||||
| Magistrala concept | Atom concept | Meaning |
|
||||
|--------------------|--------------|---------|
|
||||
| Domain | Tenant | Isolation boundary for one organization, project, or environment |
|
||||
| User | Entity with kind `human` | A person who logs in and uses the UI/API |
|
||||
| Client | Entity with kind `device` | A device or application that sends/receives data |
|
||||
| Channel | Resource with kind `channel` | A messaging/data path that clients can publish or subscribe to |
|
||||
| Group | Group | A collection of users, clients, channels, or other grouped objects |
|
||||
|
||||
In simple terms:
|
||||
|
||||
```text
|
||||
MG Domain = Atom Tenant
|
||||
MG User = Atom Human Entity
|
||||
MG Client = Atom Device Entity
|
||||
MG Channel = Atom Channel Resource
|
||||
MG Group = Atom Group
|
||||
```
|
||||
|
||||
### Actions, Permission Blocks, Roles, and Assignments
|
||||
|
||||
Atom access control has these basic parts:
|
||||
|
||||
| Atom word | Simple meaning | Example |
|
||||
|-----------|----------------|---------|
|
||||
| Action | One permission verb | `read`, `write`, `delete`, `role.manage`, `policy.manage` |
|
||||
| Permission Block | Where actions apply | all channels in domain `d1` can `read`, `publish` |
|
||||
| Role | A bundle of permission blocks | `tenant-admin` bundles domain, role, and member access |
|
||||
| Role Assignment | Who gets a role | give `user1` the `tenant-admin` role |
|
||||
|
||||
Read an assignment like this:
|
||||
|
||||
```text
|
||||
Give <who> this <role>.
|
||||
The role contains permission blocks that say where and what.
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Give user1 the tenant-admin role on domain d1.
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
```text
|
||||
user1 can use the tenant-admin permissions inside domain d1.
|
||||
```
|
||||
|
||||
### How MG Roles Work With Atom
|
||||
|
||||
MG UI shows actions such as:
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
- manage roles
|
||||
- add/remove members
|
||||
- publish
|
||||
- subscribe
|
||||
|
||||
These are mapped to Atom actions:
|
||||
|
||||
| MG action | Atom action |
|
||||
|-----------|-----------------|
|
||||
| view/read | `read` |
|
||||
| create/update/edit/connect | `write` |
|
||||
| delete/remove | `delete` |
|
||||
| manage roles | `role.manage` |
|
||||
| add/remove members or access | `policy.manage` |
|
||||
| channel publish | `publish` |
|
||||
| channel subscribe | `subscribe` |
|
||||
|
||||
So when MG UI checks:
|
||||
|
||||
```text
|
||||
Can user1 manage roles for client1?
|
||||
```
|
||||
|
||||
Atom checks:
|
||||
|
||||
```text
|
||||
Does user1 have role.manage on client1, or on the domain that contains client1?
|
||||
```
|
||||
|
||||
When MG UI checks:
|
||||
|
||||
```text
|
||||
Can user1 add a member to channel1?
|
||||
```
|
||||
|
||||
Atom checks:
|
||||
|
||||
```text
|
||||
Does user1 have policy.manage on channel1, or on the domain that contains channel1?
|
||||
```
|
||||
|
||||
### Practical Rule
|
||||
|
||||
If a user is domain admin, they usually receive a tenant-scoped role in Atom.
|
||||
|
||||
That tenant-scoped role can allow them to manage objects inside the domain:
|
||||
- clients
|
||||
- channels
|
||||
- groups
|
||||
- rules
|
||||
- alarms
|
||||
- reports
|
||||
|
||||
For narrower access, create object-scoped roles. For example:
|
||||
|
||||
```text
|
||||
Give user2 a reader role only on channel1.
|
||||
```
|
||||
|
||||
Then user2 can read only that channel, not the whole domain.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -151,54 +283,6 @@ make run_latest
|
||||
|
||||
---
|
||||
|
||||
## Upgrade from v0.19.0 to v0.20.0
|
||||
|
||||
Before upgrading, back up the Domains, Rules Engine, Reports, Alarms, Auth, and SpiceDB databases.
|
||||
|
||||
v0.20.0 adds new domain admin actions for alarms and reports, and it requires existing rules and reports to have their built-in admin roles backfilled. The service database migrations run when the v0.20.0 services start, then the role backfill scripts must be run once.
|
||||
|
||||
For the default Docker Compose setup:
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
|
||||
docker compose up -d \
|
||||
spicedb-db spicedb-migrate spicedb \
|
||||
auth-db auth \
|
||||
domains-db domains \
|
||||
re-db re \
|
||||
reports-db reports \
|
||||
alarms-db alarms
|
||||
```
|
||||
|
||||
Wait until the services are running. The `auth` service must start successfully because it loads the SpiceDB schema.
|
||||
|
||||
From the repository root, run the backfills:
|
||||
|
||||
```bash
|
||||
go run ./scripts/re-backfill-roles/
|
||||
go run ./scripts/reports-backfill-roles/
|
||||
```
|
||||
|
||||
The scripts are idempotent. If they are interrupted, fix the issue and run them again.
|
||||
|
||||
Expected successful summaries:
|
||||
|
||||
```text
|
||||
backfill finished processed=<number> skipped=<number> failed=0
|
||||
```
|
||||
|
||||
After the backfills finish, verify that the services are still running:
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
docker compose ps re reports alarms domains auth spicedb
|
||||
```
|
||||
|
||||
For non-default deployments, make sure the database and SpiceDB connection settings used by the backfill scripts match your environment before running them.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
|
||||
+6
-11
@@ -26,16 +26,11 @@ The service is configured using the following environment variables (values show
|
||||
| `MG_MESSAGE_BROKER_URL` | Message broker URL for alarm ingestion | `nats://nats:4222` |
|
||||
| `MG_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` |
|
||||
| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` |
|
||||
| `MG_AUTH_GRPC_URL` | Auth gRPC endpoint | `auth:7001` |
|
||||
| `MG_AUTH_GRPC_TIMEOUT` | Auth gRPC timeout | `300s` |
|
||||
| `MG_AUTH_GRPC_CLIENT_CERT` | Auth gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}` |
|
||||
| `MG_AUTH_GRPC_CLIENT_KEY` | Auth gRPC client key path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}` |
|
||||
| `MG_AUTH_GRPC_SERVER_CA_CERTS` | Auth gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `MG_DOMAINS_GRPC_URL` | Domains gRPC endpoint | `domains:7003` |
|
||||
| `MG_DOMAINS_GRPC_TIMEOUT` | Domains gRPC timeout | `300s` |
|
||||
| `MG_DOMAINS_GRPC_CLIENT_CERT` | Domains gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}` |
|
||||
| `MG_DOMAINS_GRPC_CLIENT_KEY` | Domains gRPC client key path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}` |
|
||||
| `MG_DOMAINS_GRPC_SERVER_CA_CERTS` | Domains gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `ATOM_URL` | Atom HTTP endpoint | `http://atom:8080` |
|
||||
| `ATOM_JWKS_URL` | Atom JWKS endpoint for JWT verification | `http://atom:8080/.well-known/jwks.json` |
|
||||
| `ATOM_ADMIN_USERNAME` | Atom admin login for service projections | `atom-admin` |
|
||||
| `ATOM_ADMIN_SECRET` | Atom admin secret for service projections | `change-me` |
|
||||
| `ATOM_TIMEOUT` | Atom request timeout | `5s` |
|
||||
| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` |
|
||||
|
||||
## Features
|
||||
@@ -44,7 +39,7 @@ The service is configured using the following environment variables (values show
|
||||
- **Stateful updates**: Updates assignee, acknowledgment, resolution, and metadata fields.
|
||||
- **Filtering and paging**: Lists alarms by domain, rule, channel, client, subtopic, status, severity, and time range.
|
||||
- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support.
|
||||
- **Auth and authorization**: Authn/authz enforced via gRPC auth and domains services.
|
||||
- **Auth and authorization**: Authn/authz enforced through Atom JWT verification and PDP checks.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
+1
-2
@@ -106,7 +106,7 @@ func (a Alarm) Validate() error {
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service.
|
||||
type Service interface {
|
||||
CreateAlarm(ctx context.Context, alarm Alarm) error
|
||||
CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error)
|
||||
UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error)
|
||||
ViewAlarm(ctx context.Context, session authn.Session, id string) (Alarm, error)
|
||||
ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error)
|
||||
@@ -118,6 +118,5 @@ type Repository interface {
|
||||
UpdateAlarm(ctx context.Context, alarm Alarm) (Alarm, error)
|
||||
ViewAlarm(ctx context.Context, alarmID, domainID string) (Alarm, error)
|
||||
ListAllAlarms(ctx context.Context, pm PageMetadata) (AlarmsPage, error)
|
||||
ListUserAlarms(ctx context.Context, userID string, pm PageMetadata) (AlarmsPage, error)
|
||||
DeleteAlarm(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/internal/atom"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
)
|
||||
|
||||
type atomService struct {
|
||||
Service
|
||||
projector atom.Projector
|
||||
}
|
||||
|
||||
func WithAtom(svc Service, projector atom.Projector) Service {
|
||||
if projector == nil {
|
||||
return svc
|
||||
}
|
||||
return atomService{Service: svc, projector: projector}
|
||||
}
|
||||
|
||||
func (svc atomService) CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) {
|
||||
created, err := svc.Service.CreateAlarm(ctx, alarm)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
if created.ID == "" {
|
||||
return created, nil
|
||||
}
|
||||
if err := svc.projector.UpsertResource(ctx, alarmProjection(created)); err != nil {
|
||||
return created, nil
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (svc atomService) UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error) {
|
||||
updated, err := svc.Service.UpdateAlarm(ctx, session, alarm)
|
||||
if err != nil {
|
||||
return updated, err
|
||||
}
|
||||
if err := svc.projector.UpsertResource(ctx, alarmProjection(updated)); err != nil {
|
||||
return updated, nil
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (svc atomService) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
if err := svc.Service.DeleteAlarm(ctx, session, id); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = svc.projector.DeleteResource(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func alarmProjection(a Alarm) atom.Resource {
|
||||
res := atom.ResourceFromFields(atom.ObjectFields{
|
||||
ID: a.ID,
|
||||
Kind: atom.KindAlarm,
|
||||
Name: a.Cause,
|
||||
TenantID: a.DomainID,
|
||||
OwnerID: a.AssigneeID,
|
||||
Status: a.Status.String(),
|
||||
Metadata: map[string]any(a.Metadata),
|
||||
UpdatedBy: a.UpdatedBy,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
})
|
||||
res.Attributes["rule_id"] = a.RuleID
|
||||
res.Attributes["channel_id"] = a.ChannelID
|
||||
res.Attributes["client_id"] = a.ClientID
|
||||
res.Attributes["subtopic"] = a.Subtopic
|
||||
res.Attributes["severity"] = a.Severity
|
||||
res.Attributes["measurement"] = a.Measurement
|
||||
res.Attributes["value"] = a.Value
|
||||
res.Attributes["unit"] = a.Unit
|
||||
res.Attributes["threshold"] = a.Threshold
|
||||
res.Attributes["cause"] = a.Cause
|
||||
res.Attributes["assignee_id"] = a.AssigneeID
|
||||
res.Attributes["assigned_at"] = alarmTimeString(a.AssignedAt)
|
||||
res.Attributes["assigned_by"] = a.AssignedBy
|
||||
res.Attributes["acknowledged_at"] = alarmTimeString(a.AcknowledgedAt)
|
||||
res.Attributes["acknowledged_by"] = a.AcknowledgedBy
|
||||
res.Attributes["resolved_at"] = alarmTimeString(a.ResolvedAt)
|
||||
res.Attributes["resolved_by"] = a.ResolvedBy
|
||||
return res
|
||||
}
|
||||
|
||||
func alarmTimeString(ts time.Time) string {
|
||||
if ts.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ts.Format(time.RFC3339Nano)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/internal/atom"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
)
|
||||
|
||||
func TestAtomServiceCreateAlarmProjectsCreatedAlarm(t *testing.T) {
|
||||
projector := &alarmProjector{}
|
||||
svc := WithAtom(alarmService{
|
||||
create: Alarm{
|
||||
ID: "alarm-1",
|
||||
RuleID: "rule-1",
|
||||
DomainID: "domain-1",
|
||||
ChannelID: "channel-1",
|
||||
ClientID: "client-1",
|
||||
Cause: "high temperature",
|
||||
Measurement: "temperature",
|
||||
Value: "92.4",
|
||||
Unit: "C",
|
||||
Threshold: "80",
|
||||
Severity: 90,
|
||||
Status: ActiveStatus,
|
||||
},
|
||||
}, projector)
|
||||
|
||||
created, err := svc.CreateAlarm(context.Background(), Alarm{RuleID: "rule-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("create alarm: %v", err)
|
||||
}
|
||||
if created.ID != "alarm-1" {
|
||||
t.Fatalf("unexpected created alarm: %#v", created)
|
||||
}
|
||||
if projector.resource.ID != "alarm-1" || projector.resource.Kind != atom.KindAlarm {
|
||||
t.Fatalf("unexpected projection: %#v", projector.resource)
|
||||
}
|
||||
if projector.resource.Attributes["rule_id"] != "rule-1" {
|
||||
t.Fatalf("missing rule projection: %#v", projector.resource.Attributes)
|
||||
}
|
||||
if projector.resource.Attributes["value"] != "92.4" || projector.resource.Attributes["threshold"] != "80" {
|
||||
t.Fatalf("missing alarm value projection: %#v", projector.resource.Attributes)
|
||||
}
|
||||
}
|
||||
|
||||
type alarmService struct {
|
||||
create Alarm
|
||||
}
|
||||
|
||||
func (svc alarmService) CreateAlarm(context.Context, Alarm) (Alarm, error) {
|
||||
return svc.create, nil
|
||||
}
|
||||
|
||||
func (svc alarmService) UpdateAlarm(context.Context, authn.Session, Alarm) (Alarm, error) {
|
||||
return Alarm{}, nil
|
||||
}
|
||||
|
||||
func (svc alarmService) ViewAlarm(context.Context, authn.Session, string) (Alarm, error) {
|
||||
return Alarm{}, nil
|
||||
}
|
||||
|
||||
func (svc alarmService) ListAlarms(context.Context, authn.Session, PageMetadata) (AlarmsPage, error) {
|
||||
return AlarmsPage{}, nil
|
||||
}
|
||||
|
||||
func (svc alarmService) DeleteAlarm(context.Context, authn.Session, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type alarmProjector struct {
|
||||
atom.Projector
|
||||
resource atom.Resource
|
||||
}
|
||||
|
||||
func (p *alarmProjector) UpsertResource(_ context.Context, resource atom.Resource) error {
|
||||
p.resource = resource
|
||||
return nil
|
||||
}
|
||||
@@ -48,7 +48,8 @@ func (h handler) Handle(msg *messaging.Message) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.svc.CreateAlarm(context.Background(), alarm)
|
||||
_, err = h.svc.CreateAlarm(context.Background(), alarm)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h handler) Cancel() error {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/operations"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/internal/atom"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
smqauthz "github.com/absmach/magistrala/pkg/authz"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
@@ -26,6 +27,7 @@ var (
|
||||
type authorizationMiddleware struct {
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
atomAuthz atom.Authorizer
|
||||
entitiesOps permissions.EntitiesOperations[permissions.Operation]
|
||||
}
|
||||
|
||||
@@ -43,7 +45,19 @@ func NewAuthorizationMiddleware(svc alarms.Service, authz smqauthz.Authorization
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func NewAtomAuthorizationMiddleware(svc alarms.Service, authz atom.Authorizer, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (alarms.Service, error) {
|
||||
if err := entitiesOps.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
atomAuthz: authz,
|
||||
entitiesOps: entitiesOps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
return am.svc.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
@@ -58,17 +72,19 @@ func (am *authorizationMiddleware) UpdateAlarm(ctx context.Context, session auth
|
||||
if err := am.authorize(ctx, operations.OpAssignAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
domainUserID := auth.EncodeDomainUserID(session.DomainID, alarm.AssigneeID)
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: domainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}, nil); err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
if am.atomAuthz == nil {
|
||||
domainUserID := auth.EncodeDomainUserID(session.DomainID, alarm.AssigneeID)
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: domainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}, nil); err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +120,9 @@ func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn
|
||||
case err == nil:
|
||||
session.SuperAdmin = true
|
||||
case errors.Contains(err, svcerr.ErrSuperAdminAction):
|
||||
if err := am.authorize(ctx, operations.OpListAlarms, session, operations.EntityType, auth.AnyIDs); err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
default:
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
@@ -124,6 +143,9 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if am.atomAuthz != nil {
|
||||
return atom.Authorize(ctx, am.atomAuthz, session, perm.String(), objType, obj, atom.KindAlarm)
|
||||
}
|
||||
|
||||
pr := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
@@ -159,6 +181,9 @@ func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session
|
||||
if session.Role != authn.SuperAdminRole {
|
||||
return svcerr.ErrSuperAdminAction
|
||||
}
|
||||
if am.atomAuthz != nil {
|
||||
return atom.Authorize(ctx, am.atomAuthz, session, policies.AdminPermission, policies.PlatformType, policies.MagistralaObject, policies.PlatformType)
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: session.UserID,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/mocks"
|
||||
"github.com/absmach/magistrala/alarms/operations"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/internal/atom"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
pkgerrors "github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/pkg/permissions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type recordingAtomAuthorizer struct {
|
||||
allowed bool
|
||||
reqs []atom.AuthzRequest
|
||||
}
|
||||
|
||||
func (a *recordingAtomAuthorizer) CheckAuthz(_ context.Context, req atom.AuthzRequest) (atom.AuthzResponse, error) {
|
||||
a.reqs = append(a.reqs, req)
|
||||
return atom.AuthzResponse{Allowed: a.allowed}, nil
|
||||
}
|
||||
|
||||
func TestListAlarmsAuthorizesRegularUser(t *testing.T) {
|
||||
svc := mocks.NewService(t)
|
||||
pm := alarms.PageMetadata{Limit: 10}
|
||||
expectedPM := pm
|
||||
expectedPM.DomainID = "domain-1"
|
||||
session := authn.Session{UserID: "user-1", DomainID: "domain-1", DomainUserID: "domain-1_user-1"}
|
||||
authz := &recordingAtomAuthorizer{allowed: true}
|
||||
wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
svc.On("ListAlarms", mock.Anything, session, expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once()
|
||||
page, err := wrapped.ListAlarms(context.Background(), session, pm)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(10), page.Limit)
|
||||
require.Len(t, authz.reqs, 1)
|
||||
assert.Equal(t, atom.AuthzRequest{
|
||||
SubjectID: "user-1",
|
||||
Action: "list",
|
||||
ResourceID: auth.AnyIDs,
|
||||
ObjectKind: "resource",
|
||||
ObjectID: auth.AnyIDs,
|
||||
Context: map[string]any{
|
||||
"domain_id": "domain-1",
|
||||
"legacy_object_type": operations.EntityType,
|
||||
},
|
||||
}, authz.reqs[0])
|
||||
}
|
||||
|
||||
func TestListAlarmsDeniedRegularUserDoesNotDelegate(t *testing.T) {
|
||||
svc := mocks.NewService(t)
|
||||
authz := &recordingAtomAuthorizer{allowed: false}
|
||||
wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = wrapped.ListAlarms(context.Background(), authn.Session{UserID: "user-1", DomainID: "domain-1"}, alarms.PageMetadata{})
|
||||
|
||||
assert.True(t, pkgerrors.Contains(err, pkgerrors.ErrAuthorization))
|
||||
require.Len(t, authz.reqs, 1)
|
||||
}
|
||||
|
||||
func TestListAlarmsSuperAdminSkipsListAuthorization(t *testing.T) {
|
||||
svc := mocks.NewService(t)
|
||||
pm := alarms.PageMetadata{Limit: 10}
|
||||
expectedPM := pm
|
||||
expectedPM.DomainID = "domain-1"
|
||||
session := authn.Session{UserID: "admin-1", DomainID: "domain-1", Role: authn.SuperAdminRole}
|
||||
authz := &recordingAtomAuthorizer{allowed: true}
|
||||
wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
svc.On("ListAlarms", mock.Anything, mock.MatchedBy(func(s authn.Session) bool {
|
||||
return s.SuperAdmin
|
||||
}), expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once()
|
||||
_, err = wrapped.ListAlarms(context.Background(), session, pm)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, authz.reqs, 1)
|
||||
assert.Equal(t, "manage", authz.reqs[0].Action)
|
||||
}
|
||||
|
||||
func testEntitiesOps(t *testing.T) permissions.EntitiesOperations[permissions.Operation] {
|
||||
t.Helper()
|
||||
details := operations.OperationDetails()
|
||||
perms := make(map[string]permissions.Permission, len(details))
|
||||
for _, detail := range details {
|
||||
if detail.PermissionRequired {
|
||||
perms[detail.Name] = permissions.Permission(detail.Name)
|
||||
}
|
||||
}
|
||||
entitiesOps, err := permissions.NewEntitiesOperations(
|
||||
permissions.EntitiesPermission{operations.EntityType: perms},
|
||||
permissions.EntitiesOperationDetails[permissions.Operation]{operations.EntityType: details},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return entitiesOps
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func NewLoggingMiddleware(logger *slog.Logger, service alarms.Service) alarms.Se
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (err error) {
|
||||
func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (created alarms.Alarm, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
@@ -52,7 +52,7 @@ func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm
|
||||
lm.logger.Warn("Create alarm failed", args...)
|
||||
return
|
||||
}
|
||||
if alarm.ID != "" {
|
||||
if created.ID != "" {
|
||||
lm.logger.Info("Create alarm completed successfully", args...)
|
||||
}
|
||||
}(time.Now())
|
||||
|
||||
@@ -28,7 +28,7 @@ func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, se
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "create_alarm").Add(1)
|
||||
mm.latency.With("method", "create_alarm").Observe(time.Since(begin).Seconds())
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewTracingMiddleware(tracer trace.Tracer, svc alarms.Service) alarms.Servic
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "create_alarm", trace.WithAttributes(
|
||||
attribute.String("rule_id", alarm.RuleID),
|
||||
attribute.String("measurement", alarm.Measurement),
|
||||
|
||||
@@ -231,78 +231,6 @@ func (_c *Repository_ListAllAlarms_Call) RunAndReturn(run func(ctx context.Conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, userID, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, userID, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListUserAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserAlarms'
|
||||
type Repository_ListUserAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListUserAlarms(ctx interface{}, userID interface{}, pm interface{}) *Repository_ListUserAlarms_Call {
|
||||
return &Repository_ListUserAlarms_Call{Call: _e.mock.On("ListUserAlarms", ctx, userID, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Run(run func(ctx context.Context, userID string, pm alarms.PageMetadata)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 alarms.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) RunAndReturn(run func(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
+17
-8
@@ -44,20 +44,29 @@ func (_m *Service) EXPECT() *Service_Expecter {
|
||||
}
|
||||
|
||||
// CreateAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) error); ok {
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
return r0
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm'
|
||||
@@ -90,12 +99,12 @@ func (_c *Service_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alar
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) Return(err error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
func (_c *Service_CreateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) error) *Service_CreateAlarm_Call {
|
||||
func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -198,35 +198,6 @@ func (r *repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata)
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
clauses := []string{
|
||||
`(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM rules_roles rr
|
||||
JOIN rules_role_members rrm ON rrm.role_id = rr.id
|
||||
WHERE rr.entity_id = alarms.rule_id AND rrm.member_id = :user_id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM domains_roles dr
|
||||
JOIN domains_role_members drm ON drm.role_id = dr.id
|
||||
JOIN domains_role_actions dra ON dra.role_id = dr.id
|
||||
WHERE dr.entity_id = alarms.domain_id
|
||||
AND drm.member_id = :user_id
|
||||
AND dra.action LIKE 'alarm%'
|
||||
)
|
||||
)`,
|
||||
}
|
||||
|
||||
clauses = append(clauses, pageQueryConditions(pm)...)
|
||||
query := fmt.Sprintf("WHERE %s", strings.Join(clauses, " AND "))
|
||||
pm.UserID = userID
|
||||
comQuery := fmt.Sprintf(`SELECT DISTINCT %s FROM alarms %s`, alarmColumns, query)
|
||||
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) alarmsPage(ctx context.Context, comQuery string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
dir := api.DescDir
|
||||
if pm.Dir == api.AscDir {
|
||||
|
||||
@@ -415,215 +415,6 @@ func TestListAlarms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserAlarms(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM domains_role_actions")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_role_actions unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains_role_members")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_role_members unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains_roles")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_roles unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM rules")
|
||||
require.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
domainID := generateUUID(t)
|
||||
domainRoute := generateUUID(t)
|
||||
userID := generateUUID(t)
|
||||
otherUserID := generateUUID(t)
|
||||
adminUserID := generateUUID(t)
|
||||
domainUserID := generateUUID(t)
|
||||
|
||||
_, err := db.Exec(`INSERT INTO domains (id, name, route, status) VALUES ($1, $2, $3, $4)`, domainID, namegen.Generate(), domainRoute, 0)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains unexpected error: %s", err))
|
||||
|
||||
// Create 10 rules and 10 alarms referencing them.
|
||||
// Assign userID to the first 6 rules via role membership.
|
||||
var ruleIDs []string
|
||||
var createdAlarms []alarms.Alarm
|
||||
for i := range 10 {
|
||||
ruleID := generateUUID(t)
|
||||
_, err := db.Exec(`INSERT INTO rules (id, name, domain_id, status, logic_type, logic_value) VALUES ($1, $2, $3, 0, 0, '')`,
|
||||
ruleID, fmt.Sprintf("rule-%d", i), domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert rule unexpected error: %s", err))
|
||||
ruleIDs = append(ruleIDs, ruleID)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: ruleID,
|
||||
DomainID: domainID,
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC().Add(time.Duration(i) * time.Minute),
|
||||
}
|
||||
alarm, err = repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
createdAlarms = append(createdAlarms, alarm)
|
||||
}
|
||||
|
||||
// Assign userID to the first 6 rules via rules_roles + rules_role_members.
|
||||
userRoleIDs := make([]string, 6)
|
||||
for i := range 6 {
|
||||
roleID := generateUUID(t)
|
||||
userRoleIDs[i] = roleID
|
||||
_, err := db.Exec(`INSERT INTO rules_roles (id, name, entity_id) VALUES ($1, $2, $3)`, roleID, "admin", ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_roles unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO rules_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, roleID, userID, ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
|
||||
}
|
||||
|
||||
for i := range 10 {
|
||||
var roleID string
|
||||
if i < 6 {
|
||||
roleID = userRoleIDs[i]
|
||||
} else {
|
||||
roleID = generateUUID(t)
|
||||
_, err := db.Exec(`INSERT INTO rules_roles (id, name, entity_id) VALUES ($1, $2, $3)`, roleID, "admin", ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_roles unexpected error: %s", err))
|
||||
}
|
||||
_, err := db.Exec(`INSERT INTO rules_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, roleID, adminUserID, ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
|
||||
}
|
||||
|
||||
domainRoleID := generateUUID(t)
|
||||
_, err = db.Exec(`INSERT INTO domains_roles (id, name, entity_id) VALUES ($1, $2, $3)`, domainRoleID, "admin", domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_roles unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO domains_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, domainRoleID, domainUserID, domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_role_members unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO domains_role_actions (role_id, action) VALUES ($1, $2)`, domainRoleID, "alarm_read")
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_role_actions unexpected error: %s", err))
|
||||
|
||||
_ = createdAlarms
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
pm alarms.PageMetadata
|
||||
count int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list user alarms returns only accessible alarms",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with limit",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
count: 3,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with offset",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 4,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 2,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with domain filter",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
DomainID: domainID,
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with non-existing domain returns 0",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
DomainID: generateUUID(t),
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 0,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for user with no role assignments returns 0",
|
||||
userID: otherUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 0,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for admin user with role on all rules returns all alarms",
|
||||
userID: adminUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for user with domain-level rule access returns all alarms",
|
||||
userID: domainUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms ordered by created_at ascending",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Order: "created_at",
|
||||
Dir: "asc",
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
page, err := repo.ListUserAlarms(context.Background(), tc.userID, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Equal(t, tc.count, len(page.Alarms), fmt.Sprintf("%s: expected %d alarms, got %d", tc.desc, tc.count, len(page.Alarms)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
rpostgres "github.com/absmach/magistrala/re/postgres"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
@@ -54,12 +51,5 @@ func Migration() (*migrate.MemoryMigrationSource, error) {
|
||||
},
|
||||
}
|
||||
|
||||
rulesMigration, err := rpostgres.Migration()
|
||||
if err != nil {
|
||||
return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err)
|
||||
}
|
||||
|
||||
alarmsMigration.Migrations = append(alarmsMigration.Migrations, rulesMigration.Migrations...)
|
||||
|
||||
return alarmsMigration, nil
|
||||
}
|
||||
|
||||
+12
-10
@@ -26,10 +26,10 @@ func NewService(idp magistrala.IDProvider, repo Repository) Service {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) {
|
||||
id, err := s.idp.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
return Alarm{}, err
|
||||
}
|
||||
alarm.ID = id
|
||||
if alarm.CreatedAt.IsZero() {
|
||||
@@ -37,14 +37,18 @@ func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
}
|
||||
|
||||
if err := alarm.Validate(); err != nil {
|
||||
return err
|
||||
return Alarm{}, err
|
||||
}
|
||||
|
||||
if _, err = s.repo.CreateAlarm(ctx, alarm); err != nil && err != repoerr.ErrNotFound {
|
||||
return err
|
||||
created, err := s.repo.CreateAlarm(ctx, alarm)
|
||||
if err != nil && err != repoerr.ErrNotFound {
|
||||
return Alarm{}, err
|
||||
}
|
||||
if err == repoerr.ErrNotFound {
|
||||
return Alarm{}, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID string) (Alarm, error) {
|
||||
@@ -52,10 +56,8 @@ func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID
|
||||
}
|
||||
|
||||
func (s *service) ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return s.repo.ListAllAlarms(ctx, pm)
|
||||
}
|
||||
return s.repo.ListUserAlarms(ctx, session.UserID, pm)
|
||||
pm.DomainID = session.DomainID
|
||||
return s.repo.ListAllAlarms(ctx, pm)
|
||||
}
|
||||
|
||||
func (s *service) DeleteAlarm(ctx context.Context, session authn.Session, alarmID string) error {
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestCreateAlarm(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repoCall := repo.On("CreateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err)
|
||||
err := svc.CreateAlarm(context.Background(), tc.alarm)
|
||||
_, err := svc.CreateAlarm(context.Background(), tc.alarm)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
})
|
||||
@@ -205,7 +205,7 @@ func TestListAlarms(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.pm.DomainID}
|
||||
repoCall := repo.On("ListUserAlarms", context.Background(), s.UserID, tc.pm).Return(tc.page, tc.err)
|
||||
repoCall := repo.On("ListAllAlarms", context.Background(), tc.pm).Return(tc.page, tc.err)
|
||||
_, err := svc.ListAlarms(context.Background(), s, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
+18
-6
@@ -13,10 +13,7 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/clients"
|
||||
"github.com/absmach/magistrala/groups"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/users"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
@@ -80,9 +77,9 @@ const (
|
||||
DefStartLevel = 1
|
||||
DefEndLevel = 0
|
||||
DefStatus = "enabled"
|
||||
DefClientStatus = clients.Enabled
|
||||
DefUserStatus = users.Enabled
|
||||
DefGroupStatus = groups.Enabled
|
||||
DefClientStatus = "enabled"
|
||||
DefUserStatus = "enabled"
|
||||
DefGroupStatus = "enabled"
|
||||
|
||||
// ContentType represents JSON content type.
|
||||
ContentType = "application/json"
|
||||
@@ -184,6 +181,21 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Contains(err, errors.ErrAuthentication) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if errors.Contains(err, errors.ErrAuthorization) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch retErr := err.(type) {
|
||||
case *errors.RequestError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
@@ -260,12 +260,24 @@ func TestEncodeError(t *testing.T) {
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Generic Authentication Failed",
|
||||
err: errors.ErrAuthentication,
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "AuthZError - Authorization Failed",
|
||||
err: svcerr.ErrAuthorization,
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Generic Authorization Failed",
|
||||
err: errors.Wrap(errors.New("not authorized"), errors.ErrAuthorization),
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "AuthZError - Domain Authorization Failed",
|
||||
err: svcerr.ErrDomainAuthorization,
|
||||
|
||||
@@ -66,6 +66,8 @@ paths:
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcAuthV1 "github.com/absmach/magistrala/api/grpc/auth/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
grpcapi "github.com/absmach/magistrala/auth/api/grpc/auth"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
id = "testID"
|
||||
usersType = "users"
|
||||
adminPermission = "admin"
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
validPATToken = "valid"
|
||||
)
|
||||
|
||||
var (
|
||||
domainID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
clientID = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcAuthV1.RegisterAuthServiceServer(server, grpcapi.NewAuthServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
defer conn.Close()
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
key auth.Key
|
||||
idt *grpcAuthV1.AuthNRes
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authenticate user with valid user token",
|
||||
token: validToken,
|
||||
key: auth.Key{ID: "", Subject: id, Role: auth.UserRole},
|
||||
idt: &grpcAuthV1.AuthNRes{UserId: id, UserRole: uint32(auth.UserRole)},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid user token",
|
||||
token: "invalid",
|
||||
key: auth.Key{},
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with empty token",
|
||||
token: "",
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with valid PAT token",
|
||||
token: "pat_" + validPATToken,
|
||||
key: auth.Key{ID: id, Type: auth.PersonalAccessToken, Subject: clientID, Role: auth.UserRole},
|
||||
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: clientID, UserRole: uint32(auth.UserRole), TokenType: uint32(auth.PersonalAccessToken)},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid PAT token",
|
||||
token: "pat_invalid",
|
||||
key: auth.Key{},
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("Identify", mock.Anything, tc.token).Return(tc.key, tc.svcErr)
|
||||
idt, err := grpcClient.Authenticate(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
defer conn.Close()
|
||||
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
authRequest *grpcAuthV1.AuthZReq
|
||||
authResponse *grpcAuthV1.AuthZRes
|
||||
expectedReq *policies.Policy
|
||||
expectedPAT *auth.PATAuthz
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty subject",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: "",
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty subject type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: "",
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty object",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: "",
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty object type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: "",
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty permission",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: "",
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMalformedPolicyPer,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with valid PAT token",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize bootstrap PAT keeps PAT domain when policy domain is empty",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: domainID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "create",
|
||||
UserId: id,
|
||||
EntityId: auth.AnyIDs,
|
||||
EntityType: auth.BootstrapStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
expectedReq: &policies.Policy{
|
||||
Domain: domainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: id,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: domainID,
|
||||
},
|
||||
expectedPAT: &auth.PATAuthz{
|
||||
PatID: id,
|
||||
UserID: id,
|
||||
EntityType: auth.BootstrapType,
|
||||
EntityID: auth.AnyIDs,
|
||||
Operation: "create",
|
||||
Domain: domainID,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized PAT token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize PAT with missing user id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingUserID,
|
||||
},
|
||||
{
|
||||
desc: "authorize PAT with missing entity id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return(tc.err)
|
||||
ar, err := grpcClient.Authorize(context.Background(), tc.authRequest)
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcTokenV1 "github.com/absmach/magistrala/api/grpc/token/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
grpcapi "github.com/absmach/magistrala/auth/api/grpc/token"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8082
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
invalidID = "invalid"
|
||||
)
|
||||
|
||||
var (
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcTokenV1.RegisterTokenServiceServer(server, grpcapi.NewTokenServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userId string
|
||||
kind auth.KeyType
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
userId: validID,
|
||||
kind: auth.AccessKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
userId: validID,
|
||||
kind: auth.RecoveryKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthenticated",
|
||||
userId: validID,
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
userId: validID,
|
||||
kind: 32,
|
||||
issueResponse: auth.Token{},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that does notexist",
|
||||
userId: "",
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Issue(context.Background(), &grpcTokenV1.IssueReq{UserId: tc.userId, Type: uint32(tc.kind)})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "refresh token with valid token",
|
||||
token: validToken,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with invalid token",
|
||||
token: inValidToken,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with empty token",
|
||||
token: "",
|
||||
issueResponse: auth.Token{},
|
||||
err: apiutil.ErrMissingSecret,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke token with valid id",
|
||||
id: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with invalid id",
|
||||
id: invalidID,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "revoke already revoked token",
|
||||
id: validID,
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("RevokeToken", mock.Anything, mock.Anything, tc.id).Return(tc.err)
|
||||
_, err := grpcClient.Revoke(context.Background(), &grpcTokenV1.RevokeReq{TokenId: tc.id})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserRefreshTokens(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
listResponse []auth.TokenInfo
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list tokens for user with valid id",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 1"},
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens for user with empty list",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with invalid user id",
|
||||
userID: invalidID,
|
||||
listResponse: nil,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with empty user id",
|
||||
userID: "",
|
||||
listResponse: nil,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("ListUserRefreshTokens", mock.Anything, tc.userID).Return(tc.listResponse, tc.err)
|
||||
_, err := grpcClient.ListUserRefreshTokens(context.Background(), &grpcTokenV1.ListUserRefreshTokensReq{UserId: tc.userID})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ type loggingMiddleware struct {
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// NewLogging adds logging facilities to the core service.
|
||||
// NewLogging adds logging facilities to the service.
|
||||
func NewLogging(svc auth.Service, logger *slog.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type metricsMiddleware struct {
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// NewMetrics instruments core service by tracking request count and latency.
|
||||
// NewMetrics instruments service by tracking request count and latency.
|
||||
func NewMetrics(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
channelsOps "github.com/absmach/magistrala/channels/operations"
|
||||
clientsOps "github.com/absmach/magistrala/clients/operations"
|
||||
groupsOps "github.com/absmach/magistrala/groups/operations"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopeAuthorized(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
entityType auth.EntityType
|
||||
domainID string
|
||||
operation string
|
||||
entityID string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Authorized with matching entity type, domain, operation and entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "any-entity",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: "view",
|
||||
EntityID: "client1",
|
||||
},
|
||||
entityType: auth.ClientsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "client1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity type",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.ChannelsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain2",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different operation",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "delete",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity2",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with nil scope",
|
||||
scope: nil,
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
result := tc.scope.Authorized(tc.entityType, tc.domainID, tc.operation, tc.entityID)
|
||||
assert.Equal(t, tc.expected, result, "Authorized() = %v, expected %v", result, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Valid scope for groups with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for channels with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for clients with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "update",
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for messages with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "domain1",
|
||||
Operation: "message_publish",
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for dashboard with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "domain1",
|
||||
Operation: "dashboard_share",
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil scope",
|
||||
scope: nil,
|
||||
err: assert.AnError, // Will be checked with Contains
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope without entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingEntityID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for groups without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for channels without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "",
|
||||
Operation: channelsOps.OperationDetails()[channelsOps.OpViewChannel].Name,
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for clients without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: clientsOps.OperationDetails()[clientsOps.OpViewClient].Name,
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for dashboard without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpShare,
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for messages without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpPublish,
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.scope.Validate()
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
if tc.err != assert.AnError {
|
||||
assert.Equal(t, tc.err, err, "Validate() error = %v, expected %v", err, tc.err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
pat *auth.PAT
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Valid PAT",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil PAT",
|
||||
pat: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without name",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without user",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.pat.Validate()
|
||||
if tc.err {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATMarshalUnmarshalBinary(t *testing.T) {
|
||||
pat := auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
Secret: "secret",
|
||||
IssuedAt: time.Now().UTC().Round(time.Second),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
|
||||
Status: auth.ActiveStatus,
|
||||
}
|
||||
|
||||
// Marshal
|
||||
data, err := pat.MarshalBinary()
|
||||
assert.NoError(t, err, "MarshalBinary() should not return error")
|
||||
assert.NotNil(t, data, "MarshalBinary() should return data")
|
||||
|
||||
// Unmarshal
|
||||
var newPAT auth.PAT
|
||||
err = newPAT.UnmarshalBinary(data)
|
||||
assert.NoError(t, err, "UnmarshalBinary() should not return error")
|
||||
|
||||
assert.Equal(t, pat.ID, newPAT.ID, "ID mismatch")
|
||||
assert.Equal(t, pat.User, newPAT.User, "User mismatch")
|
||||
assert.Equal(t, pat.Name, newPAT.Name, "Name mismatch")
|
||||
assert.Equal(t, pat.Description, newPAT.Description, "Description mismatch")
|
||||
assert.Equal(t, pat.Secret, newPAT.Secret, "Secret mismatch")
|
||||
assert.Equal(t, pat.Status, newPAT.Status, "Status mismatch")
|
||||
}
|
||||
@@ -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 bootstrap enrollments
|
||||
|
||||
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 bootstrap enrollments. Bootstrapping does not implicitly enable an enrollment; 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 a bootstrap enrollment is an enrollment toggle. Configuration keeps a _status_:
|
||||
|
||||
| Status | What it means |
|
||||
| -------- | ----------------------------------------------------------- |
|
||||
| disabled | Enrollment exists, but bootstrap is not allowed |
|
||||
| enabled | Enrollment can be used to fetch bootstrap configuration |
|
||||
|
||||
Switching between statuses `enabled` and `disabled` enables and disables the enrollment, 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 |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| MG_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | info |
|
||||
| MG_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MG_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| MG_BOOTSTRAP_DB_USER | Database user | magistrala |
|
||||
| MG_BOOTSTRAP_DB_PASS | Database password | magistrala |
|
||||
| MG_BOOTSTRAP_DB_NAME | Name of the database used by the service | bootstrap |
|
||||
| MG_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
|
||||
| MG_BOOTSTRAP_HTTP_HOST | Bootstrap service HTTP host | "" |
|
||||
| MG_BOOTSTRAP_HTTP_PORT | Bootstrap service HTTP port | 9013 |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_CERT | Path to server certificate in pem format | "" |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_KEY | Path to server key in pem format | "" |
|
||||
| MG_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MG_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| MG_AUTH_GRPC_URL | Auth service Auth gRPC URL | <localhost:8181> |
|
||||
| MG_AUTH_GRPC_TIMEOUT | Auth service Auth gRPC request timeout in seconds | 1s |
|
||||
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service Auth gRPC client certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service Auth gRPC client key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server Auth gRPC server trusted CA certificate file | "" |
|
||||
| MG_CLIENTS_URL | Base URL for Magistrala Clients | <http://localhost:9000> |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:4318/v1/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| MG_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.yaml) 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
|
||||
MG_BOOTSTRAP_LOG_LEVEL=info \
|
||||
MG_BOOTSTRAP_DB_HOST=localhost \
|
||||
MG_BOOTSTRAP_DB_PORT=5432 \
|
||||
MG_BOOTSTRAP_DB_USER=magistrala \
|
||||
MG_BOOTSTRAP_DB_PASS=magistrala \
|
||||
MG_BOOTSTRAP_DB_NAME=bootstrap \
|
||||
MG_BOOTSTRAP_DB_SSL_MODE=disable \
|
||||
MG_BOOTSTRAP_DB_SSL_CERT="" \
|
||||
MG_BOOTSTRAP_DB_SSL_KEY="" \
|
||||
MG_BOOTSTRAP_DB_SSL_ROOT_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_HOST=localhost \
|
||||
MG_BOOTSTRAP_HTTP_PORT=9013 \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_KEY="" \
|
||||
MG_BOOTSTRAP_EVENT_CONSUMER=bootstrap \
|
||||
MG_ES_URL=nats://localhost:4222 \
|
||||
MG_AUTH_GRPC_URL=localhost:8181 \
|
||||
MG_AUTH_GRPC_TIMEOUT=1s \
|
||||
MG_AUTH_GRPC_CLIENT_CERT="" \
|
||||
MG_AUTH_GRPC_CLIENT_KEY="" \
|
||||
MG_AUTH_GRPC_SERVER_CERTS="" \
|
||||
MG_CLIENTS_URL=http://localhost:9000 \
|
||||
MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_BOOTSTRAP_INSTANCE_ID="" \
|
||||
$GOBIN/magistrala-bootstrap
|
||||
```
|
||||
|
||||
Setting `MG_BOOTSTRAP_HTTP_SERVER_CERT` and `MG_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 `MG_AUTH_GRPC_CLIENT_CERT` and `MG_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 `MG_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.absmach.eu/?urls.primaryName=bootstrap.yaml).
|
||||
@@ -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
|
||||
@@ -1,506 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
Name: req.Name,
|
||||
ClientCert: req.ClientCert,
|
||||
ClientKey: req.ClientKey,
|
||||
CACert: req.CACert,
|
||||
Content: req.Content,
|
||||
ProfileID: req.ProfileID,
|
||||
RenderContext: req.RenderContext,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(ctx, session, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
ID: saved.ID,
|
||||
ExternalID: saved.ExternalID,
|
||||
Name: saved.Name,
|
||||
Content: saved.Content,
|
||||
Status: saved.Status,
|
||||
ProfileID: saved.ProfileID,
|
||||
RenderContext: saved.RenderContext,
|
||||
ClientCert: saved.ClientCert,
|
||||
CACert: saved.CACert,
|
||||
ClientKey: saved.ClientKey,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
cfg, err := svc.UpdateCert(ctx, session, req.configID, req.ClientCert, req.ClientKey, req.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := updateConfigRes{
|
||||
ID: cfg.ID,
|
||||
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 any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config, err := svc.View(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
ID: config.ID,
|
||||
ExternalID: config.ExternalID,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
Status: config.Status,
|
||||
ProfileID: config.ProfileID,
|
||||
RenderContext: config.RenderContext,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
RenderContext: req.RenderContext,
|
||||
}
|
||||
|
||||
if err := svc.Update(ctx, session, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updateRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.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 {
|
||||
view := viewRes{
|
||||
ID: cfg.ID,
|
||||
ExternalID: cfg.ExternalID,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
Status: cfg.Status,
|
||||
ProfileID: cfg.ProfileID,
|
||||
RenderContext: cfg.RenderContext,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.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 any) (any, 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 enableConfigEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeConfigStatusReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
cfg, err := svc.EnableConfig(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changeConfigStatusRes{Config: cfg}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disableConfigEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeConfigStatusReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
cfg, err := svc.DisableConfig(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changeConfigStatusRes{Config: cfg}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func createProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(createProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
saved, err := svc.CreateProfile(ctx, session, req.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profileRes{Profile: saved, created: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func uploadProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(uploadProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
saved, err := svc.CreateProfile(ctx, session, req.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profileRes{Profile: saved, created: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(viewProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
p, err := svc.ViewProfile(ctx, session, req.profileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profileRes{Profile: p}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func profileSlotsEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(viewProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
p, err := svc.ViewProfile(ctx, session, req.profileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profileSlotsRes{BindingSlots: p.BindingSlots}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func renderPreviewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(renderPreviewReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
p, err := svc.ViewProfile(ctx, session, req.profileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := req.Config
|
||||
bindings := req.Bindings
|
||||
|
||||
if req.ConfigID != "" {
|
||||
stored, err := svc.View(ctx, session, req.ConfigID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg = stored
|
||||
bindings, err = svc.ListBindings(ctx, session, req.ConfigID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cfg.DomainID = session.DomainID
|
||||
cfg.ProfileID = p.ID
|
||||
if cfg.RenderContext == nil {
|
||||
cfg.RenderContext = req.RenderContext
|
||||
}
|
||||
|
||||
rendered, err := bootstrap.NewRenderer().Render(p, cfg, bindings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return renderPreviewRes{Content: string(rendered)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
req.Profile.ID = req.profileID
|
||||
updated, err := svc.UpdateProfile(ctx, session, req.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profileRes{Profile: updated}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(deleteProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
if err := svc.DeleteProfile(ctx, session, req.profileID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listProfilesEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listProfilesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
page, err := svc.ListProfiles(ctx, session, req.offset, req.limit, req.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profilesPageRes{ProfilesPage: page}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func assignProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(assignProfileReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
if err := svc.AssignProfile(ctx, session, req.configID, req.ProfileID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func bindResourcesEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(bindResourcesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
if err := svc.BindResources(ctx, session, req.token, req.configID, req.Bindings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listBindingsEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listBindingsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
snapshots, err := svc.ListBindings(ctx, session, req.configID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bindingsRes{Bindings: snapshots}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func refreshBindingsEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(refreshBindingsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
if err := svc.RefreshBindings(ctx, session, req.token, req.configID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,280 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addReq struct {
|
||||
token string
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
ProfileID string `json:"profile_id"`
|
||||
RenderContext map[string]any `json:"render_context"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
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"`
|
||||
RenderContext map[string]any `json:"render_context"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
configID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.configID == "" {
|
||||
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 changeConfigStatusReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req changeConfigStatusReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Profile requests ---
|
||||
|
||||
type createProfileReq struct {
|
||||
bootstrap.Profile
|
||||
}
|
||||
|
||||
func (req createProfileReq) validate() error {
|
||||
if req.Name == "" {
|
||||
return apiutil.ErrMissingName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type uploadProfileReq struct {
|
||||
bootstrap.Profile
|
||||
}
|
||||
|
||||
func (req uploadProfileReq) validate() error {
|
||||
if req.Name == "" {
|
||||
return apiutil.ErrMissingName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewProfileReq struct {
|
||||
profileID string
|
||||
}
|
||||
|
||||
func (req viewProfileReq) validate() error {
|
||||
if req.profileID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateProfileReq struct {
|
||||
profileID string
|
||||
bootstrap.Profile
|
||||
}
|
||||
|
||||
func (req updateProfileReq) validate() error {
|
||||
if req.profileID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type renderPreviewReq struct {
|
||||
profileID string
|
||||
ConfigID string `json:"config_id,omitempty"`
|
||||
Config bootstrap.Config `json:"config"`
|
||||
RenderContext map[string]any `json:"render_context,omitempty"`
|
||||
Bindings []bootstrap.BindingSnapshot `json:"bindings,omitempty"`
|
||||
}
|
||||
|
||||
func (req renderPreviewReq) validate() error {
|
||||
if req.profileID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type deleteProfileReq struct {
|
||||
profileID string
|
||||
}
|
||||
|
||||
func (req deleteProfileReq) validate() error {
|
||||
if req.profileID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listProfilesReq struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
name string
|
||||
}
|
||||
|
||||
func (req listProfilesReq) validate() error {
|
||||
if req.limit == 0 || req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Enrollment binding requests ---
|
||||
|
||||
type assignProfileReq struct {
|
||||
configID string
|
||||
ProfileID string `json:"profile_id"`
|
||||
}
|
||||
|
||||
func (req assignProfileReq) validate() error {
|
||||
if req.configID == "" || req.ProfileID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type bindResourcesReq struct {
|
||||
token string
|
||||
configID string
|
||||
Bindings []bootstrap.BindingRequest `json:"bindings"`
|
||||
}
|
||||
|
||||
func (req bindResourcesReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.configID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if len(req.Bindings) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
for _, b := range req.Bindings {
|
||||
if b.Slot == "" || b.Type == "" || b.ResourceID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listBindingsReq struct {
|
||||
configID string
|
||||
}
|
||||
|
||||
func (req listBindingsReq) validate() error {
|
||||
if req.configID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type refreshBindingsReq struct {
|
||||
token string
|
||||
configID string
|
||||
}
|
||||
|
||||
func (req refreshBindingsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.configID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external key and external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
}
|
||||
|
||||
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
|
||||
configID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty config id",
|
||||
configID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
configID: tc.configID,
|
||||
}
|
||||
|
||||
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 TestChangeConfigStatusReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "valid request",
|
||||
token: "token",
|
||||
id: "id",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeConfigStatusReq{
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
)
|
||||
|
||||
var (
|
||||
_ magistrala.Response = (*removeRes)(nil)
|
||||
_ magistrala.Response = (*configRes)(nil)
|
||||
_ magistrala.Response = (*changeConfigStatusRes)(nil)
|
||||
_ magistrala.Response = (*viewRes)(nil)
|
||||
_ magistrala.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 updateRes struct{}
|
||||
|
||||
func (res updateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type configRes struct {
|
||||
ID string `json:"id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Status bootstrap.Status `json:"status"`
|
||||
ProfileID string `json:"profile_id,omitempty"`
|
||||
RenderContext map[string]any `json:"render_context,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
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 false
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Status bootstrap.Status `json:"status"`
|
||||
ProfileID string `json:"profile_id,omitempty"`
|
||||
RenderContext map[string]any `json:"render_context,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,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 changeConfigStatusRes struct {
|
||||
bootstrap.Config
|
||||
}
|
||||
|
||||
func (res changeConfigStatusRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res changeConfigStatusRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res changeConfigStatusRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type updateConfigRes struct {
|
||||
ID string `json:"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
|
||||
}
|
||||
|
||||
// profileRes is returned on create (201) or update (200).
|
||||
type profileRes struct {
|
||||
bootstrap.Profile
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res profileRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res profileRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/bootstrap/profiles/%s", res.ID),
|
||||
}
|
||||
}
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res profileRes) Empty() bool { return false }
|
||||
|
||||
// profilesPageRes is returned by ListProfiles.
|
||||
type profilesPageRes struct {
|
||||
bootstrap.ProfilesPage
|
||||
}
|
||||
|
||||
func (res profilesPageRes) Code() int { return http.StatusOK }
|
||||
func (res profilesPageRes) Headers() map[string]string { return map[string]string{} }
|
||||
func (res profilesPageRes) Empty() bool { return false }
|
||||
|
||||
// profileSlotsRes is returned by profile slots endpoint.
|
||||
type profileSlotsRes struct {
|
||||
BindingSlots []bootstrap.BindingSlot `json:"binding_slots"`
|
||||
}
|
||||
|
||||
func (res profileSlotsRes) Code() int { return http.StatusOK }
|
||||
func (res profileSlotsRes) Headers() map[string]string { return map[string]string{} }
|
||||
func (res profileSlotsRes) Empty() bool { return false }
|
||||
|
||||
// renderPreviewRes is returned by profile render-preview endpoint.
|
||||
type renderPreviewRes struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (res renderPreviewRes) Code() int { return http.StatusOK }
|
||||
func (res renderPreviewRes) Headers() map[string]string { return map[string]string{} }
|
||||
func (res renderPreviewRes) Empty() bool { return false }
|
||||
|
||||
// bindingsRes is returned by ListBindings.
|
||||
type bindingsRes struct {
|
||||
Bindings []bootstrap.BindingSnapshot `json:"bindings"`
|
||||
}
|
||||
|
||||
func (res bindingsRes) Code() int { return http.StatusOK }
|
||||
func (res bindingsRes) Headers() map[string]string { return map[string]string{} }
|
||||
func (res bindingsRes) Empty() bool { return false }
|
||||
@@ -1,512 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
yamlContentType = "yaml"
|
||||
tomlContentType = "toml"
|
||||
byteContentType = "application/octet-stream"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
fullMatch = []string{"status", "external_id", "id"}
|
||||
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.AuthNMiddleware, 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(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
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.Patch("/{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/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_cert").ServeHTTP)
|
||||
|
||||
r.Post("/{configID}/enable", otelhttp.NewHandler(kithttp.NewServer(
|
||||
enableConfigEndpoint(svc),
|
||||
decodeChangeConfigStatusRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "enable_config").ServeHTTP)
|
||||
|
||||
r.Post("/{configID}/disable", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disableConfigEndpoint(svc),
|
||||
decodeChangeConfigStatusRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "disable_config").ServeHTTP)
|
||||
})
|
||||
})
|
||||
|
||||
// Profile and enrollment binding endpoints.
|
||||
r.Route("/bootstrap", func(r chi.Router) {
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
|
||||
r.Route("/profiles", func(r chi.Router) {
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
createProfileEndpoint(svc),
|
||||
decodeCreateProfileRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "create_profile").ServeHTTP)
|
||||
|
||||
r.Post("/upload", otelhttp.NewHandler(kithttp.NewServer(
|
||||
uploadProfileEndpoint(svc),
|
||||
decodeUploadProfileRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "upload_profile").ServeHTTP)
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listProfilesEndpoint(svc),
|
||||
decodeListProfilesRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "list_profiles").ServeHTTP)
|
||||
|
||||
r.Get("/{profileID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewProfileEndpoint(svc),
|
||||
decodeProfileEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "view_profile").ServeHTTP)
|
||||
|
||||
r.Get("/{profileID}/slots", otelhttp.NewHandler(kithttp.NewServer(
|
||||
profileSlotsEndpoint(svc),
|
||||
decodeProfileEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "profile_slots").ServeHTTP)
|
||||
|
||||
r.Post("/{profileID}/render-preview", otelhttp.NewHandler(kithttp.NewServer(
|
||||
renderPreviewEndpoint(svc),
|
||||
decodeRenderPreviewRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "render_preview").ServeHTTP)
|
||||
|
||||
r.Patch("/{profileID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateProfileEndpoint(svc),
|
||||
decodeUpdateProfileRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_profile").ServeHTTP)
|
||||
|
||||
r.Delete("/{profileID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
deleteProfileEndpoint(svc),
|
||||
decodeDeleteProfileRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "delete_profile").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Route("/enrollments", func(r chi.Router) {
|
||||
r.Patch("/{configID}/profile", otelhttp.NewHandler(kithttp.NewServer(
|
||||
assignProfileEndpoint(svc),
|
||||
decodeAssignProfileRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "assign_profile").ServeHTTP)
|
||||
|
||||
r.Put("/{configID}/bindings", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bindResourcesEndpoint(svc),
|
||||
decodeBindResourcesRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bind_resources").ServeHTTP)
|
||||
|
||||
r.Get("/{configID}/bindings", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listBindingsEndpoint(svc),
|
||||
decodeEnrollmentEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "list_bindings").ServeHTTP)
|
||||
|
||||
r.Post("/{configID}/bindings/refresh", otelhttp.NewHandler(kithttp.NewServer(
|
||||
refreshBindingsEndpoint(svc),
|
||||
decodeRefreshBindingsRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "refresh_bindings").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", magistrala.Health("bootstrap", instanceID))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
configID: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (any, 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,
|
||||
}
|
||||
|
||||
rawStatus := q.Get("status")
|
||||
parsed, err := bootstrap.ToStatus(rawStatus)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidQueryParams)
|
||||
}
|
||||
if parsed == bootstrap.AllStatus {
|
||||
delete(req.filter.FullMatch, "status")
|
||||
} else {
|
||||
req.filter.FullMatch["status"] = parsed.String()
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := bootstrapReq{
|
||||
id: chi.URLParam(r, "externalID"),
|
||||
key: apiutil.ExtractClientSecret(r),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeChangeConfigStatusRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
return changeConfigStatusReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := entityReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response any) 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
|
||||
}
|
||||
|
||||
func decodeCreateProfileRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
var req createProfileReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Profile); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUploadProfileRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
var req uploadProfileReq
|
||||
var inferredFormat bootstrap.ContentFormat
|
||||
|
||||
switch {
|
||||
case strings.Contains(contentType, "json"):
|
||||
inferredFormat = bootstrap.ContentFormatJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Profile); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
case strings.Contains(contentType, yamlContentType):
|
||||
inferredFormat = bootstrap.ContentFormatYAML
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
if err := decodeYAMLProfile(body, &req.Profile); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
case strings.Contains(contentType, tomlContentType):
|
||||
inferredFormat = bootstrap.ContentFormatTOML
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
if err := decodeTOMLProfile(body, &req.Profile); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
default:
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
if req.Profile.ContentFormat == "" {
|
||||
req.Profile.ContentFormat = inferredFormat
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeYAMLProfile(body []byte, profile *bootstrap.Profile) error {
|
||||
var raw map[string]any
|
||||
if err := yaml.Unmarshal(body, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeProfileMap(raw, profile)
|
||||
}
|
||||
|
||||
func decodeTOMLProfile(body []byte, profile *bootstrap.Profile) error {
|
||||
var raw map[string]any
|
||||
if err := toml.Unmarshal(body, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeProfileMap(raw, profile)
|
||||
}
|
||||
|
||||
func decodeProfileMap(raw map[string]any, profile *bootstrap.Profile) error {
|
||||
body, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, profile)
|
||||
}
|
||||
|
||||
func decodeListProfilesRequest(_ context.Context, r *http.Request) (any, 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)
|
||||
}
|
||||
n, err := apiutil.ReadStringQuery(r, api.NameKey, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
return listProfilesReq{offset: o, limit: l, name: n}, nil
|
||||
}
|
||||
|
||||
func decodeProfileEntityRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
return viewProfileReq{profileID: chi.URLParam(r, "profileID")}, nil
|
||||
}
|
||||
|
||||
func decodeDeleteProfileRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
return deleteProfileReq{profileID: chi.URLParam(r, "profileID")}, nil
|
||||
}
|
||||
|
||||
func decodeUpdateProfileRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
req := updateProfileReq{profileID: chi.URLParam(r, "profileID")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Profile); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRenderPreviewRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
req := renderPreviewReq{profileID: chi.URLParam(r, "profileID")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeAssignProfileRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
req := assignProfileReq{configID: chi.URLParam(r, "configID")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBindResourcesRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
req := bindResourcesReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
configID: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEnrollmentEntityRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
return listBindingsReq{configID: chi.URLParam(r, "configID")}, nil
|
||||
}
|
||||
|
||||
func decodeRefreshBindingsRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
return refreshBindingsReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
configID: chi.URLParam(r, "configID"),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
)
|
||||
|
||||
var errBindingSlot = errors.New("invalid binding slot")
|
||||
|
||||
func validateProfileBindingSlots(profile Profile) error {
|
||||
seen := make(map[string]struct{}, len(profile.BindingSlots))
|
||||
for _, slot := range profile.BindingSlots {
|
||||
if slot.Name == "" {
|
||||
return fmt.Errorf("%w: slot name is required", errBindingSlot)
|
||||
}
|
||||
if slot.Type == "" {
|
||||
return fmt.Errorf("%w: slot %q type is required", errBindingSlot, slot.Name)
|
||||
}
|
||||
if _, ok := seen[slot.Name]; ok {
|
||||
return fmt.Errorf("%w: duplicate slot %q", errBindingSlot, slot.Name)
|
||||
}
|
||||
seen[slot.Name] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRequestedBindings(profile Profile, requested []BindingRequest) error {
|
||||
if len(profile.BindingSlots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
slots := make(map[string]BindingSlot, len(profile.BindingSlots))
|
||||
for _, slot := range profile.BindingSlots {
|
||||
slots[slot.Name] = slot
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(requested))
|
||||
for _, binding := range requested {
|
||||
slot, ok := slots[binding.Slot]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unknown slot %q", errBindingSlot, binding.Slot)
|
||||
}
|
||||
if slot.Type != binding.Type {
|
||||
return fmt.Errorf("%w: slot %q expects %q, got %q", errBindingSlot, binding.Slot, slot.Type, binding.Type)
|
||||
}
|
||||
if _, ok := seen[binding.Slot]; ok {
|
||||
return fmt.Errorf("%w: duplicate binding for slot %q", errBindingSlot, binding.Slot)
|
||||
}
|
||||
seen[binding.Slot] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRequiredBindings(profile Profile, bindings []BindingSnapshot) error {
|
||||
if len(profile.BindingSlots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bound := make(map[string]BindingSnapshot, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
bound[binding.Slot] = binding
|
||||
}
|
||||
|
||||
for _, slot := range profile.BindingSlots {
|
||||
binding, ok := bound[slot.Name]
|
||||
if !slot.Required && !ok {
|
||||
continue
|
||||
}
|
||||
if slot.Required && !ok {
|
||||
return fmt.Errorf("%w: required slot %q is not bound", errBindingSlot, slot.Name)
|
||||
}
|
||||
if binding.Type != slot.Type {
|
||||
return fmt.Errorf("%w: slot %q expects %q, got %q", errBindingSlot, slot.Name, slot.Type, binding.Type)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeBindingSnapshots(existing, updated []BindingSnapshot) []BindingSnapshot {
|
||||
merged := make(map[string]BindingSnapshot, len(existing)+len(updated))
|
||||
for _, binding := range existing {
|
||||
merged[binding.Slot] = binding
|
||||
}
|
||||
for _, binding := range updated {
|
||||
merged[binding.Slot] = binding
|
||||
}
|
||||
|
||||
bindings := make([]BindingSnapshot, 0, len(merged))
|
||||
for _, binding := range merged {
|
||||
bindings = append(bindings, binding)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
func validateProfileTemplate(p Profile) error {
|
||||
if p.ContentTemplate == "" || p.ContentFormat == ContentFormatRaw {
|
||||
return nil
|
||||
}
|
||||
_, err := template.New("bootstrap").Funcs(allowlistedFuncs()).Parse(p.ContentTemplate)
|
||||
if err != nil {
|
||||
return errors.Wrap(ErrRenderFailed, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BindingRequest carries a user's intent to bind a named profile slot to
|
||||
// a concrete resource.
|
||||
type BindingRequest struct {
|
||||
Slot string `json:"slot"`
|
||||
Type string `json:"type"` // "client" | "channel" | "cert"
|
||||
ResourceID string `json:"resource_id"` // ID of the resource in its owning service
|
||||
}
|
||||
|
||||
// BindingSnapshot is a Bootstrap-owned point-in-time copy of the resource
|
||||
// fields needed for template rendering. It is populated at binding time so
|
||||
// that the render path never calls external services.
|
||||
type BindingSnapshot struct {
|
||||
ConfigID string `json:"config_id"`
|
||||
Slot string `json:"slot"`
|
||||
Type string `json:"type"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
Snapshot map[string]any `json:"snapshot,omitempty"`
|
||||
SecretSnapshot map[string]any `json:"secret_snapshot,omitempty"` // encrypted at rest
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// BindingStore is the persistence interface for BindingSnapshots.
|
||||
type BindingStore interface {
|
||||
// Save upserts all given snapshots for the config.
|
||||
Save(ctx context.Context, configID string, bindings []BindingSnapshot) error
|
||||
|
||||
// Retrieve returns all snapshots for the given config.
|
||||
Retrieve(ctx context.Context, configID string) ([]BindingSnapshot, error)
|
||||
|
||||
// Delete removes the snapshot for a specific slot of a config.
|
||||
Delete(ctx context.Context, configID, slot string) error
|
||||
}
|
||||
|
||||
// ResolveRequest carries everything the BindingResolver needs to snapshot a
|
||||
// set of resource bindings.
|
||||
type ResolveRequest struct {
|
||||
Enrollment Config
|
||||
Token string
|
||||
Requested []BindingRequest
|
||||
}
|
||||
|
||||
// BindingResolver validates that requested resources exist in their owning
|
||||
// services, verifies type and slot compatibility, and returns snapshots ready
|
||||
// for storage. It is called at binding time only; the render path must not
|
||||
// call it.
|
||||
type BindingResolver interface {
|
||||
Resolve(ctx context.Context, req ResolveRequest) ([]BindingSnapshot, error)
|
||||
}
|
||||
|
||||
// RenderContext is the typed value injected into Go templates during rendering.
|
||||
type RenderContext struct {
|
||||
Device DeviceContext
|
||||
Vars map[string]any
|
||||
Bindings map[string]BindingContext
|
||||
}
|
||||
|
||||
// DeviceContext holds enrollment identity fields available inside templates.
|
||||
type DeviceContext struct {
|
||||
ID string
|
||||
ExternalID string
|
||||
DomainID string
|
||||
}
|
||||
|
||||
// BindingContext holds the resolved resource data available inside templates
|
||||
// for a specific slot.
|
||||
type BindingContext struct {
|
||||
Type string
|
||||
ID string
|
||||
Snapshot map[string]any
|
||||
Secret map[string]any
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import "context"
|
||||
|
||||
// Config represents a bootstrap enrollment.
|
||||
type Config struct {
|
||||
ID string `json:"id"`
|
||||
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"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
ProfileID string `json:"profile_id,omitempty"`
|
||||
RenderContext map[string]any `json:"render_context,omitempty"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(ctx context.Context, cfg Config) (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 belong to the given domain,
|
||||
// with given filter parameters.
|
||||
RetrieveAll(ctx context.Context, domainID 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
|
||||
|
||||
// AssignProfile sets the profile reference for the given Config.
|
||||
AssignProfile(ctx context.Context, domainID, id, profileID string) 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, id, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(ctx context.Context, domainID, id string) error
|
||||
|
||||
// ChangeStatus changes the Status of the Config owned by the specific user.
|
||||
ChangeStatus(ctx context.Context, domainID, id string, status Status) error
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// Magistrala bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,288 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
configPrefix = "bootstrap.config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configView = configPrefix + "view"
|
||||
configList = configPrefix + "list"
|
||||
clientPrefix = "bootstrap.client."
|
||||
clientBootstrap = clientPrefix + "bootstrap"
|
||||
configEnable = configPrefix + "enable"
|
||||
configDisable = configPrefix + "disable"
|
||||
certUpdate = "bootstrap.cert.update"
|
||||
|
||||
profilePrefix = "bootstrap.profile."
|
||||
profileCreate = profilePrefix + "create"
|
||||
profileView = profilePrefix + "view"
|
||||
profileUpdate = profilePrefix + "update"
|
||||
profileList = profilePrefix + "list"
|
||||
profileDelete = profilePrefix + "delete"
|
||||
profileAssign = profilePrefix + "assign"
|
||||
bindingsPrefix = "bootstrap.bindings."
|
||||
bindingsBind = bindingsPrefix + "bind"
|
||||
bindingsList = bindingsPrefix + "list"
|
||||
bindingsRefresh = bindingsPrefix + "refresh"
|
||||
)
|
||||
|
||||
var (
|
||||
_ events.Event = (*configEvent)(nil)
|
||||
_ events.Event = (*removeConfigEvent)(nil)
|
||||
_ events.Event = (*bootstrapEvent)(nil)
|
||||
_ events.Event = (*enableConfigEvent)(nil)
|
||||
_ events.Event = (*disableConfigEvent)(nil)
|
||||
_ events.Event = (*updateCertEvent)(nil)
|
||||
_ events.Event = (*listConfigsEvent)(nil)
|
||||
_ events.Event = (*profileEvent)(nil)
|
||||
_ events.Event = (*deleteProfileEvent)(nil)
|
||||
_ events.Event = (*assignProfileEvent)(nil)
|
||||
_ events.Event = (*bindResourcesEvent)(nil)
|
||||
_ events.Event = (*listBindingsEvent)(nil)
|
||||
_ events.Event = (*refreshBindingsEvent)(nil)
|
||||
)
|
||||
|
||||
type configEvent struct {
|
||||
bootstrap.Config
|
||||
operation string
|
||||
}
|
||||
|
||||
func (ce configEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"status": ce.Status.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
if ce.ID != "" {
|
||||
val["config_id"] = ce.ID
|
||||
}
|
||||
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 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 {
|
||||
config string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": rce.config,
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listConfigsEvent struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
fullMatch map[string]string
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"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]any, error) {
|
||||
val := map[string]any{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"operation": clientBootstrap,
|
||||
}
|
||||
|
||||
if be.ID != "" {
|
||||
val["config_id"] = be.ID
|
||||
}
|
||||
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 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 enableConfigEvent struct {
|
||||
configID string
|
||||
}
|
||||
|
||||
func (e enableConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": e.configID,
|
||||
"operation": configEnable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type disableConfigEvent struct {
|
||||
configID string
|
||||
}
|
||||
|
||||
func (e disableConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": e.configID,
|
||||
"operation": configDisable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateCertEvent struct {
|
||||
configID string
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": uce.configID,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
"ca_cert": uce.caCert,
|
||||
"operation": certUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type profileEvent struct {
|
||||
bootstrap.Profile
|
||||
operation string
|
||||
}
|
||||
|
||||
func (pe profileEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"operation": pe.operation,
|
||||
}
|
||||
if pe.ID != "" {
|
||||
val["profile_id"] = pe.ID
|
||||
}
|
||||
if pe.DomainID != "" {
|
||||
val["domain_id"] = pe.DomainID
|
||||
}
|
||||
if pe.Name != "" {
|
||||
val["name"] = pe.Name
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type deleteProfileEvent struct {
|
||||
profileID string
|
||||
}
|
||||
|
||||
func (dpe deleteProfileEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"profile_id": dpe.profileID,
|
||||
"operation": profileDelete,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type assignProfileEvent struct {
|
||||
configID string
|
||||
profileID string
|
||||
}
|
||||
|
||||
func (ape assignProfileEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": ape.configID,
|
||||
"profile_id": ape.profileID,
|
||||
"operation": profileAssign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type bindResourcesEvent struct {
|
||||
configID string
|
||||
slots []string
|
||||
}
|
||||
|
||||
func (bre bindResourcesEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": bre.configID,
|
||||
"slots": bre.slots,
|
||||
"operation": bindingsBind,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listBindingsEvent struct {
|
||||
configID string
|
||||
}
|
||||
|
||||
func (lbe listBindingsEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": lbe.configID,
|
||||
"operation": bindingsList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type refreshBindingsEvent struct {
|
||||
configID string
|
||||
}
|
||||
|
||||
func (rbe refreshBindingsEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": rbe.configID,
|
||||
"operation": bindingsRefresh,
|
||||
}, nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/events"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*eventStore)(nil)
|
||||
|
||||
const (
|
||||
magistralaPrefix = "magistrala."
|
||||
createStream = magistralaPrefix + configCreate
|
||||
listStream = magistralaPrefix + configList
|
||||
removeStream = magistralaPrefix + configRemove
|
||||
updateCertStream = magistralaPrefix + certUpdate
|
||||
bootstrapStream = magistralaPrefix + clientBootstrap
|
||||
enableConfigStream = magistralaPrefix + configEnable
|
||||
disableConfigStream = magistralaPrefix + configDisable
|
||||
createProfileStream = magistralaPrefix + profileCreate
|
||||
viewProfileStream = magistralaPrefix + profileView
|
||||
updateProfileStream = magistralaPrefix + profileUpdate
|
||||
listProfilesStream = magistralaPrefix + profileList
|
||||
deleteProfileStream = magistralaPrefix + profileDelete
|
||||
assignProfileStream = magistralaPrefix + profileAssign
|
||||
bindResourcesStream = magistralaPrefix + bindingsBind
|
||||
listBindingsStream = magistralaPrefix + bindingsList
|
||||
refreshBindingsStream = magistralaPrefix + bindingsRefresh
|
||||
)
|
||||
|
||||
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, createStream, 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, magistralaPrefix+configView, 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, magistralaPrefix+configUpdate, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := updateCertEvent{
|
||||
configID: id,
|
||||
clientCert: clientCert,
|
||||
clientKey: clientKey,
|
||||
caCert: caCert,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, updateCertStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
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, listStream, 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{
|
||||
config: id,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeStream, 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, bootstrapStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.EnableConfig(ctx, session, id)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := enableConfigEvent{configID: id}
|
||||
if err := es.Publish(ctx, enableConfigStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.DisableConfig(ctx, session, id)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := disableConfigEvent{configID: id}
|
||||
if err := es.Publish(ctx, disableConfigStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
saved, err := es.svc.CreateProfile(ctx, session, p)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
ev := profileEvent{saved, profileCreate}
|
||||
if err := es.Publish(ctx, createProfileStream, ev); err != nil {
|
||||
return saved, err
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
|
||||
p, err := es.svc.ViewProfile(ctx, session, profileID)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
ev := profileEvent{p, profileView}
|
||||
if err := es.Publish(ctx, viewProfileStream, ev); err != nil {
|
||||
return p, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
updated, err := es.svc.UpdateProfile(ctx, session, p)
|
||||
if err != nil {
|
||||
return bootstrap.Profile{}, err
|
||||
}
|
||||
ev := profileEvent{updated, profileUpdate}
|
||||
return updated, es.Publish(ctx, updateProfileStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
|
||||
pp, err := es.svc.ListProfiles(ctx, session, offset, limit, name)
|
||||
if err != nil {
|
||||
return pp, err
|
||||
}
|
||||
ev := profileEvent{operation: profileList}
|
||||
if err := es.Publish(ctx, listProfilesStream, ev); err != nil {
|
||||
return pp, err
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
|
||||
if err := es.svc.DeleteProfile(ctx, session, profileID); err != nil {
|
||||
return err
|
||||
}
|
||||
ev := deleteProfileEvent{profileID: profileID}
|
||||
return es.Publish(ctx, deleteProfileStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
|
||||
if err := es.svc.AssignProfile(ctx, session, configID, profileID); err != nil {
|
||||
return err
|
||||
}
|
||||
ev := assignProfileEvent{configID: configID, profileID: profileID}
|
||||
return es.Publish(ctx, assignProfileStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
|
||||
if err := es.svc.BindResources(ctx, session, token, configID, bindings); err != nil {
|
||||
return err
|
||||
}
|
||||
slots := make([]string, len(bindings))
|
||||
for i, b := range bindings {
|
||||
slots[i] = b.Slot
|
||||
}
|
||||
ev := bindResourcesEvent{configID: configID, slots: slots}
|
||||
return es.Publish(ctx, bindResourcesStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
|
||||
bs, err := es.svc.ListBindings(ctx, session, configID)
|
||||
if err != nil {
|
||||
return bs, err
|
||||
}
|
||||
ev := listBindingsEvent{configID: configID}
|
||||
if err := es.Publish(ctx, listBindingsStream, ev); err != nil {
|
||||
return bs, err
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
|
||||
if err := es.svc.RefreshBindings(ctx, session, token, configID); err != nil {
|
||||
return err
|
||||
}
|
||||
ev := refreshBindingsEvent{configID: configID}
|
||||
return es.Publish(ctx, refreshBindingsStream, ev)
|
||||
}
|
||||
@@ -1,914 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/bootstrap/events/producer"
|
||||
bootstraphasher "github.com/absmach/magistrala/bootstrap/hasher"
|
||||
"github.com/absmach/magistrala/bootstrap/mocks"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/events/store"
|
||||
sdkmocks "github.com/absmach/magistrala/pkg/sdk/mocks"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
streamID = "magistrala.bootstrap"
|
||||
validToken = "validToken"
|
||||
unknownID = "unknown"
|
||||
|
||||
configPrefix = "config."
|
||||
configCreate = configPrefix + "create"
|
||||
configView = configPrefix + "view"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configList = configPrefix + "list"
|
||||
clientPrefix = "client."
|
||||
clientBootstrap = clientPrefix + "bootstrap"
|
||||
configEnable = configPrefix + "enable"
|
||||
configDisable = configPrefix + "disable"
|
||||
|
||||
certUpdate = "cert.update"
|
||||
)
|
||||
|
||||
var (
|
||||
encKey = []byte("1234567891011121")
|
||||
|
||||
domainID = testsutil.GenerateUUID(&testing.T{})
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
config = bootstrap.Config{
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
ExternalID: testsutil.GenerateUUID(&testing.T{}),
|
||||
ExternalKey: testsutil.GenerateUUID(&testing.T{}),
|
||||
Content: "config",
|
||||
Status: bootstrap.EnabledStatus,
|
||||
}
|
||||
)
|
||||
|
||||
type testVariable struct {
|
||||
svc bootstrap.Service
|
||||
boot *mocks.ConfigRepository
|
||||
sdk *sdkmocks.SDK
|
||||
}
|
||||
|
||||
func newTestVariable(t *testing.T, redisURL string) testVariable {
|
||||
boot := new(mocks.ConfigRepository)
|
||||
sdk := new(sdkmocks.SDK)
|
||||
idp := uuid.NewMock()
|
||||
svc := bootstrap.New(boot, nil, nil, nil, nil, sdk, bootstraphasher.New(), encKey, idp)
|
||||
publisher, err := store.NewPublisher(context.Background(), redisURL, "bootstrap-es-pub-test")
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
svc = producer.NewEventStoreMiddleware(svc, publisher)
|
||||
return testVariable{
|
||||
svc: svc,
|
||||
boot: boot,
|
||||
sdk: sdk,
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
session smqauthn.Session
|
||||
id string
|
||||
domainID string
|
||||
saveErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "create config successfully",
|
||||
config: config,
|
||||
token: validToken,
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
event: map[string]any{
|
||||
"config_id": "1",
|
||||
"domain_id": domainID,
|
||||
"name": config.Name,
|
||||
"external_id": config.ExternalID,
|
||||
"content": config.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configCreate,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create config with failed to save",
|
||||
config: config,
|
||||
token: validToken,
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
event: nil,
|
||||
saveErr: svcerr.ErrCreateEntity,
|
||||
err: svcerr.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: validID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("Save", context.Background(), mock.Anything).Return(mock.Anything, tc.saveErr)
|
||||
|
||||
_, err := tv.svc.Add(context.Background(), tc.session, tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.ID = unknownID
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
session smqauthn.Session
|
||||
id string
|
||||
domainID string
|
||||
retrieveErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "view successfully",
|
||||
config: config,
|
||||
token: validToken,
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": config.ID,
|
||||
"domain_id": config.DomainID,
|
||||
"name": config.Name,
|
||||
"external_id": config.ExternalID,
|
||||
"content": config.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configView,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "view with failed retrieve",
|
||||
config: nonExisting,
|
||||
token: validToken,
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
retrieveErr: svcerr.ErrViewEntity,
|
||||
err: svcerr.ErrViewEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: validID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("RetrieveByID", context.Background(), tc.domainID, tc.config.ID).Return(config, tc.retrieveErr)
|
||||
_, err := tv.svc.View(context.Background(), tc.session, tc.config.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
event["timestamp"] = msg.ID
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
modified := config
|
||||
modified.Content = "new-config"
|
||||
modified.Name = "new name"
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.ID = unknownID
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
session smqauthn.Session
|
||||
id string
|
||||
domainID string
|
||||
updateErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "update config successfully",
|
||||
config: modified,
|
||||
token: validToken,
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"name": modified.Name,
|
||||
"content": modified.Content,
|
||||
"timestamp": time.Now().UnixNano(),
|
||||
"operation": configUpdate,
|
||||
"external_id": modified.ExternalID,
|
||||
"config_id": modified.ID,
|
||||
"domain_id": domainID,
|
||||
"status": bootstrap.Disabled,
|
||||
"occurred_at": time.Now().UnixNano(),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "update with failed update",
|
||||
config: nonExisting,
|
||||
token: validToken,
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
updateErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: validID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("Update", context.Background(), mock.Anything).Return(tc.updateErr)
|
||||
err := tv.svc.Update(context.Background(), tc.session, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
event["timestamp"] = msg.ID
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
configID string
|
||||
userID string
|
||||
domainID string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
updateErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "update cert successfully",
|
||||
configID: config.ID,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
token: validToken,
|
||||
clientCert: "clientCert",
|
||||
clientKey: "clientKey",
|
||||
caCert: "caCert",
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"client_cert": "clientCert",
|
||||
"client_key": "clientKey",
|
||||
"ca_cert": "caCert",
|
||||
"operation": certUpdate,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "update cert with failed update",
|
||||
configID: "clientID",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
clientCert: "clientCert",
|
||||
clientKey: "clientKey",
|
||||
caCert: "caCert",
|
||||
updateErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "update cert with empty client certificate",
|
||||
configID: config.ID,
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
clientCert: "",
|
||||
clientKey: "clientKey",
|
||||
caCert: "caCert",
|
||||
err: nil,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "update cert with empty client key",
|
||||
configID: config.ID,
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
clientCert: "clientCert",
|
||||
clientKey: "",
|
||||
caCert: "caCert",
|
||||
err: nil,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "update cert with empty CA certificate",
|
||||
configID: config.ID,
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
clientCert: "clientCert",
|
||||
clientKey: "clientKey",
|
||||
caCert: "",
|
||||
err: nil,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("UpdateCert", context.Background(), tc.domainID, tc.configID, tc.clientCert, tc.clientKey, tc.caCert).Return(config, tc.updateErr)
|
||||
_, err := tv.svc.UpdateCert(context.Background(), tc.session, tc.configID, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
numClients := 101
|
||||
var c bootstrap.Config
|
||||
saved := make([]bootstrap.Config, 0)
|
||||
for i := 0; i < numClients; i++ {
|
||||
c = config
|
||||
c.ExternalID = testsutil.GenerateUUID(t)
|
||||
c.ExternalKey = testsutil.GenerateUUID(t)
|
||||
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
|
||||
if i == 41 {
|
||||
c.Status = bootstrap.Active
|
||||
}
|
||||
saved = append(saved, c)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
userID string
|
||||
domainID string
|
||||
config bootstrap.ConfigsPage
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
retrieveErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "list successfully as super admin",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": c.ID,
|
||||
"domain_id": c.DomainID,
|
||||
"name": c.Name,
|
||||
"external_id": c.ExternalID,
|
||||
"content": c.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configList,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "list successfully as domain admin",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": c.ID,
|
||||
"domain_id": c.DomainID,
|
||||
"name": c.Name,
|
||||
"external_id": c.ExternalID,
|
||||
"content": c.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configList,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "list successfully as non admin",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": c.ID,
|
||||
"domain_id": c.DomainID,
|
||||
"name": c.Name,
|
||||
"external_id": c.ExternalID,
|
||||
"content": c.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configList,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "list as super admin with failed retrieve all",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
|
||||
filter: bootstrap.Filter{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
retrieveErr: nil,
|
||||
err: nil,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "list as domain admin with failed retrieve all",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
|
||||
filter: bootstrap.Filter{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
retrieveErr: nil,
|
||||
err: nil,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "list as non admin with failed retrieve all",
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
|
||||
filter: bootstrap.Filter{},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
retrieveErr: nil,
|
||||
err: nil,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
repoCall := tv.boot.On("RetrieveAll", context.Background(), mock.Anything, tc.filter, tc.offset, tc.limit).Return(tc.config, tc.retrieveErr)
|
||||
|
||||
_, err := tv.svc.List(context.Background(), tc.session, tc.filter, tc.offset, tc.limit)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.ID = unknownID
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
configID string
|
||||
userID string
|
||||
domainID string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
removeErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "remove config successfully",
|
||||
configID: config.ID,
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": config.ID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configRemove,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "remove config with failed removal",
|
||||
configID: nonExisting.ID,
|
||||
token: validToken,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
removeErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: validID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("Remove", context.Background(), mock.Anything, mock.Anything).Return(tc.removeErr)
|
||||
err := tv.svc.Remove(context.Background(), tc.session, tc.configID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
retrieveErr error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap successfully",
|
||||
externalID: config.ExternalID,
|
||||
externalKey: config.ExternalKey,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"external_id": config.ExternalID,
|
||||
"success": "1",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": clientBootstrap,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "bootstrap with an error",
|
||||
externalID: "external_id1",
|
||||
externalKey: "external_id",
|
||||
retrieveErr: bootstrap.ErrBootstrap,
|
||||
err: bootstrap.ErrBootstrap,
|
||||
event: map[string]any{
|
||||
"external_id": "external_id",
|
||||
"success": "0",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": clientBootstrap,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
repoCall := tv.boot.On("RetrieveByExternalID", context.Background(), mock.Anything).Return(config, tc.retrieveErr)
|
||||
_, err = tv.svc.Bootstrap(context.Background(), tc.externalKey, tc.externalID, false)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
test(t, tc.event, event, tc.desc)
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableConfig(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
userID string
|
||||
domainID string
|
||||
session smqauthn.Session
|
||||
retrieveErr error
|
||||
statusErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "enable config",
|
||||
id: config.ID,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": config.ID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configEnable,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "enable with failed retrieve by ID",
|
||||
id: "",
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
retrieveErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "enable with repo status error",
|
||||
id: config.ID,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
statusErr: svcerr.ErrUpdateEntity,
|
||||
err: svcerr.ErrUpdateEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
disabledConfig := config
|
||||
disabledConfig.Status = bootstrap.DisabledStatus
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: validID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("RetrieveByID", context.Background(), tc.domainID, tc.id).Return(disabledConfig, tc.retrieveErr)
|
||||
repoCall1 := tv.boot.On("ChangeStatus", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(tc.statusErr)
|
||||
_, err := tv.svc.EnableConfig(context.Background(), tc.session, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableConfig(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tv := newTestVariable(t, redisURL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
userID string
|
||||
domainID string
|
||||
session smqauthn.Session
|
||||
retrieveErr error
|
||||
statusErr error
|
||||
err error
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "disable config",
|
||||
id: config.ID,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]any{
|
||||
"config_id": config.ID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configDisable,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "disable with failed retrieve by ID",
|
||||
id: "",
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
retrieveErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
{
|
||||
desc: "disable with repo status error",
|
||||
id: config.ID,
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
statusErr: svcerr.ErrUpdateEntity,
|
||||
err: svcerr.ErrUpdateEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
tc.session = smqauthn.Session{UserID: validID, DomainID: tc.domainID, DomainUserID: validID}
|
||||
repoCall := tv.boot.On("RetrieveByID", context.Background(), tc.domainID, tc.id).Return(config, tc.retrieveErr)
|
||||
repoCall1 := tv.boot.On("ChangeStatus", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(tc.statusErr)
|
||||
_, err := tv.svc.DisableConfig(context.Background(), tc.session, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func test(t *testing.T, expected, actual map[string]any, description string) {
|
||||
if expected != nil && actual != nil {
|
||||
ts1 := expected["timestamp"].(int64)
|
||||
ats := actual["timestamp"].(string)
|
||||
ts2, err := strconv.ParseInt(strings.Split(ats, "-")[0], 10, 64)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid timestamp, got %s", description, err))
|
||||
ts1 = ts1 / 1e9
|
||||
ts2 = ts2 / 1e3
|
||||
if assert.WithinDuration(t, time.Unix(ts1, 0), time.Unix(ts2, 0), time.Second, fmt.Sprintf("%s: timestamp is not in valid range of 1 second", description)) {
|
||||
delete(expected, "timestamp")
|
||||
delete(actual, "timestamp")
|
||||
}
|
||||
|
||||
oa1 := expected["occurred_at"].(int64)
|
||||
aoa := actual["occurred_at"].(string)
|
||||
oa2, err := strconv.ParseInt(aoa, 10, 64)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid occurred_at, got %s", description, err))
|
||||
oa1 = oa1 / 1e9
|
||||
oa2 = oa2 / 1e9
|
||||
if assert.WithinDuration(t, time.Unix(oa1, 0), time.Unix(oa2, 0), time.Second, fmt.Sprintf("%s: occurred_at is not in valid range of 1 second", description)) {
|
||||
delete(expected, "occurred_at")
|
||||
delete(actual, "occurred_at")
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual, fmt.Sprintf("%s: got incorrect event\n", description))
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
// Hasher specifies an API for generating hashes of arbitrary textual content.
|
||||
type Hasher interface {
|
||||
// Hash generates the hashed string from plain-text.
|
||||
Hash(string) (string, error)
|
||||
|
||||
// Compare compares plain-text version to the hashed one. An error should
|
||||
// indicate failed comparison.
|
||||
Compare(string, string) error
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package hasher
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
cost = 10
|
||||
legacyScryptPrefix = "scrypt$"
|
||||
legacyScryptKeyN = 16384
|
||||
legacyScryptKeyR = 8
|
||||
legacyScryptKeyP = 1
|
||||
legacyScryptKeySize = 32
|
||||
)
|
||||
|
||||
var (
|
||||
errHashExternalKey = errors.NewServiceError("generate hash from external key failed")
|
||||
errCompareExternalKey = errors.NewServiceError("compare external key and hash failed")
|
||||
errInvalidHashStore = errors.New("invalid stored external key hash format")
|
||||
errDecode = errors.New("failed to decode external key hash")
|
||||
)
|
||||
|
||||
var _ bootstrap.Hasher = (*bcryptHasher)(nil)
|
||||
|
||||
type bcryptHasher struct{}
|
||||
|
||||
// New instantiates a bcrypt-based hasher implementation.
|
||||
func New() bootstrap.Hasher {
|
||||
return &bcryptHasher{}
|
||||
}
|
||||
|
||||
func (*bcryptHasher) Hash(key string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(key), cost)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errHashExternalKey, err)
|
||||
}
|
||||
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func (*bcryptHasher) Compare(plain, hashed string) error {
|
||||
if strings.HasPrefix(hashed, legacyScryptPrefix) {
|
||||
return compareLegacyScryptHash(plain, hashed)
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Legacy rows may still contain plaintext external keys.
|
||||
if subtle.ConstantTimeCompare([]byte(plain), []byte(hashed)) == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bootstrap.ErrExternalKey
|
||||
}
|
||||
|
||||
func compareLegacyScryptHash(plain, hashed string) error {
|
||||
parts := strings.Split(strings.TrimPrefix(hashed, legacyScryptPrefix), ".")
|
||||
if len(parts) != 2 {
|
||||
return errInvalidHashStore
|
||||
}
|
||||
|
||||
actualHash, err := base64.StdEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(errDecode, err)
|
||||
}
|
||||
|
||||
salt, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return errors.Wrap(errDecode, err)
|
||||
}
|
||||
|
||||
derivedHash, err := scrypt.Key([]byte(plain), salt, legacyScryptKeyN, legacyScryptKeyR, legacyScryptKeyP, legacyScryptKeySize)
|
||||
if err != nil {
|
||||
return errors.Wrap(errCompareExternalKey, err)
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(derivedHash, actualHash) == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bootstrap.ErrExternalKey
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/authz"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
)
|
||||
|
||||
const (
|
||||
createOperation = "create"
|
||||
viewOperation = "view"
|
||||
updateOperation = "update"
|
||||
updateCertOperation = "update_cert"
|
||||
listOperation = "list"
|
||||
removeOperation = "remove"
|
||||
changeStateOperation = "change_state"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc bootstrap.Service
|
||||
authz authz.Authorization
|
||||
}
|
||||
|
||||
// AuthorizationMiddleware adds authorization to the clients service.
|
||||
func AuthorizationMiddleware(svc bootstrap.Service, authz authz.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, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, createOperation, auth.AnyIDs); 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, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, viewOperation, 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, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, cfg.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateCertOperation, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
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); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID, listOperation, auth.AnyIDs); 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, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, removeOperation, 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) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, changeStateOperation, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.EnableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, changeStateOperation, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.DisableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, createOperation, auth.AnyIDs); err != nil {
|
||||
return bootstrap.Profile{}, err
|
||||
}
|
||||
return am.svc.CreateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, viewOperation, auth.AnyIDs); err != nil {
|
||||
return bootstrap.Profile{}, err
|
||||
}
|
||||
return am.svc.ViewProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, auth.AnyIDs); err != nil {
|
||||
return bootstrap.Profile{}, err
|
||||
}
|
||||
return am.svc.UpdateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, listOperation, auth.AnyIDs); err != nil {
|
||||
return bootstrap.ProfilesPage{}, err
|
||||
}
|
||||
return am.svc.ListProfiles(ctx, session, offset, limit, name)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, removeOperation, auth.AnyIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.svc.DeleteProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, configID); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.svc.AssignProfile(ctx, session, configID, profileID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, configID); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.svc.BindResources(ctx, session, token, configID, bindings)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, viewOperation, configID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return am.svc.ListBindings(ctx, session, configID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
|
||||
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, configID); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.svc.RefreshBindings(ctx, session, token, configID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session smqauthn.Session) error {
|
||||
if session.Role != smqauthn.SuperAdminRole {
|
||||
return svcerr.ErrSuperAdminAction
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, authz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: session.UserID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.MagistralaObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, session smqauthn.Session, domain, subjType, subjKind, subj, perm, objType, obj, operation, entityID string) error {
|
||||
req := authz.PolicyReq{
|
||||
Domain: domain,
|
||||
SubjectType: subjType,
|
||||
SubjectKind: subjKind,
|
||||
Subject: subj,
|
||||
Permission: perm,
|
||||
ObjectType: objType,
|
||||
Object: obj,
|
||||
}
|
||||
|
||||
var pat *authz.PATReq
|
||||
if session.PatID != "" {
|
||||
pat = &authz.PATReq{
|
||||
UserID: session.UserID,
|
||||
PatID: session.PatID,
|
||||
EntityID: entityID,
|
||||
EntityType: auth.BootstrapType.String(),
|
||||
Operation: operation,
|
||||
Domain: session.DomainID,
|
||||
}
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, req, pat); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,355 +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/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/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("config_id", saved.ID),
|
||||
}
|
||||
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("config_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("config_id", cfg.ID),
|
||||
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 config 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, id, 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("config_id", cfg.ID),
|
||||
}
|
||||
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, id, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// 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("config_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.String("error", err.Error()))
|
||||
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) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Enable config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Enable config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.EnableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Disable config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Disable config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (saved bootstrap.Profile, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("profile_id", saved.ID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Create profile failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Create profile completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (p bootstrap.Profile, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("profile_id", profileID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View profile failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View profile completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (updated bootstrap.Profile, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("profile_id", p.ID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update profile failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update profile completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (page bootstrap.ProfilesPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Uint64("offset", offset),
|
||||
slog.Uint64("limit", limit),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List profiles failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List profiles completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListProfiles(ctx, session, offset, limit, name)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("profile_id", profileID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Delete profile failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Delete profile completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DeleteProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", configID),
|
||||
slog.String("profile_id", profileID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Assign profile failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Assign profile completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AssignProfile(ctx, session, configID, profileID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", configID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Bind resources failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Bind resources completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.BindResources(ctx, session, token, configID, bindings)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) (snapshots []bootstrap.BindingSnapshot, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", configID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List bindings failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List bindings completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListBindings(ctx, session, configID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", configID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Refresh bindings failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Refresh bindings completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RefreshBindings(ctx, session, token, configID)
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/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, id, 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, id, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "enable_config").Add(1)
|
||||
mm.latency.With("method", "enable_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.EnableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disable_config").Add(1)
|
||||
mm.latency.With("method", "disable_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "create_profile").Add(1)
|
||||
mm.latency.With("method", "create_profile").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.CreateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view_profile").Add(1)
|
||||
mm.latency.With("method", "view_profile").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.ViewProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_profile").Add(1)
|
||||
mm.latency.With("method", "update_profile").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.UpdateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list_profiles").Add(1)
|
||||
mm.latency.With("method", "list_profiles").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.ListProfiles(ctx, session, offset, limit, name)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "delete_profile").Add(1)
|
||||
mm.latency.With("method", "delete_profile").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.DeleteProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "assign_profile").Add(1)
|
||||
mm.latency.With("method", "assign_profile").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.AssignProfile(ctx, session, configID, profileID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bind_resources").Add(1)
|
||||
mm.latency.With("method", "bind_resources").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.BindResources(ctx, session, token, configID, bindings)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list_bindings").Add(1)
|
||||
mm.latency.With("method", "list_bindings").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.ListBindings(ctx, session, configID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "refresh_bindings").Add(1)
|
||||
mm.latency.With("method", "refresh_bindings").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.RefreshBindings(ctx, session, token, configID)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewBindingResolver creates a new instance of BindingResolver. 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 NewBindingResolver(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *BindingResolver {
|
||||
mock := &BindingResolver{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// BindingResolver is an autogenerated mock type for the BindingResolver type
|
||||
type BindingResolver struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type BindingResolver_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *BindingResolver) EXPECT() *BindingResolver_Expecter {
|
||||
return &BindingResolver_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Resolve provides a mock function for the type BindingResolver
|
||||
func (_mock *BindingResolver) Resolve(ctx context.Context, req bootstrap.ResolveRequest) ([]bootstrap.BindingSnapshot, error) {
|
||||
ret := _mock.Called(ctx, req)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Resolve")
|
||||
}
|
||||
|
||||
var r0 []bootstrap.BindingSnapshot
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.ResolveRequest) ([]bootstrap.BindingSnapshot, error)); ok {
|
||||
return returnFunc(ctx, req)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.ResolveRequest) []bootstrap.BindingSnapshot); ok {
|
||||
r0 = returnFunc(ctx, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]bootstrap.BindingSnapshot)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.ResolveRequest) error); ok {
|
||||
r1 = returnFunc(ctx, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// BindingResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve'
|
||||
type BindingResolver_Resolve_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Resolve is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - req bootstrap.ResolveRequest
|
||||
func (_e *BindingResolver_Expecter) Resolve(ctx interface{}, req interface{}) *BindingResolver_Resolve_Call {
|
||||
return &BindingResolver_Resolve_Call{Call: _e.mock.On("Resolve", ctx, req)}
|
||||
}
|
||||
|
||||
func (_c *BindingResolver_Resolve_Call) Run(run func(ctx context.Context, req bootstrap.ResolveRequest)) *BindingResolver_Resolve_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 bootstrap.ResolveRequest
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bootstrap.ResolveRequest)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingResolver_Resolve_Call) Return(bindingSnapshots []bootstrap.BindingSnapshot, err error) *BindingResolver_Resolve_Call {
|
||||
_c.Call.Return(bindingSnapshots, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingResolver_Resolve_Call) RunAndReturn(run func(ctx context.Context, req bootstrap.ResolveRequest) ([]bootstrap.BindingSnapshot, error)) *BindingResolver_Resolve_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewBindingStore creates a new instance of BindingStore. 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 NewBindingStore(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *BindingStore {
|
||||
mock := &BindingStore{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// BindingStore is an autogenerated mock type for the BindingStore type
|
||||
type BindingStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type BindingStore_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *BindingStore) EXPECT() *BindingStore_Expecter {
|
||||
return &BindingStore_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Delete provides a mock function for the type BindingStore
|
||||
func (_mock *BindingStore) Delete(ctx context.Context, configID string, slot string) error {
|
||||
ret := _mock.Called(ctx, configID, slot)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, configID, slot)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// BindingStore_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type BindingStore_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - configID string
|
||||
// - slot string
|
||||
func (_e *BindingStore_Expecter) Delete(ctx interface{}, configID interface{}, slot interface{}) *BindingStore_Delete_Call {
|
||||
return &BindingStore_Delete_Call{Call: _e.mock.On("Delete", ctx, configID, slot)}
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Delete_Call) Run(run func(ctx context.Context, configID string, slot string)) *BindingStore_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Delete_Call) Return(err error) *BindingStore_Delete_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Delete_Call) RunAndReturn(run func(ctx context.Context, configID string, slot string) error) *BindingStore_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Retrieve provides a mock function for the type BindingStore
|
||||
func (_mock *BindingStore) Retrieve(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error) {
|
||||
ret := _mock.Called(ctx, configID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Retrieve")
|
||||
}
|
||||
|
||||
var r0 []bootstrap.BindingSnapshot
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]bootstrap.BindingSnapshot, error)); ok {
|
||||
return returnFunc(ctx, configID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []bootstrap.BindingSnapshot); ok {
|
||||
r0 = returnFunc(ctx, configID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]bootstrap.BindingSnapshot)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, configID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// BindingStore_Retrieve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Retrieve'
|
||||
type BindingStore_Retrieve_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Retrieve is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - configID string
|
||||
func (_e *BindingStore_Expecter) Retrieve(ctx interface{}, configID interface{}) *BindingStore_Retrieve_Call {
|
||||
return &BindingStore_Retrieve_Call{Call: _e.mock.On("Retrieve", ctx, configID)}
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Retrieve_Call) Run(run func(ctx context.Context, configID string)) *BindingStore_Retrieve_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Retrieve_Call) Return(bindingSnapshots []bootstrap.BindingSnapshot, err error) *BindingStore_Retrieve_Call {
|
||||
_c.Call.Return(bindingSnapshots, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Retrieve_Call) RunAndReturn(run func(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error)) *BindingStore_Retrieve_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Save provides a mock function for the type BindingStore
|
||||
func (_mock *BindingStore) Save(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error {
|
||||
ret := _mock.Called(ctx, configID, bindings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []bootstrap.BindingSnapshot) error); ok {
|
||||
r0 = returnFunc(ctx, configID, bindings)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// BindingStore_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
|
||||
type BindingStore_Save_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Save is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - configID string
|
||||
// - bindings []bootstrap.BindingSnapshot
|
||||
func (_e *BindingStore_Expecter) Save(ctx interface{}, configID interface{}, bindings interface{}) *BindingStore_Save_Call {
|
||||
return &BindingStore_Save_Call{Call: _e.mock.On("Save", ctx, configID, bindings)}
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Save_Call) Run(run func(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot)) *BindingStore_Save_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 []bootstrap.BindingSnapshot
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].([]bootstrap.BindingSnapshot)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Save_Call) Return(err error) *BindingStore_Save_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BindingStore_Save_Call) RunAndReturn(run func(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error) *BindingStore_Save_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ConfigReader is an autogenerated mock type for the ConfigReader type
|
||||
type ConfigReader struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type ConfigReader_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *ConfigReader) EXPECT() *ConfigReader_Expecter {
|
||||
return &ConfigReader_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// ReadConfig provides a mock function for the type ConfigReader
|
||||
func (_mock *ConfigReader) ReadConfig(config bootstrap.Config, b bool) (any, error) {
|
||||
ret := _mock.Called(config, b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ReadConfig")
|
||||
}
|
||||
|
||||
var r0 any
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) (any, error)); ok {
|
||||
return returnFunc(config, b)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) any); ok {
|
||||
r0 = returnFunc(config, b)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(any)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(bootstrap.Config, bool) error); ok {
|
||||
r1 = returnFunc(config, b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigReader_ReadConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadConfig'
|
||||
type ConfigReader_ReadConfig_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ReadConfig is a helper method to define mock.On call
|
||||
// - config bootstrap.Config
|
||||
// - b bool
|
||||
func (_e *ConfigReader_Expecter) ReadConfig(config interface{}, b interface{}) *ConfigReader_ReadConfig_Call {
|
||||
return &ConfigReader_ReadConfig_Call{Call: _e.mock.On("ReadConfig", config, b)}
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Run(run func(config bootstrap.Config, b bool)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 bootstrap.Config
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(bootstrap.Config)
|
||||
}
|
||||
var arg1 bool
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bool)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Return(v any, err error) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(v, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) RunAndReturn(run func(config bootstrap.Config, b bool) (any, error)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1,670 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ConfigRepository is an autogenerated mock type for the ConfigRepository type
|
||||
type ConfigRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type ConfigRepository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *ConfigRepository) EXPECT() *ConfigRepository_Expecter {
|
||||
return &ConfigRepository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// AssignProfile provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) AssignProfile(ctx context.Context, domainID string, id string, profileID string) error {
|
||||
ret := _mock.Called(ctx, domainID, id, profileID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AssignProfile")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, domainID, id, profileID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigRepository_AssignProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AssignProfile'
|
||||
type ConfigRepository_AssignProfile_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AssignProfile is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
// - profileID string
|
||||
func (_e *ConfigRepository_Expecter) AssignProfile(ctx interface{}, domainID interface{}, id interface{}, profileID interface{}) *ConfigRepository_AssignProfile_Call {
|
||||
return &ConfigRepository_AssignProfile_Call{Call: _e.mock.On("AssignProfile", ctx, domainID, id, profileID)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_AssignProfile_Call) Run(run func(ctx context.Context, domainID string, id string, profileID string)) *ConfigRepository_AssignProfile_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
var arg3 string
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_AssignProfile_Call) Return(err error) *ConfigRepository_AssignProfile_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_AssignProfile_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, profileID string) error) *ConfigRepository_AssignProfile_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ChangeStatus provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) ChangeStatus(ctx context.Context, domainID string, id string, status bootstrap.Status) error {
|
||||
ret := _mock.Called(ctx, domainID, id, status)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ChangeStatus")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, bootstrap.Status) error); ok {
|
||||
r0 = returnFunc(ctx, domainID, id, status)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigRepository_ChangeStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeStatus'
|
||||
type ConfigRepository_ChangeStatus_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ChangeStatus is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
// - status bootstrap.Status
|
||||
func (_e *ConfigRepository_Expecter) ChangeStatus(ctx interface{}, domainID interface{}, id interface{}, status interface{}) *ConfigRepository_ChangeStatus_Call {
|
||||
return &ConfigRepository_ChangeStatus_Call{Call: _e.mock.On("ChangeStatus", ctx, domainID, id, status)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_ChangeStatus_Call) Run(run func(ctx context.Context, domainID string, id string, status bootstrap.Status)) *ConfigRepository_ChangeStatus_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
var arg3 bootstrap.Status
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(bootstrap.Status)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_ChangeStatus_Call) Return(err error) *ConfigRepository_ChangeStatus_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_ChangeStatus_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, status bootstrap.Status) error) *ConfigRepository_ChangeStatus_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Remove provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) Remove(ctx context.Context, domainID string, id string) error {
|
||||
ret := _mock.Called(ctx, domainID, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, domainID, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
|
||||
type ConfigRepository_Remove_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Remove is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
func (_e *ConfigRepository_Expecter) Remove(ctx interface{}, domainID interface{}, id interface{}) *ConfigRepository_Remove_Call {
|
||||
return &ConfigRepository_Remove_Call{Call: _e.mock.On("Remove", ctx, domainID, id)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Remove_Call) Run(run func(ctx context.Context, domainID string, id string)) *ConfigRepository_Remove_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Remove_Call) Return(err error) *ConfigRepository_Remove_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Remove_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string) error) *ConfigRepository_Remove_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveAll provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) RetrieveAll(ctx context.Context, domainID string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage {
|
||||
ret := _mock.Called(ctx, domainID, filter, offset, limit)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveAll")
|
||||
}
|
||||
|
||||
var r0 bootstrap.ConfigsPage
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
|
||||
r0 = returnFunc(ctx, domainID, filter, offset, limit)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.ConfigsPage)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigRepository_RetrieveAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveAll'
|
||||
type ConfigRepository_RetrieveAll_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveAll is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - filter bootstrap.Filter
|
||||
// - offset uint64
|
||||
// - limit uint64
|
||||
func (_e *ConfigRepository_Expecter) RetrieveAll(ctx interface{}, domainID interface{}, filter interface{}, offset interface{}, limit interface{}) *ConfigRepository_RetrieveAll_Call {
|
||||
return &ConfigRepository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, domainID, filter, offset, limit)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveAll_Call) Run(run func(ctx context.Context, domainID string, filter bootstrap.Filter, offset uint64, limit uint64)) *ConfigRepository_RetrieveAll_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 bootstrap.Filter
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(bootstrap.Filter)
|
||||
}
|
||||
var arg3 uint64
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(uint64)
|
||||
}
|
||||
var arg4 uint64
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(uint64)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveAll_Call) Return(configsPage bootstrap.ConfigsPage) *ConfigRepository_RetrieveAll_Call {
|
||||
_c.Call.Return(configsPage)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, domainID string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage) *ConfigRepository_RetrieveAll_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveByExternalID provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
|
||||
ret := _mock.Called(ctx, externalID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByExternalID")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bootstrap.Config, error)); ok {
|
||||
return returnFunc(ctx, externalID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) bootstrap.Config); ok {
|
||||
r0 = returnFunc(ctx, externalID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, externalID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigRepository_RetrieveByExternalID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByExternalID'
|
||||
type ConfigRepository_RetrieveByExternalID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveByExternalID is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - externalID string
|
||||
func (_e *ConfigRepository_Expecter) RetrieveByExternalID(ctx interface{}, externalID interface{}) *ConfigRepository_RetrieveByExternalID_Call {
|
||||
return &ConfigRepository_RetrieveByExternalID_Call{Call: _e.mock.On("RetrieveByExternalID", ctx, externalID)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveByExternalID_Call) Run(run func(ctx context.Context, externalID string)) *ConfigRepository_RetrieveByExternalID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveByExternalID_Call) Return(config bootstrap.Config, err error) *ConfigRepository_RetrieveByExternalID_Call {
|
||||
_c.Call.Return(config, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveByExternalID_Call) RunAndReturn(run func(ctx context.Context, externalID string) (bootstrap.Config, error)) *ConfigRepository_RetrieveByExternalID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveByID provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) RetrieveByID(ctx context.Context, domainID string, id string) (bootstrap.Config, error) {
|
||||
ret := _mock.Called(ctx, domainID, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByID")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (bootstrap.Config, error)); ok {
|
||||
return returnFunc(ctx, domainID, id)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) bootstrap.Config); ok {
|
||||
r0 = returnFunc(ctx, domainID, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, domainID, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigRepository_RetrieveByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByID'
|
||||
type ConfigRepository_RetrieveByID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveByID is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
func (_e *ConfigRepository_Expecter) RetrieveByID(ctx interface{}, domainID interface{}, id interface{}) *ConfigRepository_RetrieveByID_Call {
|
||||
return &ConfigRepository_RetrieveByID_Call{Call: _e.mock.On("RetrieveByID", ctx, domainID, id)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveByID_Call) Run(run func(ctx context.Context, domainID string, id string)) *ConfigRepository_RetrieveByID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveByID_Call) Return(config bootstrap.Config, err error) *ConfigRepository_RetrieveByID_Call {
|
||||
_c.Call.Return(config, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_RetrieveByID_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string) (bootstrap.Config, error)) *ConfigRepository_RetrieveByID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Save provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) Save(ctx context.Context, cfg bootstrap.Config) (string, error) {
|
||||
ret := _mock.Called(ctx, cfg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config) (string, error)); ok {
|
||||
return returnFunc(ctx, cfg)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config) string); ok {
|
||||
r0 = returnFunc(ctx, cfg)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Config) error); ok {
|
||||
r1 = returnFunc(ctx, cfg)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigRepository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
|
||||
type ConfigRepository_Save_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Save is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cfg bootstrap.Config
|
||||
func (_e *ConfigRepository_Expecter) Save(ctx interface{}, cfg interface{}) *ConfigRepository_Save_Call {
|
||||
return &ConfigRepository_Save_Call{Call: _e.mock.On("Save", ctx, cfg)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Save_Call) Run(run func(ctx context.Context, cfg bootstrap.Config)) *ConfigRepository_Save_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 bootstrap.Config
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bootstrap.Config)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Save_Call) Return(s string, err error) *ConfigRepository_Save_Call {
|
||||
_c.Call.Return(s, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Save_Call) RunAndReturn(run func(ctx context.Context, cfg bootstrap.Config) (string, error)) *ConfigRepository_Save_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Update provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
|
||||
ret := _mock.Called(ctx, cfg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config) error); ok {
|
||||
r0 = returnFunc(ctx, cfg)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
|
||||
type ConfigRepository_Update_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Update is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cfg bootstrap.Config
|
||||
func (_e *ConfigRepository_Expecter) Update(ctx interface{}, cfg interface{}) *ConfigRepository_Update_Call {
|
||||
return &ConfigRepository_Update_Call{Call: _e.mock.On("Update", ctx, cfg)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Update_Call) Run(run func(ctx context.Context, cfg bootstrap.Config)) *ConfigRepository_Update_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 bootstrap.Config
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bootstrap.Config)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Update_Call) Return(err error) *ConfigRepository_Update_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_Update_Call) RunAndReturn(run func(ctx context.Context, cfg bootstrap.Config) error) *ConfigRepository_Update_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateCert provides a mock function for the type ConfigRepository
|
||||
func (_mock *ConfigRepository) UpdateCert(ctx context.Context, domainID string, id string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
|
||||
ret := _mock.Called(ctx, domainID, id, clientCert, clientKey, caCert)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateCert")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) (bootstrap.Config, error)); ok {
|
||||
return returnFunc(ctx, domainID, id, clientCert, clientKey, caCert)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) bootstrap.Config); ok {
|
||||
r0 = returnFunc(ctx, domainID, id, clientCert, clientKey, caCert)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, domainID, id, clientCert, clientKey, caCert)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigRepository_UpdateCert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCert'
|
||||
type ConfigRepository_UpdateCert_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateCert is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
// - clientCert string
|
||||
// - clientKey string
|
||||
// - caCert string
|
||||
func (_e *ConfigRepository_Expecter) UpdateCert(ctx interface{}, domainID interface{}, id interface{}, clientCert interface{}, clientKey interface{}, caCert interface{}) *ConfigRepository_UpdateCert_Call {
|
||||
return &ConfigRepository_UpdateCert_Call{Call: _e.mock.On("UpdateCert", ctx, domainID, id, clientCert, clientKey, caCert)}
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_UpdateCert_Call) Run(run func(ctx context.Context, domainID string, id string, clientCert string, clientKey string, caCert string)) *ConfigRepository_UpdateCert_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
var arg3 string
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(string)
|
||||
}
|
||||
var arg4 string
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(string)
|
||||
}
|
||||
var arg5 string
|
||||
if args[5] != nil {
|
||||
arg5 = args[5].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
arg5,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_UpdateCert_Call) Return(config bootstrap.Config, err error) *ConfigRepository_UpdateCert_Call {
|
||||
_c.Call.Return(config, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigRepository_UpdateCert_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error)) *ConfigRepository_UpdateCert_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewProfileRepository creates a new instance of ProfileRepository. 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 NewProfileRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ProfileRepository {
|
||||
mock := &ProfileRepository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// ProfileRepository is an autogenerated mock type for the ProfileRepository type
|
||||
type ProfileRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type ProfileRepository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *ProfileRepository) EXPECT() *ProfileRepository_Expecter {
|
||||
return &ProfileRepository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Delete provides a mock function for the type ProfileRepository
|
||||
func (_mock *ProfileRepository) Delete(ctx context.Context, domainID string, id string) error {
|
||||
ret := _mock.Called(ctx, domainID, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, domainID, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ProfileRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type ProfileRepository_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
func (_e *ProfileRepository_Expecter) Delete(ctx interface{}, domainID interface{}, id interface{}) *ProfileRepository_Delete_Call {
|
||||
return &ProfileRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, domainID, id)}
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Delete_Call) Run(run func(ctx context.Context, domainID string, id string)) *ProfileRepository_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Delete_Call) Return(err error) *ProfileRepository_Delete_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Delete_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string) error) *ProfileRepository_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveAll provides a mock function for the type ProfileRepository
|
||||
func (_mock *ProfileRepository) RetrieveAll(ctx context.Context, domainID string, offset uint64, limit uint64, name string) (bootstrap.ProfilesPage, error) {
|
||||
ret := _mock.Called(ctx, domainID, offset, limit, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveAll")
|
||||
}
|
||||
|
||||
var r0 bootstrap.ProfilesPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64, string) (bootstrap.ProfilesPage, error)); ok {
|
||||
return returnFunc(ctx, domainID, offset, limit, name)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64, string) bootstrap.ProfilesPage); ok {
|
||||
r0 = returnFunc(ctx, domainID, offset, limit, name)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.ProfilesPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, uint64, uint64, string) error); ok {
|
||||
r1 = returnFunc(ctx, domainID, offset, limit, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProfileRepository_RetrieveAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveAll'
|
||||
type ProfileRepository_RetrieveAll_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveAll is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - offset uint64
|
||||
// - limit uint64
|
||||
// - name string
|
||||
func (_e *ProfileRepository_Expecter) RetrieveAll(ctx interface{}, domainID interface{}, offset interface{}, limit interface{}, name interface{}) *ProfileRepository_RetrieveAll_Call {
|
||||
return &ProfileRepository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, domainID, offset, limit, name)}
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_RetrieveAll_Call) Run(run func(ctx context.Context, domainID string, offset uint64, limit uint64, name string)) *ProfileRepository_RetrieveAll_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 uint64
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(uint64)
|
||||
}
|
||||
var arg3 uint64
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(uint64)
|
||||
}
|
||||
var arg4 string
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_RetrieveAll_Call) Return(profilesPage bootstrap.ProfilesPage, err error) *ProfileRepository_RetrieveAll_Call {
|
||||
_c.Call.Return(profilesPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, domainID string, offset uint64, limit uint64, name string) (bootstrap.ProfilesPage, error)) *ProfileRepository_RetrieveAll_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveByID provides a mock function for the type ProfileRepository
|
||||
func (_mock *ProfileRepository) RetrieveByID(ctx context.Context, domainID string, id string) (bootstrap.Profile, error) {
|
||||
ret := _mock.Called(ctx, domainID, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByID")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Profile
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (bootstrap.Profile, error)); ok {
|
||||
return returnFunc(ctx, domainID, id)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) bootstrap.Profile); ok {
|
||||
r0 = returnFunc(ctx, domainID, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Profile)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, domainID, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProfileRepository_RetrieveByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByID'
|
||||
type ProfileRepository_RetrieveByID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveByID is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - domainID string
|
||||
// - id string
|
||||
func (_e *ProfileRepository_Expecter) RetrieveByID(ctx interface{}, domainID interface{}, id interface{}) *ProfileRepository_RetrieveByID_Call {
|
||||
return &ProfileRepository_RetrieveByID_Call{Call: _e.mock.On("RetrieveByID", ctx, domainID, id)}
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_RetrieveByID_Call) Run(run func(ctx context.Context, domainID string, id string)) *ProfileRepository_RetrieveByID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_RetrieveByID_Call) Return(profile bootstrap.Profile, err error) *ProfileRepository_RetrieveByID_Call {
|
||||
_c.Call.Return(profile, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_RetrieveByID_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string) (bootstrap.Profile, error)) *ProfileRepository_RetrieveByID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Save provides a mock function for the type ProfileRepository
|
||||
func (_mock *ProfileRepository) Save(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
ret := _mock.Called(ctx, p)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Profile
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) (bootstrap.Profile, error)); ok {
|
||||
return returnFunc(ctx, p)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) bootstrap.Profile); ok {
|
||||
r0 = returnFunc(ctx, p)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Profile)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Profile) error); ok {
|
||||
r1 = returnFunc(ctx, p)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProfileRepository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
|
||||
type ProfileRepository_Save_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Save is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - p bootstrap.Profile
|
||||
func (_e *ProfileRepository_Expecter) Save(ctx interface{}, p interface{}) *ProfileRepository_Save_Call {
|
||||
return &ProfileRepository_Save_Call{Call: _e.mock.On("Save", ctx, p)}
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Save_Call) Run(run func(ctx context.Context, p bootstrap.Profile)) *ProfileRepository_Save_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 bootstrap.Profile
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bootstrap.Profile)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Save_Call) Return(profile bootstrap.Profile, err error) *ProfileRepository_Save_Call {
|
||||
_c.Call.Return(profile, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Save_Call) RunAndReturn(run func(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error)) *ProfileRepository_Save_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Update provides a mock function for the type ProfileRepository
|
||||
func (_mock *ProfileRepository) Update(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
ret := _mock.Called(ctx, p)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Profile
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) (bootstrap.Profile, error)); ok {
|
||||
return returnFunc(ctx, p)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) bootstrap.Profile); ok {
|
||||
r0 = returnFunc(ctx, p)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Profile)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Profile) error); ok {
|
||||
r1 = returnFunc(ctx, p)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProfileRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
|
||||
type ProfileRepository_Update_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Update is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - p bootstrap.Profile
|
||||
func (_e *ProfileRepository_Expecter) Update(ctx interface{}, p interface{}) *ProfileRepository_Update_Call {
|
||||
return &ProfileRepository_Update_Call{Call: _e.mock.On("Update", ctx, p)}
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Update_Call) Run(run func(ctx context.Context, p bootstrap.Profile)) *ProfileRepository_Update_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 bootstrap.Profile
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bootstrap.Profile)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Update_Call) Return(profile bootstrap.Profile, err error) *ProfileRepository_Update_Call {
|
||||
_c.Call.Return(profile, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ProfileRepository_Update_Call) RunAndReturn(run func(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error)) *ProfileRepository_Update_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewRenderer creates a new instance of Renderer. 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 NewRenderer(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Renderer {
|
||||
mock := &Renderer{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Renderer is an autogenerated mock type for the Renderer type
|
||||
type Renderer struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Renderer_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Renderer) EXPECT() *Renderer_Expecter {
|
||||
return &Renderer_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Render provides a mock function for the type Renderer
|
||||
func (_mock *Renderer) Render(profile bootstrap.Profile, enrollment bootstrap.Config, bindings []bootstrap.BindingSnapshot) ([]byte, error) {
|
||||
ret := _mock.Called(profile, enrollment, bindings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Render")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Profile, bootstrap.Config, []bootstrap.BindingSnapshot) ([]byte, error)); ok {
|
||||
return returnFunc(profile, enrollment, bindings)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Profile, bootstrap.Config, []bootstrap.BindingSnapshot) []byte); ok {
|
||||
r0 = returnFunc(profile, enrollment, bindings)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(bootstrap.Profile, bootstrap.Config, []bootstrap.BindingSnapshot) error); ok {
|
||||
r1 = returnFunc(profile, enrollment, bindings)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Renderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render'
|
||||
type Renderer_Render_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Render is a helper method to define mock.On call
|
||||
// - profile bootstrap.Profile
|
||||
// - enrollment bootstrap.Config
|
||||
// - bindings []bootstrap.BindingSnapshot
|
||||
func (_e *Renderer_Expecter) Render(profile interface{}, enrollment interface{}, bindings interface{}) *Renderer_Render_Call {
|
||||
return &Renderer_Render_Call{Call: _e.mock.On("Render", profile, enrollment, bindings)}
|
||||
}
|
||||
|
||||
func (_c *Renderer_Render_Call) Run(run func(profile bootstrap.Profile, enrollment bootstrap.Config, bindings []bootstrap.BindingSnapshot)) *Renderer_Render_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 bootstrap.Profile
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(bootstrap.Profile)
|
||||
}
|
||||
var arg1 bootstrap.Config
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bootstrap.Config)
|
||||
}
|
||||
var arg2 []bootstrap.BindingSnapshot
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].([]bootstrap.BindingSnapshot)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Renderer_Render_Call) Return(bytes []byte, err error) *Renderer_Render_Call {
|
||||
_c.Call.Return(bytes, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Renderer_Render_Call) RunAndReturn(run func(profile bootstrap.Profile, enrollment bootstrap.Config, bindings []bootstrap.BindingSnapshot) ([]byte, error)) *Renderer_Render_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/absmach/magistrala/pkg/postgres"
|
||||
)
|
||||
|
||||
var _ bootstrap.BindingStore = (*bindingRepository)(nil)
|
||||
|
||||
type bindingRepository struct {
|
||||
db postgres.Database
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewBindingRepository instantiates a PostgreSQL implementation of BindingStore.
|
||||
func NewBindingRepository(db postgres.Database, log *slog.Logger) bootstrap.BindingStore {
|
||||
return &bindingRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (br bindingRepository) Save(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error {
|
||||
if len(bindings) == 0 {
|
||||
return nil
|
||||
}
|
||||
q := `INSERT INTO bindings (config_id, slot, type, resource_id, snapshot, secret_snapshot, updated_at)
|
||||
VALUES (:config_id, :slot, :type, :resource_id, :snapshot, :secret_snapshot, :updated_at)
|
||||
ON CONFLICT (config_id, slot) DO UPDATE SET
|
||||
type = EXCLUDED.type,
|
||||
resource_id = EXCLUDED.resource_id,
|
||||
snapshot = EXCLUDED.snapshot,
|
||||
secret_snapshot = EXCLUDED.secret_snapshot,
|
||||
updated_at = EXCLUDED.updated_at`
|
||||
|
||||
now := time.Now().UTC()
|
||||
dbBindings := make([]dbBindingSnapshot, 0, len(bindings))
|
||||
for _, b := range bindings {
|
||||
b.ConfigID = configID
|
||||
b.UpdatedAt = now
|
||||
dbb, err := toDBBindingSnapshot(b)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
dbBindings = append(dbBindings, dbb)
|
||||
}
|
||||
|
||||
if _, err := br.db.NamedExecContext(ctx, q, dbBindings); err != nil {
|
||||
return errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (br bindingRepository) Retrieve(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error) {
|
||||
q := `SELECT config_id, slot, type, resource_id, snapshot, secret_snapshot, updated_at
|
||||
FROM bindings WHERE config_id = $1 ORDER BY slot`
|
||||
|
||||
rows, err := br.db.QueryxContext(ctx, q, configID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var snapshots []bootstrap.BindingSnapshot
|
||||
for rows.Next() {
|
||||
var dbb dbBindingSnapshot
|
||||
if err := rows.StructScan(&dbb); err != nil {
|
||||
br.log.Error(fmt.Sprintf("failed to scan binding snapshot: %s", err))
|
||||
return nil, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
b, err := toBindingSnapshot(dbb)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
snapshots = append(snapshots, b)
|
||||
}
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
func (br bindingRepository) Delete(ctx context.Context, configID, slot string) error {
|
||||
q := `DELETE FROM bindings WHERE config_id = $1 AND slot = $2`
|
||||
if _, err := br.db.ExecContext(ctx, q, configID, slot); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dbBindingSnapshot is the database representation of a BindingSnapshot.
|
||||
type dbBindingSnapshot struct {
|
||||
ConfigID string `db:"config_id"`
|
||||
Slot string `db:"slot"`
|
||||
Type string `db:"type"`
|
||||
ResourceID string `db:"resource_id"`
|
||||
Snapshot []byte `db:"snapshot"`
|
||||
SecretSnapshot []byte `db:"secret_snapshot"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
func toDBBindingSnapshot(b bootstrap.BindingSnapshot) (dbBindingSnapshot, error) {
|
||||
snap, err := json.Marshal(b.Snapshot)
|
||||
if err != nil {
|
||||
return dbBindingSnapshot{}, err
|
||||
}
|
||||
secret, err := json.Marshal(b.SecretSnapshot)
|
||||
if err != nil {
|
||||
return dbBindingSnapshot{}, err
|
||||
}
|
||||
return dbBindingSnapshot{
|
||||
ConfigID: b.ConfigID,
|
||||
Slot: b.Slot,
|
||||
Type: b.Type,
|
||||
ResourceID: b.ResourceID,
|
||||
Snapshot: snap,
|
||||
SecretSnapshot: secret,
|
||||
UpdatedAt: b.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toBindingSnapshot(dbb dbBindingSnapshot) (bootstrap.BindingSnapshot, error) {
|
||||
b := bootstrap.BindingSnapshot{
|
||||
ConfigID: dbb.ConfigID,
|
||||
Slot: dbb.Slot,
|
||||
Type: dbb.Type,
|
||||
ResourceID: dbb.ResourceID,
|
||||
UpdatedAt: dbb.UpdatedAt,
|
||||
}
|
||||
if len(dbb.Snapshot) > 0 && string(dbb.Snapshot) != jsonNull {
|
||||
if err := json.Unmarshal(dbb.Snapshot, &b.Snapshot); err != nil {
|
||||
return bootstrap.BindingSnapshot{}, err
|
||||
}
|
||||
}
|
||||
if len(dbb.SecretSnapshot) > 0 && string(dbb.SecretSnapshot) != jsonNull {
|
||||
if err := json.Unmarshal(dbb.SecretSnapshot, &b.SecretSnapshot); err != nil {
|
||||
return bootstrap.BindingSnapshot{}, err
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/absmach/magistrala/pkg/postgres"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
const jsonNull = "null"
|
||||
|
||||
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) (string, error) {
|
||||
q := `INSERT INTO configs (id, domain_id, name, client_cert, client_key, ca_cert, external_id, external_key, content, status, profile_id, render_context)
|
||||
VALUES (:id, :domain_id, :name, :client_cert, :client_key, :ca_cert, :external_id, :external_key, :content, :status, :profile_id, :render_context)`
|
||||
|
||||
dbcfg, err := toDBConfig(cfg)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
|
||||
switch pgErr := err.(type) {
|
||||
case *pgconn.PgError:
|
||||
if pgErr.Code == pgerrcode.UniqueViolation {
|
||||
return "", repoerr.ErrConflict
|
||||
}
|
||||
}
|
||||
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
return cfg.ID, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT id, external_id, name, content, status, client_cert, client_key, ca_cert, profile_id, render_context
|
||||
FROM configs
|
||||
WHERE id = :id AND domain_id = :domain_id`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return bootstrap.Config{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
cfg, err := toConfig(dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := buildRetrieveQueryParams(domainID, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT id, external_id, name, content, status, profile_id, render_context
|
||||
FROM configs %s ORDER BY id 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, profileID sql.NullString
|
||||
var renderContext []byte
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{DomainID: domainID}
|
||||
if err := rows.Scan(&c.ID, &c.ExternalID, &name, &content, &c.Status, &profileID, &renderContext); 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
|
||||
if profileID.Valid {
|
||||
c.ProfileID = profileID.String
|
||||
}
|
||||
if len(renderContext) > 0 && string(renderContext) != jsonNull {
|
||||
if err := json.Unmarshal(renderContext, &c.RenderContext); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to decode render context due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
}
|
||||
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 id, external_key, domain_id, name, client_cert, client_key, ca_cert, content, status, profile_id, render_context
|
||||
FROM configs
|
||||
WHERE external_id = :external_id`
|
||||
dbcfg := dbConfig{
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return bootstrap.Config{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
cfg, err := toConfig(dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET name = :name, content = :content, render_context = :render_context WHERE id = :id AND domain_id = :domain_id `
|
||||
|
||||
renderContext, err := json.Marshal(cfg.RenderContext)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
dbcfg := dbConfig{
|
||||
Name: nullString(cfg.Name),
|
||||
Content: nullString(cfg.Content),
|
||||
RenderContext: renderContext,
|
||||
ID: cfg.ID,
|
||||
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) AssignProfile(ctx context.Context, domainID, id, profileID string) error {
|
||||
q := `UPDATE configs SET profile_id = :profile_id WHERE id = :id AND domain_id = :domain_id`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ID: id,
|
||||
DomainID: domainID,
|
||||
ProfileID: nullString(profileID),
|
||||
}
|
||||
|
||||
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, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE id = :id AND domain_id = :domain_id
|
||||
RETURNING id, client_cert, client_key, ca_cert, domain_id`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ID: id,
|
||||
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
|
||||
}
|
||||
|
||||
cfg, err := toConfig(dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
|
||||
q := `DELETE FROM configs WHERE id = :id AND domain_id = :domain_id`
|
||||
dbcfg := dbConfig{
|
||||
ID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeStatus(ctx context.Context, domainID, id string, status bootstrap.Status) error {
|
||||
q := `UPDATE configs SET status = :status WHERE id = :id AND domain_id = :domain_id;`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ID: id,
|
||||
Status: status,
|
||||
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 buildRetrieveQueryParams(domainID string, filter bootstrap.Filter) (string, []any) {
|
||||
params := []any{}
|
||||
queries := []string{}
|
||||
|
||||
if domainID != "" {
|
||||
params = append(params, domainID)
|
||||
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
|
||||
}
|
||||
|
||||
counter := len(params) + 1
|
||||
for k, v := range filter.FullMatch {
|
||||
if k == "status" {
|
||||
status, err := bootstrap.ToStatus(v)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
if status == bootstrap.AllStatus {
|
||||
continue
|
||||
}
|
||||
params = append(params, status)
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
counter++
|
||||
continue
|
||||
}
|
||||
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 nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
return sql.NullString{String: s, Valid: true}
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
DomainID string `db:"domain_id"`
|
||||
ID string `db:"id"`
|
||||
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"`
|
||||
Status bootstrap.Status `db:"status"`
|
||||
ProfileID sql.NullString `db:"profile_id"`
|
||||
RenderContext []byte `db:"render_context"`
|
||||
}
|
||||
|
||||
func toDBConfig(cfg bootstrap.Config) (dbConfig, error) {
|
||||
renderContext, err := json.Marshal(cfg.RenderContext)
|
||||
if err != nil {
|
||||
return dbConfig{}, err
|
||||
}
|
||||
|
||||
return dbConfig{
|
||||
ID: cfg.ID,
|
||||
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),
|
||||
Status: cfg.Status,
|
||||
ProfileID: nullString(cfg.ProfileID),
|
||||
RenderContext: renderContext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toConfig(dbcfg dbConfig) (bootstrap.Config, error) {
|
||||
cfg := bootstrap.Config{
|
||||
ID: dbcfg.ID,
|
||||
DomainID: dbcfg.DomainID,
|
||||
ExternalID: dbcfg.ExternalID,
|
||||
ExternalKey: dbcfg.ExternalKey,
|
||||
Status: dbcfg.Status,
|
||||
}
|
||||
if dbcfg.ProfileID.Valid {
|
||||
cfg.ProfileID = dbcfg.ProfileID.String
|
||||
}
|
||||
|
||||
if dbcfg.Name.Valid {
|
||||
cfg.Name = dbcfg.Name.String
|
||||
}
|
||||
if dbcfg.Content.Valid {
|
||||
cfg.Content = dbcfg.Content.String
|
||||
}
|
||||
if len(dbcfg.RenderContext) > 0 && string(dbcfg.RenderContext) != jsonNull {
|
||||
if err := json.Unmarshal(dbcfg.RenderContext, &cfg.RenderContext); err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
}
|
||||
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, nil
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/bootstrap/postgres"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/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{
|
||||
ID: "mg-client",
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
DomainID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Content: "content",
|
||||
Status: bootstrap.Inactive,
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
diff := "different"
|
||||
|
||||
duplicateClient := config
|
||||
duplicateClient.ExternalID = diff
|
||||
|
||||
duplicateExternal := config
|
||||
duplicateExternal.ID = diff
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a config",
|
||||
config: config,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Client ID",
|
||||
config: duplicateClient,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
id, err := repo.Save(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))
|
||||
if err == nil {
|
||||
assert.Equal(t, id, tc.config.ID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ID, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c)
|
||||
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)
|
||||
|
||||
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.ID = uid.String()
|
||||
|
||||
if i%2 == 0 {
|
||||
c.Status = bootstrap.Active
|
||||
}
|
||||
|
||||
_, err = repo.Save(context.Background(), c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
filter bootstrap.Filter
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all configs",
|
||||
domainID: config.DomainID,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset of configs",
|
||||
domainID: config.DomainID,
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with wrong domain ID ",
|
||||
domainID: "2",
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all active configs ",
|
||||
domainID: config.DomainID,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"status": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all with partial match filter",
|
||||
domainID: config.DomainID,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
domainID: config.DomainID,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(context.Background(), tc.domainID, 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)
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c)
|
||||
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)
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
withRenderContext := c
|
||||
withRenderContext.RenderContext = map[string]any{
|
||||
"site": "warehouse-2",
|
||||
"region": "mombasa",
|
||||
}
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
renderContext map[string]any
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domainID",
|
||||
config: wrongDomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
config: c,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a config render_context",
|
||||
config: withRenderContext,
|
||||
renderContext: map[string]any{"site": "warehouse-2", "region": "mombasa"},
|
||||
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))
|
||||
if tc.err == nil && tc.renderContext != nil {
|
||||
saved, err := repo.RetrieveByID(context.Background(), tc.config.DomainID, tc.config.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: unexpected retrieve error: %s\n", tc.desc, err))
|
||||
assert.Equal(t, tc.renderContext, saved.RenderContext, fmt.Sprintf("%s: expected render_context %v got %v\n", tc.desc, tc.renderContext, saved.RenderContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c)
|
||||
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
|
||||
configID string
|
||||
domainID string
|
||||
cert string
|
||||
certKey string
|
||||
ca string
|
||||
expectedConfig bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domain ID ",
|
||||
configID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
domainID: wrongDomainID.DomainID,
|
||||
expectedConfig: bootstrap.Config{},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
configID: c.ID,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
domainID: c.DomainID,
|
||||
expectedConfig: bootstrap.Config{
|
||||
ID: c.ID,
|
||||
ClientCert: "cert",
|
||||
CACert: "ca",
|
||||
ClientKey: "certKey",
|
||||
DomainID: c.DomainID,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.configID, 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 TestRemove(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c)
|
||||
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 TestChangeStatus(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
status bootstrap.Status
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change status with wrong domain ID ",
|
||||
id: saved,
|
||||
domainID: "2",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change status with wrong id",
|
||||
id: "wrong",
|
||||
domainID: c.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change status to Active",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
status: bootstrap.Active,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change status to Inactive",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
status: bootstrap.Inactive,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeStatus(context.Background(), tc.domainID, tc.id, tc.status)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignProfile(t *testing.T) {
|
||||
configRepo := postgres.NewConfigRepository(db, testLog)
|
||||
profileRepo := postgres.NewProfileRepository(db, testLog)
|
||||
|
||||
c := config
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := configRepo.Save(context.Background(), c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
profileID := testsutil.GenerateUUID(t)
|
||||
_, err = profileRepo.Save(context.Background(), bootstrap.Profile{
|
||||
ID: profileID,
|
||||
DomainID: c.DomainID,
|
||||
Name: "edge-gateway",
|
||||
ContentFormat: bootstrap.ContentFormatGoTemplate,
|
||||
Version: 1,
|
||||
})
|
||||
require.Nil(t, err, fmt.Sprintf("Saving profile expected to succeed: %s.\n", err))
|
||||
|
||||
err = configRepo.AssignProfile(context.Background(), c.DomainID, saved, profileID)
|
||||
require.Nil(t, err, fmt.Sprintf("Assigning profile expected to succeed: %s.\n", err))
|
||||
|
||||
stored, err := configRepo.RetrieveByID(context.Background(), c.DomainID, saved)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, profileID, stored.ProfileID, "expected profile assignment to round-trip through the repository")
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,329 +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`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_7",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN magistrala_client TO client_id`,
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN magistrala_secret TO client_secret`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS configs_client_id_key ON configs (client_id)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS configs_client_id_domain_id_key ON configs (client_id, domain_id)`,
|
||||
`DROP TABLE IF EXISTS connections`,
|
||||
`DROP TABLE IF EXISTS channels`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN client_id TO magistrala_client`,
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN client_secret TO magistrala_secret`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_8",
|
||||
Up: []string{
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'client_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'id'
|
||||
) THEN
|
||||
ALTER TABLE configs RENAME COLUMN client_id TO id;
|
||||
END IF;
|
||||
END $$`,
|
||||
`ALTER TABLE IF EXISTS configs DROP COLUMN IF EXISTS client_secret`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE IF EXISTS configs ADD COLUMN IF NOT EXISTS client_secret TEXT`,
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'client_id'
|
||||
) THEN
|
||||
ALTER TABLE configs RENAME COLUMN id TO client_id;
|
||||
END IF;
|
||||
END $$`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_10",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS profiles (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
domain_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(1024) NOT NULL,
|
||||
description TEXT,
|
||||
template_format VARCHAR(64) NOT NULL DEFAULT 'go-template',
|
||||
content_template TEXT,
|
||||
defaults JSONB,
|
||||
binding_slots JSONB,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (domain_id, name)
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_profiles_domain_id ON profiles (domain_id)`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS profiles`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_11",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs ADD COLUMN IF NOT EXISTS profile_id VARCHAR(36) REFERENCES profiles (id) ON DELETE SET NULL`,
|
||||
`ALTER TABLE IF EXISTS configs ADD COLUMN IF NOT EXISTS render_context JSONB`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE IF EXISTS configs DROP COLUMN IF EXISTS render_context`,
|
||||
`ALTER TABLE IF EXISTS configs DROP COLUMN IF EXISTS profile_id`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_12",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS bindings (
|
||||
config_id TEXT NOT NULL,
|
||||
slot VARCHAR(256) NOT NULL,
|
||||
type VARCHAR(64) NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
snapshot JSONB,
|
||||
secret_snapshot BYTEA,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (config_id, slot)
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_bindings_config_id ON bindings (config_id)`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS bindings`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_13",
|
||||
Up: []string{
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'state'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'status'
|
||||
) THEN
|
||||
ALTER TABLE configs RENAME COLUMN state TO status;
|
||||
END IF;
|
||||
END $$`,
|
||||
},
|
||||
Down: []string{
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'status'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'configs' AND column_name = 'state'
|
||||
) THEN
|
||||
ALTER TABLE configs RENAME COLUMN status TO state;
|
||||
END IF;
|
||||
END $$`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_14",
|
||||
Up: []string{
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'binding_snapshots'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'bindings'
|
||||
) THEN
|
||||
ALTER TABLE binding_snapshots RENAME TO bindings;
|
||||
END IF;
|
||||
END $$`,
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_class
|
||||
WHERE relname = 'idx_binding_snapshots_config_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_class
|
||||
WHERE relname = 'idx_bindings_config_id'
|
||||
) THEN
|
||||
ALTER INDEX idx_binding_snapshots_config_id RENAME TO idx_bindings_config_id;
|
||||
END IF;
|
||||
END $$`,
|
||||
},
|
||||
Down: []string{
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'bindings'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'binding_snapshots'
|
||||
) THEN
|
||||
ALTER TABLE bindings RENAME TO binding_snapshots;
|
||||
END IF;
|
||||
END $$`,
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_class
|
||||
WHERE relname = 'idx_bindings_config_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_class
|
||||
WHERE relname = 'idx_binding_snapshots_config_id'
|
||||
) THEN
|
||||
ALTER INDEX idx_bindings_config_id RENAME TO idx_binding_snapshots_config_id;
|
||||
END IF;
|
||||
END $$`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_15",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS profiles ADD COLUMN IF NOT EXISTS binding_slots JSONB`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE IF EXISTS profiles DROP COLUMN IF EXISTS binding_slots`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_16",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS profiles RENAME COLUMN template_format TO content_format`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE IF EXISTS profiles RENAME COLUMN content_format TO template_format`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,263 +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/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/absmach/magistrala/pkg/postgres"
|
||||
)
|
||||
|
||||
var _ bootstrap.ProfileRepository = (*profileRepository)(nil)
|
||||
|
||||
type profileRepository struct {
|
||||
db postgres.Database
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewProfileRepository instantiates a PostgreSQL implementation of ProfileRepository.
|
||||
func NewProfileRepository(db postgres.Database, log *slog.Logger) bootstrap.ProfileRepository {
|
||||
return &profileRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (pr profileRepository) Save(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
q := `INSERT INTO profiles (id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at)
|
||||
VALUES (:id, :domain_id, :name, :description, :content_format, :content_template, :defaults, :binding_slots, :version, :created_at, :updated_at)`
|
||||
|
||||
now := time.Now().UTC()
|
||||
p.CreatedAt = now
|
||||
p.UpdatedAt = now
|
||||
|
||||
dbp, err := toDBProfile(p)
|
||||
if err != nil {
|
||||
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
if _, err = pr.db.NamedExecContext(ctx, q, dbp); err != nil {
|
||||
return bootstrap.Profile{}, postgres.HandleError(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (pr profileRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Profile, error) {
|
||||
q := `SELECT id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at
|
||||
FROM profiles WHERE id = :id AND domain_id = :domain_id`
|
||||
|
||||
rows, err := pr.db.NamedQueryContext(ctx, q, dbProfile{ID: id, DomainID: domainID})
|
||||
if err != nil {
|
||||
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
return bootstrap.Profile{}, repoerr.ErrNotFound
|
||||
}
|
||||
var dbp dbProfile
|
||||
if err := rows.StructScan(&dbp); err != nil {
|
||||
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return toProfile(dbp)
|
||||
}
|
||||
|
||||
func (pr profileRepository) RetrieveAll(ctx context.Context, domainID string, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
|
||||
dbPage := dbProfilesPage{DomainID: domainID, Offset: offset, Limit: limit, Name: name}
|
||||
pageQuery := profilesPageQuery(dbPage)
|
||||
q := fmt.Sprintf(`SELECT id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at
|
||||
FROM profiles %s`, pageQuery)
|
||||
q = applyProfilesOrdering(q)
|
||||
q = fmt.Sprintf(`%s LIMIT :limit OFFSET :offset`, q)
|
||||
|
||||
rows, err := pr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var profiles []bootstrap.Profile
|
||||
for rows.Next() {
|
||||
var dbp dbProfile
|
||||
if err := rows.StructScan(&dbp); err != nil {
|
||||
pr.log.Error(fmt.Sprintf("failed to scan profile row: %s", err))
|
||||
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
p, err := toProfile(dbp)
|
||||
if err != nil {
|
||||
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM profiles %s`, pageQuery)
|
||||
total, err := postgres.Total(ctx, pr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return bootstrap.ProfilesPage{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Profiles: profiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dbProfilesPage struct {
|
||||
DomainID string `db:"domain_id"`
|
||||
Offset uint64 `db:"offset"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
func profilesPageQuery(pm dbProfilesPage) string {
|
||||
var query []string
|
||||
query = append(query, "domain_id = :domain_id")
|
||||
if pm.Name != "" {
|
||||
query = append(query, "name ILIKE '%' || :name || '%'")
|
||||
}
|
||||
return fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
|
||||
}
|
||||
|
||||
func applyProfilesOrdering(q string) string {
|
||||
return fmt.Sprintf("%s ORDER BY created_at DESC", q)
|
||||
}
|
||||
|
||||
func (pr profileRepository) Update(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
var query []string
|
||||
var upq string
|
||||
if p.Name != "" {
|
||||
query = append(query, "name = :name,")
|
||||
}
|
||||
if p.Description != "" {
|
||||
query = append(query, "description = :description,")
|
||||
}
|
||||
if p.ContentFormat != "" {
|
||||
query = append(query, "content_format = :content_format,")
|
||||
}
|
||||
if p.ContentTemplate != "" {
|
||||
query = append(query, "content_template = :content_template,")
|
||||
}
|
||||
if p.Defaults != nil {
|
||||
query = append(query, "defaults = :defaults,")
|
||||
}
|
||||
if p.BindingSlots != nil {
|
||||
query = append(query, "binding_slots = :binding_slots,")
|
||||
}
|
||||
if len(query) > 0 {
|
||||
upq = strings.Join(query, " ")
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`UPDATE profiles SET %s version = version + 1, updated_at = :updated_at
|
||||
WHERE id = :id AND domain_id = :domain_id
|
||||
RETURNING id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at`,
|
||||
upq)
|
||||
|
||||
p.UpdatedAt = time.Now().UTC()
|
||||
dbp, err := toDBProfile(p)
|
||||
if err != nil {
|
||||
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
rows, err := pr.db.NamedQueryContext(ctx, q, dbp)
|
||||
if err != nil {
|
||||
return bootstrap.Profile{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
return bootstrap.Profile{}, repoerr.ErrNotFound
|
||||
}
|
||||
var updated dbProfile
|
||||
if err := rows.StructScan(&updated); err != nil {
|
||||
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return toProfile(updated)
|
||||
}
|
||||
|
||||
func (pr profileRepository) Delete(ctx context.Context, domainID, id string) error {
|
||||
q := `DELETE FROM profiles WHERE id = :id AND domain_id = :domain_id`
|
||||
if _, err := pr.db.NamedExecContext(ctx, q, dbProfile{ID: id, DomainID: domainID}); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dbProfile is the database representation of a Profile.
|
||||
type dbProfile struct {
|
||||
ID string `db:"id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
Name string `db:"name"`
|
||||
Description sql.NullString `db:"description"`
|
||||
ContentFormat string `db:"content_format"`
|
||||
ContentTemplate sql.NullString `db:"content_template"`
|
||||
Defaults []byte `db:"defaults"`
|
||||
BindingSlots []byte `db:"binding_slots"`
|
||||
Version int `db:"version"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
func toDBProfile(p bootstrap.Profile) (dbProfile, error) {
|
||||
defaults, err := json.Marshal(p.Defaults)
|
||||
if err != nil {
|
||||
return dbProfile{}, err
|
||||
}
|
||||
bindingSlots, err := json.Marshal(p.BindingSlots)
|
||||
if err != nil {
|
||||
return dbProfile{}, err
|
||||
}
|
||||
return dbProfile{
|
||||
ID: p.ID,
|
||||
DomainID: p.DomainID,
|
||||
Name: p.Name,
|
||||
Description: nullString(p.Description),
|
||||
ContentFormat: string(p.ContentFormat),
|
||||
ContentTemplate: nullString(p.ContentTemplate),
|
||||
Defaults: defaults,
|
||||
BindingSlots: bindingSlots,
|
||||
Version: p.Version,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toProfile(dbp dbProfile) (bootstrap.Profile, error) {
|
||||
p := bootstrap.Profile{
|
||||
ID: dbp.ID,
|
||||
DomainID: dbp.DomainID,
|
||||
Name: dbp.Name,
|
||||
ContentFormat: bootstrap.ContentFormat(dbp.ContentFormat),
|
||||
Version: dbp.Version,
|
||||
CreatedAt: dbp.CreatedAt,
|
||||
UpdatedAt: dbp.UpdatedAt,
|
||||
}
|
||||
if dbp.Description.Valid {
|
||||
p.Description = dbp.Description.String
|
||||
}
|
||||
if dbp.ContentTemplate.Valid {
|
||||
p.ContentTemplate = dbp.ContentTemplate.String
|
||||
}
|
||||
if len(dbp.Defaults) > 0 && string(dbp.Defaults) != jsonNull {
|
||||
if err := json.Unmarshal(dbp.Defaults, &p.Defaults); err != nil {
|
||||
return bootstrap.Profile{}, err
|
||||
}
|
||||
}
|
||||
if len(dbp.BindingSlots) > 0 && string(dbp.BindingSlots) != jsonNull {
|
||||
if err := json.Unmarshal(dbp.BindingSlots, &p.BindingSlots); err != nil {
|
||||
return bootstrap.Profile{}, err
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap/postgres"
|
||||
mglog "github.com/absmach/magistrala/logger"
|
||||
pgclient "github.com/absmach/magistrala/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = mglog.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: "",
|
||||
}
|
||||
|
||||
migration := postgres.Migration()
|
||||
|
||||
if db, err = pgclient.Setup(dbConfig, *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)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ContentFormat enumerates the supported output formats for rendered profile templates.
|
||||
type ContentFormat string
|
||||
|
||||
const (
|
||||
ContentFormatGoTemplate ContentFormat = "go-template"
|
||||
ContentFormatRaw ContentFormat = "raw"
|
||||
ContentFormatJSON ContentFormat = "json"
|
||||
ContentFormatYAML ContentFormat = "yaml"
|
||||
ContentFormatTOML ContentFormat = "toml"
|
||||
)
|
||||
|
||||
// Profile is a user-managed device configuration template.
|
||||
type Profile struct {
|
||||
ID string `json:"id"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ContentFormat ContentFormat `json:"content_format"`
|
||||
ContentTemplate string `json:"content_template,omitempty"`
|
||||
Defaults map[string]any `json:"defaults,omitempty"`
|
||||
BindingSlots []BindingSlot `json:"binding_slots,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// BindingSlot declares a named resource placeholder that a profile template can use.
|
||||
type BindingSlot struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Fields []string `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// ProfilesPage contains pagination metadata and a slice of Profiles.
|
||||
type ProfilesPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Profiles []Profile `json:"profiles"`
|
||||
}
|
||||
|
||||
// ProfileRepository specifies the persistence API for Profiles.
|
||||
type ProfileRepository interface {
|
||||
// Save persists a new Profile and returns it with server-assigned fields set.
|
||||
Save(ctx context.Context, p Profile) (Profile, error)
|
||||
|
||||
// RetrieveByID returns the Profile with the given ID inside the given domain.
|
||||
RetrieveByID(ctx context.Context, domainID, id string) (Profile, error)
|
||||
|
||||
// RetrieveAll returns a page of Profiles belonging to the given domain, optionally filtered by name.
|
||||
RetrieveAll(ctx context.Context, domainID string, offset, limit uint64, name string) (ProfilesPage, error)
|
||||
|
||||
// Update updates editable fields of the given Profile and returns the updated Profile.
|
||||
Update(ctx context.Context, p Profile) (Profile, error)
|
||||
|
||||
// Delete removes the Profile with the given ID from the given domain.
|
||||
Delete(ctx context.Context, domainID, id string) error
|
||||
}
|
||||
@@ -1,80 +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 Magistrala Response to the Bootstrap request.
|
||||
// This is used as a response from ConfigReader and can easily be
|
||||
// replaced with any other response format.
|
||||
type bootstrapRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,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) (any, error) {
|
||||
res := bootstrapRes{
|
||||
ID: cfg.ID,
|
||||
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
|
||||
}
|
||||
@@ -1,102 +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/magistrala"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type readResp struct {
|
||||
ID string `json:"id"`
|
||||
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{
|
||||
ID: "smq_id",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
Content: "content",
|
||||
}
|
||||
ret := readResp{
|
||||
ID: "smq_id",
|
||||
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.(magistrala.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.")
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Renderer renders a Profile's content template into a concrete device
|
||||
// configuration. All input data must already be stored in Bootstrap — no
|
||||
// external service calls are allowed inside Render.
|
||||
type Renderer interface {
|
||||
Render(profile Profile, enrollment Config, bindings []BindingSnapshot) ([]byte, error)
|
||||
}
|
||||
|
||||
// ErrRenderFailed is returned when template execution or output validation fails.
|
||||
var ErrRenderFailed = errors.New("failed to render profile template")
|
||||
|
||||
type renderer struct{}
|
||||
|
||||
// NewRenderer returns the default Renderer implementation using Go text/template.
|
||||
func NewRenderer() Renderer {
|
||||
return renderer{}
|
||||
}
|
||||
|
||||
func (r renderer) Render(profile Profile, enrollment Config, bindings []BindingSnapshot) ([]byte, error) {
|
||||
rctx := buildRenderContext(profile, enrollment, bindings)
|
||||
|
||||
switch profile.ContentFormat {
|
||||
case ContentFormatRaw:
|
||||
return []byte(profile.ContentTemplate), nil
|
||||
case ContentFormatGoTemplate, ContentFormatJSON, ContentFormatYAML, ContentFormatTOML, "":
|
||||
return r.renderTemplate(profile, rctx)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unsupported template format %q", ErrRenderFailed, profile.ContentFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func (r renderer) renderTemplate(profile Profile, rctx RenderContext) ([]byte, error) {
|
||||
t, err := template.New("bootstrap").
|
||||
Option("missingkey=error").
|
||||
Funcs(allowlistedFuncs()).
|
||||
Parse(profile.ContentTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, rctx); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
||||
}
|
||||
|
||||
return convertOutput(buf.Bytes(), profile.ContentFormat)
|
||||
}
|
||||
|
||||
// convertOutput parses the rendered bytes as any structured format (JSON, YAML,
|
||||
// or TOML) and re-marshals them into the declared target format. For go-template
|
||||
// or empty format the raw bytes are returned unchanged.
|
||||
func convertOutput(out []byte, format ContentFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case ContentFormatGoTemplate, "":
|
||||
return out, nil
|
||||
case ContentFormatJSON, ContentFormatYAML, ContentFormatTOML:
|
||||
var v any
|
||||
if err := parseStructured(out, &v); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
||||
}
|
||||
result, err := marshalAs(v, format)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unsupported format %q", ErrRenderFailed, format)
|
||||
}
|
||||
}
|
||||
|
||||
// parseStructured tries JSON, then YAML, then TOML and unmarshals into v.
|
||||
func parseStructured(out []byte, v any) error {
|
||||
if err := json.Unmarshal(out, v); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := yaml.Unmarshal(out, v); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := toml.Unmarshal(out, v); err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("template output is not valid JSON, YAML, or TOML")
|
||||
}
|
||||
|
||||
// marshalAs re-marshals v into the requested format.
|
||||
func marshalAs(v any, format ContentFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case ContentFormatJSON:
|
||||
return json.MarshalIndent(v, "", " ")
|
||||
case ContentFormatYAML:
|
||||
return yaml.Marshal(v)
|
||||
case ContentFormatTOML:
|
||||
var buf bytes.Buffer
|
||||
if err := toml.NewEncoder(&buf).Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
// buildRenderContext constructs the typed RenderContext from stored data.
|
||||
// No external calls are made here.
|
||||
func buildRenderContext(profile Profile, enrollment Config, bindings []BindingSnapshot) RenderContext {
|
||||
vars := make(map[string]any)
|
||||
for k, v := range profile.Defaults {
|
||||
vars[k] = v
|
||||
}
|
||||
for k, v := range enrollment.RenderContext {
|
||||
vars[k] = v
|
||||
}
|
||||
|
||||
bctx := make(map[string]BindingContext, len(bindings))
|
||||
for _, b := range bindings {
|
||||
bctx[b.Slot] = BindingContext{
|
||||
Type: b.Type,
|
||||
ID: b.ResourceID,
|
||||
Snapshot: b.Snapshot,
|
||||
Secret: b.SecretSnapshot,
|
||||
}
|
||||
}
|
||||
|
||||
return RenderContext{
|
||||
Device: DeviceContext{
|
||||
ID: enrollment.ID,
|
||||
ExternalID: enrollment.ExternalID,
|
||||
DomainID: enrollment.DomainID,
|
||||
},
|
||||
Vars: vars,
|
||||
Bindings: bctx,
|
||||
}
|
||||
}
|
||||
|
||||
// allowlistedFuncs returns the safe set of template helper functions.
|
||||
// No function in this map may call an external service or perform I/O.
|
||||
func allowlistedFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"toJSON": func(v any) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
},
|
||||
"default": func(def, val any) any {
|
||||
if val == nil || val == "" {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
},
|
||||
"required": func(key string, val any) (any, error) {
|
||||
if val == nil || val == "" {
|
||||
return nil, fmt.Errorf("required value %q is missing", key)
|
||||
}
|
||||
return val, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRendererStructuredOutputValidation(t *testing.T) {
|
||||
renderer := bootstrap.NewRenderer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
format bootstrap.ContentFormat
|
||||
template string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid JSON output",
|
||||
format: bootstrap.ContentFormatJSON,
|
||||
template: `{"device_id":"{{ .Device.ID }}"}`,
|
||||
},
|
||||
{
|
||||
desc: "invalid output for JSON format",
|
||||
format: bootstrap.ContentFormatJSON,
|
||||
template: `[unclosed bracket`,
|
||||
err: bootstrap.ErrRenderFailed,
|
||||
},
|
||||
{
|
||||
desc: "valid YAML output",
|
||||
format: bootstrap.ContentFormatYAML,
|
||||
template: "device_id: {{ .Device.ID }}",
|
||||
},
|
||||
{
|
||||
desc: "invalid output for YAML format",
|
||||
format: bootstrap.ContentFormatYAML,
|
||||
template: "[unclosed bracket",
|
||||
err: bootstrap.ErrRenderFailed,
|
||||
},
|
||||
{
|
||||
desc: "valid TOML output",
|
||||
format: bootstrap.ContentFormatTOML,
|
||||
template: `[device]
|
||||
device_id = "{{ .Device.ID }}"`,
|
||||
},
|
||||
{
|
||||
desc: "invalid output for TOML format",
|
||||
format: bootstrap.ContentFormatTOML,
|
||||
template: `[unclosed bracket`,
|
||||
err: bootstrap.ErrRenderFailed,
|
||||
},
|
||||
{
|
||||
desc: "JSON template auto-converted to TOML",
|
||||
format: bootstrap.ContentFormatTOML,
|
||||
template: `{"device_id":"{{ .Device.ID }}"}`,
|
||||
},
|
||||
{
|
||||
desc: "TOML template auto-converted to JSON",
|
||||
format: bootstrap.ContentFormatJSON,
|
||||
template: `device_id = "{{ .Device.ID }}"`,
|
||||
},
|
||||
{
|
||||
desc: "YAML template auto-converted to TOML",
|
||||
format: bootstrap.ContentFormatTOML,
|
||||
template: "device_id: {{ .Device.ID }}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
_, err := renderer.Render(
|
||||
bootstrap.Profile{
|
||||
ContentFormat: tc.format,
|
||||
ContentTemplate: tc.template,
|
||||
},
|
||||
bootstrap.Config{ID: "config-id"},
|
||||
nil,
|
||||
)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
mgsdk "github.com/absmach/magistrala/pkg/sdk"
|
||||
)
|
||||
|
||||
var _ BindingResolver = (*sdkResolver)(nil)
|
||||
|
||||
type sdkResolver struct {
|
||||
sdk mgsdk.SDK
|
||||
}
|
||||
|
||||
// NewSDKResolver returns a BindingResolver that validates resources against
|
||||
// the Magistrala clients and channels services using the SDK. This resolver
|
||||
// is called only at binding time; the render path must never call it.
|
||||
func NewSDKResolver(sdk mgsdk.SDK) BindingResolver {
|
||||
return &sdkResolver{sdk: sdk}
|
||||
}
|
||||
|
||||
func (r *sdkResolver) Resolve(ctx context.Context, req ResolveRequest) ([]BindingSnapshot, error) {
|
||||
var snapshots []BindingSnapshot
|
||||
|
||||
for _, br := range req.Requested {
|
||||
snap, err := r.resolveOne(ctx, req.Enrollment.DomainID, req.Token, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshots = append(snapshots, snap)
|
||||
}
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
func (r *sdkResolver) resolveOne(ctx context.Context, domainID, token string, br BindingRequest) (BindingSnapshot, error) {
|
||||
switch br.Type {
|
||||
case "client":
|
||||
return r.resolveClient(ctx, domainID, token, br)
|
||||
case "channel":
|
||||
return r.resolveChannel(ctx, domainID, token, br)
|
||||
default:
|
||||
return BindingSnapshot{}, fmt.Errorf("unsupported binding type %q", br.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdkResolver) resolveClient(ctx context.Context, domainID, token string, br BindingRequest) (BindingSnapshot, error) {
|
||||
client, sdkErr := r.sdk.Client(ctx, br.ResourceID, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return BindingSnapshot{}, errors.Wrap(svcerr.ErrNotFound,
|
||||
fmt.Errorf("client %q not found: %s", br.ResourceID, sdkErr))
|
||||
}
|
||||
|
||||
snapshot := map[string]any{
|
||||
"id": client.ID,
|
||||
"name": client.Name,
|
||||
}
|
||||
if client.Credentials.Identity != "" {
|
||||
snapshot["identity"] = client.Credentials.Identity
|
||||
}
|
||||
if client.DomainID != "" {
|
||||
snapshot["domain_id"] = client.DomainID
|
||||
}
|
||||
|
||||
secret := map[string]any{}
|
||||
if client.Credentials.Secret != "" {
|
||||
secret["secret"] = client.Credentials.Secret
|
||||
}
|
||||
|
||||
return BindingSnapshot{
|
||||
Slot: br.Slot,
|
||||
Type: br.Type,
|
||||
ResourceID: br.ResourceID,
|
||||
Snapshot: snapshot,
|
||||
SecretSnapshot: secret,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *sdkResolver) resolveChannel(ctx context.Context, domainID, token string, br BindingRequest) (BindingSnapshot, error) {
|
||||
channel, sdkErr := r.sdk.Channel(ctx, br.ResourceID, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return BindingSnapshot{}, errors.Wrap(svcerr.ErrNotFound,
|
||||
fmt.Errorf("channel %q not found: %s", br.ResourceID, sdkErr))
|
||||
}
|
||||
|
||||
snapshot := map[string]any{
|
||||
"id": channel.ID,
|
||||
"name": channel.Name,
|
||||
}
|
||||
if channel.Route != "" {
|
||||
snapshot["topic"] = channel.Route
|
||||
}
|
||||
if channel.DomainID != "" {
|
||||
snapshot["domain_id"] = channel.DomainID
|
||||
}
|
||||
if channel.Metadata != nil {
|
||||
snapshot["metadata"] = channel.Metadata
|
||||
}
|
||||
|
||||
return BindingSnapshot{
|
||||
Slot: br.Slot,
|
||||
Type: br.Type,
|
||||
ResourceID: br.ResourceID,
|
||||
Snapshot: snapshot,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const secretSnapshotCiphertextKey = "ciphertext"
|
||||
|
||||
func (bs bootstrapService) encryptSecretSnapshots(bindings []BindingSnapshot) ([]BindingSnapshot, error) {
|
||||
encrypted := make([]BindingSnapshot, len(bindings))
|
||||
for i, binding := range bindings {
|
||||
encrypted[i] = binding
|
||||
if len(binding.SecretSnapshot) == 0 {
|
||||
continue
|
||||
}
|
||||
secret, err := json.Marshal(binding.SecretSnapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext, err := bs.encrypt(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted[i].SecretSnapshot = map[string]any{
|
||||
secretSnapshotCiphertextKey: ciphertext,
|
||||
}
|
||||
}
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) decryptSecretSnapshots(bindings []BindingSnapshot) ([]BindingSnapshot, error) {
|
||||
decrypted := make([]BindingSnapshot, len(bindings))
|
||||
for i, binding := range bindings {
|
||||
decrypted[i] = binding
|
||||
ciphertext, ok := binding.SecretSnapshot[secretSnapshotCiphertextKey].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
plain, err := bs.decrypt(ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var secret map[string]any
|
||||
if err := json.Unmarshal(plain, &secret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted[i].SecretSnapshot = secret
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func hideSecretSnapshots(bindings []BindingSnapshot) []BindingSnapshot {
|
||||
hidden := make([]BindingSnapshot, len(bindings))
|
||||
for i, binding := range bindings {
|
||||
hidden[i] = binding
|
||||
hidden[i].SecretSnapshot = nil
|
||||
}
|
||||
return hidden
|
||||
}
|
||||
|
||||
func (bs bootstrapService) encrypt(plain []byte) (string, error) {
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plain))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := rand.Read(iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plain)
|
||||
return hex.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) decrypt(in string) ([]byte, error) {
|
||||
ciphertext, err := hex.DecodeString(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, ErrExternalKeySecure
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return ciphertext, nil
|
||||
}
|
||||
@@ -1,532 +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/magistrala"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
mgsdk "github.com/absmach/magistrala/pkg/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
|
||||
ErrExternalKey = errors.NewAuthZError("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
|
||||
ErrExternalKeySecure = errors.NewAuthZError("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.NewServiceError("failed to add bootstrap configuration")
|
||||
|
||||
// ErrBootstrapStatus indicates an invalid bootstrap status.
|
||||
ErrBootstrapStatus = errors.NewRequestError("invalid bootstrap status")
|
||||
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errEnableConfig = errors.New("failed to enable bootstrap configuration")
|
||||
errDisableConfig = errors.New("failed to disable bootstrap configuration")
|
||||
errUpdateCert = errors.New("failed to update cert")
|
||||
|
||||
errCreateProfile = errors.New("failed to create profile")
|
||||
errViewProfile = errors.New("failed to view profile")
|
||||
errUpdateProfile = errors.New("failed to update profile")
|
||||
errDeleteProfile = errors.New("failed to delete profile")
|
||||
errListProfiles = errors.New("failed to list profiles")
|
||||
errAssignProfile = errors.New("failed to assign profile to enrollment")
|
||||
errBindResources = errors.New("failed to bind resources")
|
||||
errListBindings = errors.New("failed to list bindings")
|
||||
errRefreshBinding = errors.New("failed to refresh bindings")
|
||||
errRenderBootstrap = errors.New("failed to render bootstrap configuration")
|
||||
)
|
||||
|
||||
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).
|
||||
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, id, clientCert, clientKey, caCert string) (Config, 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)
|
||||
|
||||
// EnableConfig enables the Config so its device can successfully bootstrap.
|
||||
EnableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error)
|
||||
|
||||
// DisableConfig disables the Config, preventing its device from bootstrapping.
|
||||
DisableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error)
|
||||
|
||||
// CreateProfile persists a new device Profile.
|
||||
CreateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error)
|
||||
|
||||
// ViewProfile returns the Profile with the given ID.
|
||||
ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (Profile, error)
|
||||
|
||||
// UpdateProfile updates editable fields of the given Profile and returns the updated Profile.
|
||||
UpdateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error)
|
||||
|
||||
// ListProfiles returns a page of Profiles belonging to the domain.
|
||||
ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (ProfilesPage, error)
|
||||
|
||||
// DeleteProfile removes the Profile with the given ID.
|
||||
DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error
|
||||
|
||||
// AssignProfile sets the ProfileID on an existing enrollment (Config).
|
||||
AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error
|
||||
|
||||
// BindResources resolves the requested bindings through their owning services,
|
||||
// stores snapshots, and marks the enrollment renderable when all required slots
|
||||
// are satisfied.
|
||||
BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []BindingRequest) error
|
||||
|
||||
// ListBindings returns all stored binding snapshots for an enrollment.
|
||||
ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]BindingSnapshot, error)
|
||||
|
||||
// RefreshBindings re-resolves all existing bindings for an enrollment and
|
||||
// updates the stored snapshots.
|
||||
RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID 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.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config, bool) (any, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
configs ConfigRepository
|
||||
profiles ProfileRepository
|
||||
bindings BindingStore
|
||||
resolver BindingResolver
|
||||
renderer Renderer
|
||||
hasher Hasher
|
||||
sdk mgsdk.SDK
|
||||
encKey []byte
|
||||
idProvider magistrala.IDProvider
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(
|
||||
configs ConfigRepository,
|
||||
profiles ProfileRepository,
|
||||
bindings BindingStore,
|
||||
resolver BindingResolver,
|
||||
renderer Renderer,
|
||||
sdk mgsdk.SDK,
|
||||
hasher Hasher,
|
||||
encKey []byte,
|
||||
idp magistrala.IDProvider,
|
||||
) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
profiles: profiles,
|
||||
bindings: bindings,
|
||||
resolver: resolver,
|
||||
renderer: renderer,
|
||||
hasher: hasher,
|
||||
sdk: sdk,
|
||||
encKey: encKey,
|
||||
idProvider: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
|
||||
id, err := bs.idProvider.ID()
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrAddBootstrap, err)
|
||||
}
|
||||
|
||||
hashedKey, err := bs.hasher.Hash(cfg.ExternalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.ID = id
|
||||
cfg.DomainID = session.DomainID
|
||||
cfg.Status = Active
|
||||
cfg.ExternalKey = hashedKey
|
||||
|
||||
saved, err := bs.configs.Save(ctx, cfg)
|
||||
if err != nil {
|
||||
if errors.Contains(err, repoerr.ErrConflict) {
|
||||
return Config{}, errors.Wrap(svcerr.ErrConflict, err)
|
||||
}
|
||||
return Config{}, errors.Wrap(ErrAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.ID = saved
|
||||
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(svcerr.ErrUpdateEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (Config, error) {
|
||||
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, id, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, 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 err := bs.hasher.Compare(externalKey, cfg.ExternalKey); err != nil {
|
||||
return Config{}, ErrExternalKey
|
||||
}
|
||||
if cfg.Status == DisabledStatus {
|
||||
return Config{}, ErrBootstrap
|
||||
}
|
||||
|
||||
cfg, err = bs.renderBootstrapConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) renderBootstrapConfig(ctx context.Context, cfg Config) (Config, error) {
|
||||
if cfg.ProfileID == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
if bs.profiles == nil || bs.bindings == nil || bs.renderer == nil {
|
||||
return Config{}, errors.Wrap(errRenderBootstrap, errors.New("profile rendering support not configured"))
|
||||
}
|
||||
|
||||
profile, err := bs.profiles.RetrieveByID(ctx, cfg.DomainID, cfg.ProfileID)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errRenderBootstrap, err)
|
||||
}
|
||||
|
||||
bindings, err := bs.bindings.Retrieve(ctx, cfg.ID)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errRenderBootstrap, err)
|
||||
}
|
||||
if err := validateRequiredBindings(profile, bindings); err != nil {
|
||||
return Config{}, errors.Wrap(errRenderBootstrap, err)
|
||||
}
|
||||
bindings, err = bs.decryptSecretSnapshots(bindings)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errRenderBootstrap, err)
|
||||
}
|
||||
|
||||
rendered, err := bs.renderer.Render(profile, cfg, bindings)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errRenderBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.Content = string(rendered)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
|
||||
cfg, err := bs.changeConfigStatus(ctx, session.DomainID, id, EnabledStatus)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errEnableConfig, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
|
||||
cfg, err := bs.changeConfigStatus(ctx, session.DomainID, id, DisabledStatus)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errDisableConfig, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) changeConfigStatus(ctx context.Context, domainID, id string, status Status) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, domainID, id)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
if cfg.Status == status {
|
||||
return cfg, nil
|
||||
}
|
||||
if err := bs.configs.ChangeStatus(ctx, domainID, id, status); err != nil {
|
||||
return Config{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
||||
}
|
||||
cfg.Status = status
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// --- Profile management ---
|
||||
|
||||
func (bs bootstrapService) CreateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error) {
|
||||
if bs.profiles == nil {
|
||||
return Profile{}, errors.Wrap(errCreateProfile, errors.New("profile repository not configured"))
|
||||
}
|
||||
id, err := bs.idProvider.ID()
|
||||
if err != nil {
|
||||
return Profile{}, errors.Wrap(errCreateProfile, err)
|
||||
}
|
||||
p.ID = id
|
||||
p.DomainID = session.DomainID
|
||||
if p.ContentFormat == "" {
|
||||
p.ContentFormat = ContentFormatJSON
|
||||
}
|
||||
p.Version = 1
|
||||
if err := validateProfileBindingSlots(p); err != nil {
|
||||
return Profile{}, errors.Wrap(errCreateProfile, err)
|
||||
}
|
||||
if err := validateProfileTemplate(p); err != nil {
|
||||
return Profile{}, errors.Wrap(errCreateProfile, err)
|
||||
}
|
||||
saved, err := bs.profiles.Save(ctx, p)
|
||||
if err != nil {
|
||||
return Profile{}, errors.Wrap(errCreateProfile, err)
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (Profile, error) {
|
||||
if bs.profiles == nil {
|
||||
return Profile{}, errors.Wrap(errViewProfile, errors.New("profile repository not configured"))
|
||||
}
|
||||
p, err := bs.profiles.RetrieveByID(ctx, session.DomainID, profileID)
|
||||
if err != nil {
|
||||
return Profile{}, errors.Wrap(errViewProfile, err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error) {
|
||||
if bs.profiles == nil {
|
||||
return Profile{}, errors.Wrap(errUpdateProfile, errors.New("profile repository not configured"))
|
||||
}
|
||||
p.DomainID = session.DomainID
|
||||
if err := validateProfileBindingSlots(p); err != nil {
|
||||
return Profile{}, errors.Wrap(errUpdateProfile, err)
|
||||
}
|
||||
if err := validateProfileTemplate(p); err != nil {
|
||||
return Profile{}, errors.Wrap(errUpdateProfile, err)
|
||||
}
|
||||
updated, err := bs.profiles.Update(ctx, p)
|
||||
if err != nil {
|
||||
return Profile{}, errors.Wrap(errUpdateProfile, err)
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (ProfilesPage, error) {
|
||||
if bs.profiles == nil {
|
||||
return ProfilesPage{}, errors.Wrap(errListProfiles, errors.New("profile repository not configured"))
|
||||
}
|
||||
page, err := bs.profiles.RetrieveAll(ctx, session.DomainID, offset, limit, name)
|
||||
if err != nil {
|
||||
return ProfilesPage{}, errors.Wrap(errListProfiles, err)
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
|
||||
if bs.profiles == nil {
|
||||
return errors.Wrap(errDeleteProfile, errors.New("profile repository not configured"))
|
||||
}
|
||||
if err := bs.profiles.Delete(ctx, session.DomainID, profileID); err != nil {
|
||||
return errors.Wrap(errDeleteProfile, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Enrollment-profile assignment ---
|
||||
|
||||
func (bs bootstrapService) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
|
||||
if bs.profiles == nil {
|
||||
return errors.Wrap(errAssignProfile, errors.New("profile repository not configured"))
|
||||
}
|
||||
// Validate profile exists in domain.
|
||||
if _, err := bs.profiles.RetrieveByID(ctx, session.DomainID, profileID); err != nil {
|
||||
return errors.Wrap(errAssignProfile, err)
|
||||
}
|
||||
if err := bs.configs.AssignProfile(ctx, session.DomainID, configID, profileID); err != nil {
|
||||
return errors.Wrap(errAssignProfile, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Binding management ---
|
||||
|
||||
func (bs bootstrapService) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, requested []BindingRequest) error {
|
||||
if bs.profiles == nil || bs.bindings == nil || bs.resolver == nil {
|
||||
return errors.Wrap(errBindResources, errors.New("binding support not configured"))
|
||||
}
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
profile, err := bs.profiles.RetrieveByID(ctx, session.DomainID, cfg.ProfileID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
if err := validateRequestedBindings(profile, requested); err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
snapshots, err := bs.resolver.Resolve(ctx, ResolveRequest{
|
||||
Enrollment: cfg,
|
||||
Token: token,
|
||||
Requested: requested,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
existing, err := bs.bindings.Retrieve(ctx, configID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
if err := validateRequiredBindings(profile, mergeBindingSnapshots(existing, snapshots)); err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
snapshots, err = bs.encryptSecretSnapshots(snapshots)
|
||||
if err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
if err := bs.bindings.Save(ctx, configID, snapshots); err != nil {
|
||||
return errors.Wrap(errBindResources, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]BindingSnapshot, error) {
|
||||
if bs.bindings == nil {
|
||||
return nil, errors.Wrap(errListBindings, errors.New("binding support not configured"))
|
||||
}
|
||||
if _, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID); err != nil {
|
||||
return nil, errors.Wrap(errListBindings, err)
|
||||
}
|
||||
snapshots, err := bs.bindings.Retrieve(ctx, configID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errListBindings, err)
|
||||
}
|
||||
return hideSecretSnapshots(snapshots), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
|
||||
if bs.profiles == nil || bs.bindings == nil || bs.resolver == nil {
|
||||
return errors.Wrap(errRefreshBinding, errors.New("binding support not configured"))
|
||||
}
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
profile, err := bs.profiles.RetrieveByID(ctx, session.DomainID, cfg.ProfileID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
existing, err := bs.bindings.Retrieve(ctx, configID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Re-resolve every existing binding to refresh its snapshot.
|
||||
requested := make([]BindingRequest, len(existing))
|
||||
for i, b := range existing {
|
||||
requested[i] = BindingRequest{Slot: b.Slot, Type: b.Type, ResourceID: b.ResourceID}
|
||||
}
|
||||
if err := validateRequestedBindings(profile, requested); err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
refreshed, err := bs.resolver.Resolve(ctx, ResolveRequest{
|
||||
Enrollment: cfg,
|
||||
Token: token,
|
||||
Requested: requested,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
if err := validateRequiredBindings(profile, refreshed); err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
refreshed, err = bs.encryptSecretSnapshots(refreshed)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRefreshBinding, err)
|
||||
}
|
||||
return bs.bindings.Save(ctx, configID, refreshed)
|
||||
}
|
||||
|
||||
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
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
)
|
||||
|
||||
// Status represents bootstrap enrollment availability.
|
||||
type Status uint8
|
||||
|
||||
// Possible bootstrap enrollment statuses.
|
||||
const (
|
||||
EnabledStatus Status = iota
|
||||
DisabledStatus
|
||||
// AllStatus is used for querying purposes to list configs irrespective
|
||||
// of their status. It is never stored in the database.
|
||||
AllStatus
|
||||
)
|
||||
|
||||
// String representation of bootstrap status values.
|
||||
const (
|
||||
Disabled = "disabled"
|
||||
Enabled = "enabled"
|
||||
All = "all"
|
||||
Unknown = "unknown"
|
||||
)
|
||||
|
||||
// Backward-compatible aliases kept while callers move off the old names.
|
||||
const (
|
||||
Inactive = DisabledStatus
|
||||
Active = EnabledStatus
|
||||
)
|
||||
|
||||
// String returns string representation of Status.
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case DisabledStatus:
|
||||
return Disabled
|
||||
case EnabledStatus:
|
||||
return Enabled
|
||||
case AllStatus:
|
||||
return All
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
// ToStatus converts a string or legacy numeric string value to Status.
|
||||
func ToStatus(status string) (Status, error) {
|
||||
switch strings.ToLower(status) {
|
||||
case "", Enabled, "0":
|
||||
return EnabledStatus, nil
|
||||
case Disabled, "1":
|
||||
return DisabledStatus, nil
|
||||
case All:
|
||||
return AllStatus, nil
|
||||
}
|
||||
return Status(0), svcerr.ErrInvalidStatus
|
||||
}
|
||||
|
||||
// MarshalJSON renders bootstrap status as a string literal.
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON accepts both string and legacy numeric bootstrap statuses.
|
||||
func (s *Status) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || string(data) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if data[0] != '"' {
|
||||
var n int
|
||||
if err := json.Unmarshal(data, &n); err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, err := ToStatus(strconv.Itoa(n))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
var status string
|
||||
if err := json.Unmarshal(data, &status); err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, err := ToStatus(status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = parsed
|
||||
return nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing provides tracing instrumentation for Magistrala Users service.
|
||||
//
|
||||
// This package provides tracing middleware for Magistrala Users service.
|
||||
// It can be used to trace incoming requests and add tracing capabilities to
|
||||
// Magistrala Users service.
|
||||
//
|
||||
// For more details about tracing instrumentation for Magistrala messaging refer
|
||||
// to the documentation at https://magistrala.absmach.eu/docs/.
|
||||
package tracing
|
||||
@@ -1,198 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/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("config_id", cfg.ID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("external_id", cfg.ExternalID),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("status", cfg.Status.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("config_id", cfg.ID),
|
||||
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, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_cert", trace.WithAttributes(
|
||||
attribute.String("config_id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// 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_id", externalID),
|
||||
attribute.Bool("secure", secure),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_enable_config", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.EnableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_disable_config", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.DisableConfig(ctx, session, id)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_create_profile", trace.WithAttributes(
|
||||
attribute.String("name", p.Name),
|
||||
attribute.String("domain_id", p.DomainID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.CreateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_view_profile", trace.WithAttributes(
|
||||
attribute.String("profile_id", profileID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.ViewProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_profile", trace.WithAttributes(
|
||||
attribute.String("profile_id", p.ID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.UpdateProfile(ctx, session, p)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_list_profiles", trace.WithAttributes(
|
||||
attribute.Int64("offset", int64(offset)),
|
||||
attribute.Int64("limit", int64(limit)),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.ListProfiles(ctx, session, offset, limit, name)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_delete_profile", trace.WithAttributes(
|
||||
attribute.String("profile_id", profileID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.DeleteProfile(ctx, session, profileID)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_assign_profile", trace.WithAttributes(
|
||||
attribute.String("config_id", configID),
|
||||
attribute.String("profile_id", profileID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.AssignProfile(ctx, session, configID, profileID)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_bind_resources", trace.WithAttributes(
|
||||
attribute.String("config_id", configID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.BindResources(ctx, session, token, configID, bindings)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_list_bindings", trace.WithAttributes(
|
||||
attribute.String("config_id", configID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.ListBindings(ctx, session, configID)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_refresh_bindings", trace.WithAttributes(
|
||||
attribute.String("config_id", configID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.RefreshBindings(ctx, session, token, configID)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type loggingMiddleware struct {
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
// LoggingMiddleware adds logging facilities to the service.
|
||||
func LoggingMiddleware(svc certs.Service, logger *slog.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type metricsMiddleware struct {
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
// MetricsMiddleware instruments service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metrics.Histogram) certs.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
# Channels
|
||||
|
||||
The Channels service is a core component of Magistrala that manages communication channels between devices and applications. It handles channel creation, configuration, access control and message routing within the Magistrala ecosystem.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the following environment variables (unset variables use default values):
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------- | --------------------------------------------- | ------------------------------ |
|
||||
| `MG_CHANNELS_LOG_LEVEL` | Log level (debug, info, warn, error) | info |
|
||||
| `MG_CHANNELS_HTTP_HOST` | HTTP host for Channels service | localhost |
|
||||
| `MG_CHANNELS_HTTP_PORT` | HTTP port for Channels service | 9005 |
|
||||
| `MG_CHANNELS_SERVER_CERT` | Path to PEM encoded server certificate | "" |
|
||||
| `MG_CHANNELS_SERVER_KEY` | Path to PEM encoded server key file | "" |
|
||||
| `MG_CHANNELS_GRPC_HOST` | gRPC host for Channels service | localhost |
|
||||
| `MG_CHANNELS_GRPC_PORT` | gRPC port for Channels service | 7005 |
|
||||
| `MG_CHANNELS_DB_HOST` | Database host address | localhost |
|
||||
| `MG_CHANNELS_DB_PORT` | Database port | 5432 |
|
||||
| `MG_CHANNELS_DB_USER` | Database user | magistrala |
|
||||
| `MG_CHANNELS_DB_PASS` | Database password | magistrala |
|
||||
| `MG_CHANNELS_DB_NAME` | Name of the database used by the service | channels |
|
||||
| `MG_CHANNELS_DB_SSL_MODE` | Database connection SSL mode | disable |
|
||||
| `MG_CHANNELS_CACHE_URL` | Cache database URL | <redis://localhost:6379/0> |
|
||||
| `MG_JAEGER_URL` | Jaeger tracing server URL | <http://jaeger:4318/v1/traces> |
|
||||
| `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | true |
|
||||
|
||||
## Features
|
||||
|
||||
- **Channel Management**: Create, update, delete and list channels
|
||||
- **Access Control**: Manage channel permissions and user access
|
||||
- **Message Routing**: Route messages between connected devices and services
|
||||
- **Channel Groups**: Organize channels into logical groups
|
||||
- **Metadata Support**: Attach custom metadata to channels
|
||||
- **Real-time Updates**: Live channel state synchronization
|
||||
|
||||
## Architecture
|
||||
|
||||
The service is built using:
|
||||
|
||||
- **Go**: Core service implementation
|
||||
- **gRPC**: Inter-service communication
|
||||
- **PostgreSQL**: Primary data storage
|
||||
- **Redis**: Caching and pub/sub messaging
|
||||
- **Docker**: Containerized deployment
|
||||
|
||||
### Channels Table
|
||||
|
||||
| Column | Type | Description |
|
||||
| ----------------- | ------------- | ----------------------------------------------------- |
|
||||
| `id` | VARCHAR(36) | UUID of the channel (primary key) |
|
||||
| `name` | VARCHAR(1024) | Human-readable name |
|
||||
| `domain_id` | VARCHAR(36) | Domain to which the channel belongs |
|
||||
| `parent_group_id` | VARCHAR(36) | Optional group parent |
|
||||
| `tags` | TEXT[] | Array of tags |
|
||||
| `metadata` | JSONB | Free-form structured metadata |
|
||||
| `created_by` | VARCHAR(254) | User that created the channel |
|
||||
| `created_at` | TIMESTAMPTZ | Timestamp of creation |
|
||||
| `updated_at` | TIMESTAMPTZ | Timestamp of last update |
|
||||
| `updated_by` | VARCHAR(254) | User that performed last update |
|
||||
| `status` | SMALLINT | 0 = enabled, 1 = disabled |
|
||||
| `route` | VARCHAR(36) | Optional route identifier unique within domain if set |
|
||||
|
||||
### Connections Table
|
||||
|
||||
| Column | Type | Description |
|
||||
| ------------ | ----------- | ----------------------------------------------- |
|
||||
| `channel_id` | VARCHAR(36) | Channel UUID |
|
||||
| `domain_id` | VARCHAR(36) | Domain of channel and client |
|
||||
| `client_id` | VARCHAR(36) | Client UUID |
|
||||
| `type` | SMALLINT | Connection type: `1 = Publish`, `2 = Subscribe` |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service is available as a Docker container. Refer to the Docker Compose section for the `channels` service in `docker-compose.yaml` for deployment configuration.
|
||||
|
||||
To build and run locally:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/magistrala
|
||||
cd magistrala
|
||||
|
||||
# compile the channels
|
||||
make channels
|
||||
|
||||
make install
|
||||
|
||||
MG_CHANNELS_HTTP_HOST=localhost \
|
||||
MG_CHANNELS_HTTP_PORT=9005 \
|
||||
MG_CHANNELS_DB_HOST=localhost \
|
||||
MG_CHANNELS_DB_PORT=5432 \
|
||||
MG_CHANNELS_DB_USER=magistrala \MG_CHANNELS_DB_PASS=magistrala \MG_CHANNELS_DB_NAME=channels \
|
||||
$GOBIN/magistrala-channels
|
||||
```
|
||||
|
||||
### Running the Service
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export MQ_CHANNELS_DB_HOST=localhost
|
||||
export MQ_CHANNELS_DB_PORT=5432
|
||||
|
||||
# Run the service
|
||||
go run cmd/main.go
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```bash
|
||||
docker run -p 8180:8180 magistrala/channels
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run unit tests
|
||||
go test ./...
|
||||
|
||||
# Run integration tests
|
||||
make test-integration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Channels service supports the following operations:
|
||||
|
||||
| Operation | Description |
|
||||
| --------------- | -------------------------------------------- |
|
||||
| `create` | Create a new channel |
|
||||
| `list` | Retrieve all channels (paged) |
|
||||
| `get` | Retrieve a single channel by ID |
|
||||
| `update` | Update a channel’s name & metadata |
|
||||
| `delete` | Permanently delete a channel |
|
||||
| `enable` | Enable a previously disabled channel |
|
||||
| `disable` | Disable an active channel |
|
||||
| `set-parent` | Assign a parent group to a channel |
|
||||
| `remove-parent` | Remove parent group from a channel |
|
||||
| `connect` | Connect one or more clients to channels |
|
||||
| `disconnect` | Disconnect one or more clients from channels |
|
||||
|
||||
### Example: Create a Channel
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9005/<domainID>/channels \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "myChannel",
|
||||
"metadata": { "location": "lab" },
|
||||
"route": "sensor-data",
|
||||
"tags": ["sensor","edge"],
|
||||
"status": "enabled"
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Connect Clients & Channels
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9005/<domainID>/channels/connect \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"channel_ids": ["<chanID1>", "<chanID2>"],
|
||||
"client_ids": ["<clientID1>", "<clientID2>"],
|
||||
"types": ["publish", "subscribe"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Disconnect Clients from a Channel
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9005/<domainID>/channels/disconnect \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"channel_ids": ["<chanID>"],
|
||||
"client_ids": ["<clientID>"],
|
||||
"types": ["publish"]
|
||||
}'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use tags and metadata to manage and categorize channels (e.g., environment, region, purpose).
|
||||
- Assign `route` thoughtfully when channels need a predictable identifier.
|
||||
- Keep channel hierarchies shallow for easier navigation (avoid deep nesting unless required).
|
||||
- Use `disable` rather than immediate delete when you want to suspend a channel temporarily.
|
||||
- Clean up unused connections: regularly review which clients are connected to channels and remove stale links.
|
||||
- Enforce minimal privileges: only allow clients to connect to channels they truly need.
|
||||
- Monitoring: use the `/health` endpoint and version metadata for service stability.
|
||||
|
||||
## Versioning & Health Check
|
||||
|
||||
The Channels service exposes a `/health` endpoint to provide operational status and version info.
|
||||
|
||||
### Health Check Request
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:9005/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "pass",
|
||||
"version": "0.18.0",
|
||||
"commit": "<commit-hash>",
|
||||
"description": "channels service",
|
||||
"build_time": "2025-11-19T..."
|
||||
}
|
||||
```
|
||||
@@ -1,224 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
|
||||
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const svcName = "channels.v1.ChannelsService"
|
||||
|
||||
var _ grpcChannelsV1.ChannelsServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
timeout time.Duration
|
||||
authorize endpoint.Endpoint
|
||||
removeClientConnections endpoint.Endpoint
|
||||
unsetParentGroupFromChannels endpoint.Endpoint
|
||||
retrieveEntity endpoint.Endpoint
|
||||
retrieveIDByRoute endpoint.Endpoint
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(conn *grpc.ClientConn, timeout time.Duration) grpcChannelsV1.ChannelsServiceClient {
|
||||
return &grpcClient{
|
||||
authorize: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Authorize",
|
||||
encodeAuthorizeRequest,
|
||||
decodeAuthorizeResponse,
|
||||
grpcChannelsV1.AuthzRes{},
|
||||
).Endpoint(),
|
||||
removeClientConnections: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RemoveClientConnections",
|
||||
encodeRemoveClientConnectionsRequest,
|
||||
decodeRemoveClientConnectionsResponse,
|
||||
grpcChannelsV1.RemoveClientConnectionsRes{},
|
||||
).Endpoint(),
|
||||
unsetParentGroupFromChannels: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"UnsetParentGroupFromChannels",
|
||||
encodeUnsetParentGroupFromChannelsRequest,
|
||||
decodeUnsetParentGroupFromChannelsResponse,
|
||||
grpcChannelsV1.UnsetParentGroupFromChannelsRes{},
|
||||
).Endpoint(),
|
||||
retrieveEntity: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RetrieveEntity",
|
||||
encodeRetrieveEntityRequest,
|
||||
decodeRetrieveEntityResponse,
|
||||
grpcCommonV1.RetrieveEntityRes{},
|
||||
).Endpoint(),
|
||||
retrieveIDByRoute: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RetrieveIDByRoute",
|
||||
encodeRetrieveIDByRouteRequest,
|
||||
decodeRetrieveIDByRouteResponse,
|
||||
grpcCommonV1.RetrieveEntityRes{},
|
||||
).Endpoint(),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client grpcClient) Authorize(ctx context.Context, req *grpcChannelsV1.AuthzReq, _ ...grpc.CallOption) (r *grpcChannelsV1.AuthzRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authorize(ctx, authorizeReq{
|
||||
domainID: req.GetDomainId(),
|
||||
clientID: req.GetClientId(),
|
||||
clientType: req.GetClientType(),
|
||||
channelID: req.GetChannelId(),
|
||||
connType: connections.ConnType(req.GetType()),
|
||||
})
|
||||
if err != nil {
|
||||
return &grpcChannelsV1.AuthzRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
|
||||
return &grpcChannelsV1.AuthzRes{Authorized: ar.authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(authorizeReq)
|
||||
|
||||
return &grpcChannelsV1.AuthzReq{
|
||||
DomainId: req.domainID,
|
||||
ClientId: req.clientID,
|
||||
ClientType: req.clientType,
|
||||
ChannelId: req.channelID,
|
||||
Type: uint32(req.connType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(*grpcChannelsV1.AuthzRes)
|
||||
|
||||
return authorizeRes{authorized: res.GetAuthorized()}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) RemoveClientConnections(ctx context.Context, req *grpcChannelsV1.RemoveClientConnectionsReq, _ ...grpc.CallOption) (r *grpcChannelsV1.RemoveClientConnectionsRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
if _, err := client.removeClientConnections(ctx, req); err != nil {
|
||||
return &grpcChannelsV1.RemoveClientConnectionsRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return &grpcChannelsV1.RemoveClientConnectionsRes{}, nil
|
||||
}
|
||||
|
||||
func encodeRemoveClientConnectionsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcChannelsV1.RemoveClientConnectionsReq), nil
|
||||
}
|
||||
|
||||
func decodeRemoveClientConnectionsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcChannelsV1.RemoveClientConnectionsRes), nil
|
||||
}
|
||||
|
||||
func (client grpcClient) UnsetParentGroupFromChannels(ctx context.Context, req *grpcChannelsV1.UnsetParentGroupFromChannelsReq, _ ...grpc.CallOption) (r *grpcChannelsV1.UnsetParentGroupFromChannelsRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
if _, err := client.unsetParentGroupFromChannels(ctx, req); err != nil {
|
||||
return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, nil
|
||||
}
|
||||
|
||||
func encodeUnsetParentGroupFromChannelsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcChannelsV1.UnsetParentGroupFromChannelsReq), nil
|
||||
}
|
||||
|
||||
func decodeUnsetParentGroupFromChannelsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcChannelsV1.UnsetParentGroupFromChannelsRes), nil
|
||||
}
|
||||
|
||||
func (client grpcClient) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntityRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.retrieveEntity(ctx, req)
|
||||
if err != nil {
|
||||
return &grpcCommonV1.RetrieveEntityRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func encodeRetrieveEntityRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcCommonV1.RetrieveEntityReq), nil
|
||||
}
|
||||
|
||||
func decodeRetrieveEntityResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func (client grpcClient) RetrieveIDByRoute(ctx context.Context, req *grpcCommonV1.RetrieveIDByRouteReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntityRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.retrieveIDByRoute(ctx, req)
|
||||
if err != nil {
|
||||
return &grpcCommonV1.RetrieveEntityRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func encodeRetrieveIDByRouteRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcCommonV1.RetrieveIDByRouteReq), nil
|
||||
}
|
||||
|
||||
func decodeRetrieveIDByRouteResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func decodeError(err error) error {
|
||||
if st, ok := status.FromError(err); ok {
|
||||
switch st.Code() {
|
||||
case codes.Unauthenticated:
|
||||
return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message()))
|
||||
case codes.PermissionDenied:
|
||||
return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message()))
|
||||
case codes.InvalidArgument:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message()))
|
||||
case codes.FailedPrecondition:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message()))
|
||||
case codes.NotFound:
|
||||
return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message()))
|
||||
case codes.AlreadyExists:
|
||||
return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message()))
|
||||
case codes.OK:
|
||||
if msg := st.Message(); msg != "" {
|
||||
return errors.Wrap(errors.ErrUnidentified, errors.New(msg))
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message()))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of Auth service gRPC API.
|
||||
package grpc
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ch "github.com/absmach/magistrala/channels"
|
||||
channels "github.com/absmach/magistrala/channels/private"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func authorizeEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(authorizeReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.Authorize(ctx, ch.AuthzReq{
|
||||
DomainID: req.domainID,
|
||||
ClientID: req.clientID,
|
||||
ClientType: req.clientType,
|
||||
ChannelID: req.channelID,
|
||||
Type: req.connType,
|
||||
}); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
return authorizeRes{authorized: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeClientConnectionsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(removeClientConnectionsReq)
|
||||
|
||||
if err := svc.RemoveClientConnections(ctx, req.clientID); err != nil {
|
||||
return removeClientConnectionsRes{}, err
|
||||
}
|
||||
|
||||
return removeClientConnectionsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func unsetParentGroupFromChannelsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(unsetParentGroupFromChannelsReq)
|
||||
|
||||
if err := svc.UnsetParentGroupFromChannels(ctx, req.parentGroupID); err != nil {
|
||||
return unsetParentGroupFromChannelsRes{}, err
|
||||
}
|
||||
|
||||
return unsetParentGroupFromChannelsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveEntityEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(retrieveEntityReq)
|
||||
channel, err := svc.RetrieveByID(ctx, req.Id)
|
||||
if err != nil {
|
||||
return retrieveEntityRes{}, err
|
||||
}
|
||||
|
||||
return retrieveEntityRes{id: channel.ID, domain: channel.Domain, parentGroup: channel.ParentGroup, status: uint8(channel.Status)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveIDByRouteEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(retrieveIDByRouteReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return retrieveIDByRouteRes{}, err
|
||||
}
|
||||
|
||||
id, err := svc.RetrieveIDByRoute(ctx, req.route, req.domainID)
|
||||
if err != nil {
|
||||
return retrieveIDByRouteRes{}, err
|
||||
}
|
||||
|
||||
return retrieveIDByRouteRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
|
||||
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
ch "github.com/absmach/magistrala/channels"
|
||||
grpcapi "github.com/absmach/magistrala/channels/api/grpc"
|
||||
"github.com/absmach/magistrala/channels/private/mocks"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const port = 7005
|
||||
|
||||
var (
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
validChannel = ch.Channel{
|
||||
ID: validID,
|
||||
Domain: testsutil.GenerateUUID(&testing.T{}),
|
||||
Status: channels.EnabledStatus,
|
||||
}
|
||||
)
|
||||
|
||||
func startGRPCServer(svc *mocks.Service, port int) *grpc.Server {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to obtain port: %s", err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
grpcChannelsV1.RegisterChannelsServiceServer(server, grpcapi.NewServer(svc))
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil {
|
||||
panic(fmt.Sprintf("failed to serve: %s", err))
|
||||
}
|
||||
}()
|
||||
return server
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
clientID string
|
||||
clientType string
|
||||
channelID string
|
||||
connType connections.ConnType
|
||||
err error
|
||||
authzErr error
|
||||
res *grpcChannelsV1.AuthzRes
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "authorize successfully",
|
||||
domainID: validID,
|
||||
clientID: validID,
|
||||
clientType: policies.UserType,
|
||||
channelID: validID,
|
||||
connType: connections.Publish,
|
||||
res: &grpcChannelsV1.AuthzRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize with authorization error",
|
||||
domainID: validID,
|
||||
clientID: validID,
|
||||
clientType: policies.UserType,
|
||||
channelID: validID,
|
||||
connType: connections.Publish,
|
||||
res: &grpcChannelsV1.AuthzRes{Authorized: false},
|
||||
authzErr: svcerr.ErrAuthorization,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize withnot found error",
|
||||
domainID: validID,
|
||||
clientID: validID,
|
||||
clientType: policies.UserType,
|
||||
channelID: validID,
|
||||
connType: connections.Publish,
|
||||
res: &grpcChannelsV1.AuthzRes{Authorized: false},
|
||||
authzErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
authReq := ch.AuthzReq{
|
||||
DomainID: tc.domainID,
|
||||
ClientID: tc.clientID,
|
||||
ClientType: tc.clientType,
|
||||
ChannelID: tc.channelID,
|
||||
Type: tc.connType,
|
||||
}
|
||||
svcCall := svc.On("Authorize", mock.Anything, authReq).Return(tc.authzErr)
|
||||
res, err := client.Authorize(context.Background(), &grpcChannelsV1.AuthzReq{
|
||||
DomainId: tc.domainID,
|
||||
ClientId: tc.clientID,
|
||||
ClientType: tc.clientType,
|
||||
ChannelId: tc.channelID,
|
||||
Type: uint32(tc.connType),
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.res, res, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.res, res))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClientConnections(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "remove client connections successfully",
|
||||
clientID: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove client connections with error",
|
||||
clientID: validID,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("RemoveClientConnections", mock.Anything, tc.clientID).Return(tc.err)
|
||||
res, err := client.RemoveClientConnections(context.Background(), &grpcChannelsV1.RemoveClientConnectionsReq{
|
||||
ClientId: tc.clientID,
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, &grpcChannelsV1.RemoveClientConnectionsRes{}, res)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsetParentGroupFromChannelsEndpoint(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
parentGroupID string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "unset parent group from channels successfully",
|
||||
parentGroupID: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "unset parent group from channels authorization error",
|
||||
parentGroupID: validID,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "unset parent group from channels with not found error",
|
||||
parentGroupID: validID,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("UnsetParentGroupFromChannels", mock.Anything, tc.parentGroupID).Return(tc.err)
|
||||
res, err := client.UnsetParentGroupFromChannels(context.Background(), &grpcChannelsV1.UnsetParentGroupFromChannelsReq{
|
||||
ParentGroupId: tc.parentGroupID,
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, res)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveEntity(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
svcRes ch.Channel
|
||||
resp *grpcCommonV1.RetrieveEntityRes
|
||||
code codes.Code
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve entity successfully",
|
||||
id: validID,
|
||||
svcRes: validChannel,
|
||||
resp: &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: validChannel.ID,
|
||||
DomainId: validChannel.Domain,
|
||||
ParentGroupId: validChannel.ParentGroup,
|
||||
Status: uint32(validChannel.Status),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity with error",
|
||||
id: validID,
|
||||
resp: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("RetrieveByID", mock.Anything, tc.id).Return(tc.svcRes, tc.err)
|
||||
res, err := client.RetrieveEntity(context.Background(), &grpcCommonV1.RetrieveEntityReq{
|
||||
Id: tc.id,
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.resp.Entity, res.Entity)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveIDByRoute(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
validRoute := "validRoute"
|
||||
domainID := testsutil.GenerateUUID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
retrieveReq *grpcCommonV1.RetrieveIDByRouteReq
|
||||
svcRes string
|
||||
svcErr error
|
||||
retrieveRes *grpcCommonV1.RetrieveEntityRes
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve entity by route successfully",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: validRoute,
|
||||
DomainId: domainID,
|
||||
},
|
||||
svcRes: validID,
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: validID,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity by route with empty route",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: "",
|
||||
DomainId: domainID,
|
||||
},
|
||||
svcRes: "",
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: apiutil.ErrMissingRoute,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity by route with empty domain ID",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: validRoute,
|
||||
DomainId: "",
|
||||
},
|
||||
svcRes: "",
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity by route with invalid route",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: "invalidRoute",
|
||||
DomainId: domainID,
|
||||
},
|
||||
svcRes: "",
|
||||
svcErr: svcerr.ErrNotFound,
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("RetrieveIDByRoute", mock.Anything, tc.retrieveReq.Route, tc.retrieveReq.DomainId).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := client.RetrieveIDByRoute(context.Background(), tc.retrieveReq)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.retrieveRes.Entity, res.Entity)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
)
|
||||
|
||||
var errDomainID = errors.New("domain id required for users")
|
||||
|
||||
type authorizeReq struct {
|
||||
domainID string
|
||||
channelID string
|
||||
clientID string
|
||||
clientType string
|
||||
connType connections.ConnType
|
||||
}
|
||||
|
||||
func (req authorizeReq) validate() error {
|
||||
if req.clientType == policies.UserType && req.domainID == "" {
|
||||
return errDomainID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type removeClientConnectionsReq struct {
|
||||
clientID string
|
||||
}
|
||||
|
||||
type unsetParentGroupFromChannelsReq struct {
|
||||
parentGroupID string
|
||||
}
|
||||
|
||||
type retrieveEntityReq struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
type retrieveIDByRouteReq struct {
|
||||
route string
|
||||
domainID string
|
||||
}
|
||||
|
||||
func (req retrieveIDByRouteReq) validate() error {
|
||||
if req.route == "" {
|
||||
return apiutil.ErrMissingRoute
|
||||
}
|
||||
if req.domainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
type authorizeRes struct {
|
||||
authorized bool
|
||||
}
|
||||
|
||||
type removeClientConnectionsRes struct{}
|
||||
|
||||
type unsetParentGroupFromChannelsRes struct{}
|
||||
|
||||
type channelBasic struct {
|
||||
id string
|
||||
domain string
|
||||
parentGroup string
|
||||
status uint8
|
||||
}
|
||||
|
||||
type retrieveEntityRes channelBasic
|
||||
|
||||
type retrieveIDByRouteRes struct {
|
||||
id string
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
|
||||
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
smqauth "github.com/absmach/magistrala/auth"
|
||||
channels "github.com/absmach/magistrala/channels/private"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ grpcChannelsV1.ChannelsServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
grpcChannelsV1.UnimplementedChannelsServiceServer
|
||||
authorize kitgrpc.Handler
|
||||
removeClientConnections kitgrpc.Handler
|
||||
unsetParentGroupFromChannels kitgrpc.Handler
|
||||
retrieveEntity kitgrpc.Handler
|
||||
retrieveIDByRoute kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new AuthServiceServer instance.
|
||||
func NewServer(svc channels.Service) grpcChannelsV1.ChannelsServiceServer {
|
||||
return &grpcServer{
|
||||
authorize: kitgrpc.NewServer(
|
||||
authorizeEndpoint(svc),
|
||||
decodeAuthorizeRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
removeClientConnections: kitgrpc.NewServer(
|
||||
removeClientConnectionsEndpoint(svc),
|
||||
decodeRemoveClientConnectionsRequest,
|
||||
encodeRemoveClientConnectionsResponse,
|
||||
),
|
||||
unsetParentGroupFromChannels: kitgrpc.NewServer(
|
||||
unsetParentGroupFromChannelsEndpoint(svc),
|
||||
decodeUnsetParentGroupFromChannelsRequest,
|
||||
encodeUnsetParentGroupFromChannelsResponse,
|
||||
),
|
||||
retrieveEntity: kitgrpc.NewServer(
|
||||
retrieveEntityEndpoint(svc),
|
||||
decodeRetrieveEntityRequest,
|
||||
encodeRetrieveEntityResponse,
|
||||
),
|
||||
retrieveIDByRoute: kitgrpc.NewServer(
|
||||
retrieveIDByRouteEndpoint(svc),
|
||||
decodeRetrieveIDByRouteRequest,
|
||||
encodeRetrieveIDByRouteResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) Authorize(ctx context.Context, req *grpcChannelsV1.AuthzReq) (*grpcChannelsV1.AuthzRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcChannelsV1.AuthzRes), nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcChannelsV1.AuthzReq)
|
||||
|
||||
connType := connections.ConnType(req.GetType())
|
||||
if err := connections.CheckConnType(connType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorizeReq{
|
||||
domainID: req.GetDomainId(),
|
||||
clientID: req.GetClientId(),
|
||||
clientType: req.GetClientType(),
|
||||
channelID: req.GetChannelId(),
|
||||
connType: connType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &grpcChannelsV1.AuthzRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) RemoveClientConnections(ctx context.Context, req *grpcChannelsV1.RemoveClientConnectionsReq) (*grpcChannelsV1.RemoveClientConnectionsRes, error) {
|
||||
_, res, err := s.removeClientConnections.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcChannelsV1.RemoveClientConnectionsRes), nil
|
||||
}
|
||||
|
||||
func decodeRemoveClientConnectionsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcChannelsV1.RemoveClientConnectionsReq)
|
||||
|
||||
return removeClientConnectionsReq{
|
||||
clientID: req.GetClientId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRemoveClientConnectionsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
_ = grpcRes.(removeClientConnectionsRes)
|
||||
return &grpcChannelsV1.RemoveClientConnectionsRes{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UnsetParentGroupFromChannels(ctx context.Context, req *grpcChannelsV1.UnsetParentGroupFromChannelsReq) (*grpcChannelsV1.UnsetParentGroupFromChannelsRes, error) {
|
||||
_, res, err := s.unsetParentGroupFromChannels.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcChannelsV1.UnsetParentGroupFromChannelsRes), nil
|
||||
}
|
||||
|
||||
func decodeUnsetParentGroupFromChannelsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcChannelsV1.UnsetParentGroupFromChannelsReq)
|
||||
|
||||
return unsetParentGroupFromChannelsReq{
|
||||
parentGroupID: req.GetParentGroupId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeUnsetParentGroupFromChannelsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
_ = grpcRes.(unsetParentGroupFromChannelsRes)
|
||||
return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq) (*grpcCommonV1.RetrieveEntityRes, error) {
|
||||
_, res, err := s.retrieveEntity.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func decodeRetrieveEntityRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcCommonV1.RetrieveEntityReq)
|
||||
return retrieveEntityReq{
|
||||
Id: req.GetId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRetrieveEntityResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(retrieveEntityRes)
|
||||
|
||||
return &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: res.id,
|
||||
DomainId: res.domain,
|
||||
ParentGroupId: res.parentGroup,
|
||||
Status: uint32(res.status),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeRetrieveIDByRouteRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcCommonV1.RetrieveIDByRouteReq)
|
||||
return retrieveIDByRouteReq{
|
||||
route: req.GetRoute(),
|
||||
domainID: req.GetDomainId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRetrieveIDByRouteResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(retrieveIDByRouteRes)
|
||||
|
||||
return &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: res.id,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) RetrieveIDByRoute(ctx context.Context, req *grpcCommonV1.RetrieveIDByRouteReq) (*grpcCommonV1.RetrieveEntityRes, error) {
|
||||
_, res, err := s.retrieveIDByRoute.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrInvalidAuthKey,
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMalformedPolicyAct:
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrAuthentication),
|
||||
errors.Contains(err, smqauth.ErrKeyExpired),
|
||||
err == apiutil.ErrMissingEmail,
|
||||
err == apiutil.ErrBearerToken:
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrAuthorization):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/internal/nullable"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func decodeViewChannel(_ context.Context, r *http.Request) (any, error) {
|
||||
roles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false)
|
||||
if err != nil {
|
||||
return viewChannelReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
req := viewChannelReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
roles: roles,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCreateChannelReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := createChannelReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Channel); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCreateChannelsReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := createChannelsReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Channels); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListChannels(_ context.Context, r *http.Request) (any, error) {
|
||||
name, err := apiutil.ReadStringQuery(r, api.NameKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
tags, err := apiutil.ReadStringQuery(r, api.TagsKey, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
var tq channels.TagsQuery
|
||||
if tags != "" {
|
||||
tq = channels.ToTagsQuery(tags)
|
||||
}
|
||||
|
||||
s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefGroupStatus)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
status, err := channels.ToStatus(s)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
meta, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
dir, err := apiutil.ReadStringQuery(r, api.DirKey, api.DefDir)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
allActions, err := apiutil.ReadStringQuery(r, api.ActionsKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
actions := []string{}
|
||||
|
||||
allActions = strings.TrimSpace(allActions)
|
||||
if allActions != "" {
|
||||
actions = strings.Split(allActions, ",")
|
||||
}
|
||||
roleID, err := apiutil.ReadStringQuery(r, api.RoleIDKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
roleName, err := apiutil.ReadStringQuery(r, api.RoleNameKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
accessType, err := apiutil.ReadStringQuery(r, api.AccessTypeKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
userID, err := apiutil.ReadStringQuery(r, api.UserKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
groupID, err := nullable.Parse(r.URL.Query(), api.GroupKey, nullable.ParseString)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
clientID, err := apiutil.ReadStringQuery(r, api.ClientKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
id, err := apiutil.ReadStringQuery(r, api.IDOrder, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
ot, err := apiutil.ReadBoolQuery(r, api.OnlyTotal, false)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
connectionType, err := apiutil.ReadStringQuery(r, api.ConnTypeKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
var createdFrom, createdTo time.Time
|
||||
if cfrom != "" {
|
||||
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
|
||||
}
|
||||
}
|
||||
if cto != "" {
|
||||
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
|
||||
}
|
||||
}
|
||||
|
||||
req := listChannelsReq{
|
||||
Page: channels.Page{
|
||||
Name: name,
|
||||
Tags: tq,
|
||||
Status: status,
|
||||
Metadata: meta,
|
||||
RoleName: roleName,
|
||||
RoleID: roleID,
|
||||
Actions: actions,
|
||||
AccessType: accessType,
|
||||
Order: order,
|
||||
Dir: dir,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Group: groupID,
|
||||
Client: clientID,
|
||||
ConnectionType: connectionType,
|
||||
ID: id,
|
||||
OnlyTotal: ot,
|
||||
CreatedFrom: createdFrom,
|
||||
CreatedTo: createdTo,
|
||||
},
|
||||
userID: userID,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateChannelReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateChannelTags(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateChannelTagsReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeSetChannelParentGroupStatus(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := setChannelParentGroupReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRemoveChannelParentGroupStatus(_ context.Context, r *http.Request) (any, error) {
|
||||
req := removeChannelParentGroupReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeChangeChannelStatus(_ context.Context, r *http.Request) (any, error) {
|
||||
req := changeChannelStatusReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDeleteChannelReq(_ context.Context, r *http.Request) (any, error) {
|
||||
req := deleteChannelReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeConnectChannelClientRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
req := connectChannelClientsRequest{
|
||||
channelID: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDisconnectChannelClientsRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
req := disconnectChannelClientsRequest{
|
||||
channelID: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeConnectRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := connectRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDisconnectRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := disconnectRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func createChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(createChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
channels, _, err := svc.CreateChannels(ctx, session, req.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createChannelRes{
|
||||
Channel: channels[0],
|
||||
created: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(createChannelsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
channels, _, err := svc.CreateChannels(ctx, session, req.Channels...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := channelsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: uint64(len(channels)),
|
||||
},
|
||||
Channels: []viewChannelRes{},
|
||||
}
|
||||
for _, c := range channels {
|
||||
res.Channels = append(res.Channels, viewChannelRes{Channel: c})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(viewChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
c, err := svc.ViewChannel(ctx, session, req.id, req.roles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return viewChannelRes{Channel: c}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listChannelsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listChannelsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
var page channels.ChannelsPage
|
||||
var err error
|
||||
switch req.userID != "" {
|
||||
case true:
|
||||
page, err = svc.ListUserChannels(ctx, session, req.userID, req.Page)
|
||||
default:
|
||||
page, err = svc.ListChannels(ctx, session, req.Page)
|
||||
}
|
||||
if err != nil {
|
||||
return channelsPageRes{}, err
|
||||
}
|
||||
|
||||
res := channelsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Channels: []viewChannelRes{},
|
||||
}
|
||||
for _, c := range page.Channels {
|
||||
res.Channels = append(res.Channels, viewChannelRes{Channel: c})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch := channels.Channel{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
ch, err := svc.UpdateChannel(ctx, session, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updateChannelRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelTagsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateChannelTagsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch := channels.Channel{
|
||||
ID: req.id,
|
||||
Tags: req.Tags,
|
||||
}
|
||||
ch, err := svc.UpdateChannelTags(ctx, session, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updateChannelRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func setChannelParentGroupEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(setChannelParentGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.SetParentGroup(ctx, session, req.ParentGroupID, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return setChannelParentGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelParentGroupEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(removeChannelParentGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.RemoveParentGroup(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeChannelParentGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func enableChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeChannelStatusReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch, err := svc.EnableChannel(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changeChannelStatusRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disableChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeChannelStatusReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch, err := svc.DisableChannel(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changeChannelStatusRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func connectChannelClientEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(connectChannelClientsRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Connect(ctx, session, []string{req.channelID}, req.ClientIDs, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connectChannelClientsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectChannelClientsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(disconnectChannelClientsRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(ctx, session, []string{req.channelID}, req.ClientIds, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectChannelClientsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func connectEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(connectRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Connect(ctx, session, req.ChannelIds, req.ClientIds, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connectRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(disconnectRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(ctx, session, req.ChannelIds, req.ClientIds, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(deleteChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.RemoveChannel(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deleteChannelRes{}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
)
|
||||
|
||||
type createChannelReq struct {
|
||||
Channel channels.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if len(req.Channel.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
if req.Channel.ID != "" {
|
||||
if strings.TrimSpace(req.Channel.ID) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
if req.Channel.Route != "" {
|
||||
if err := api.ValidateRoute(req.Channel.Route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.ValidateUUID(req.Channel.Route); err == nil {
|
||||
return apiutil.ErrInvalidRouteFormat
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type createChannelsReq struct {
|
||||
Channels []channels.Channel
|
||||
}
|
||||
|
||||
func (req createChannelsReq) validate() error {
|
||||
if len(req.Channels) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
for _, channel := range req.Channels {
|
||||
if channel.ID != "" {
|
||||
if strings.TrimSpace(channel.ID) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
if len(channel.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
if channel.Route != "" {
|
||||
if err := api.ValidateRoute(channel.Route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.ValidateUUID(channel.Route); err == nil {
|
||||
return apiutil.ErrInvalidRouteFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewChannelReq struct {
|
||||
id string
|
||||
roles bool
|
||||
}
|
||||
|
||||
func (req viewChannelReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listChannelsReq struct {
|
||||
channels.Page
|
||||
userID string
|
||||
}
|
||||
|
||||
func (req listChannelsReq) validate() error {
|
||||
if req.Limit > api.MaxLimitSize || req.Limit < 1 {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
if len(req.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
switch req.Order {
|
||||
case "", api.NameOrder, api.CreatedAtOrder, api.UpdatedAtOrder:
|
||||
default:
|
||||
return apiutil.ErrInvalidOrder
|
||||
}
|
||||
|
||||
if req.Dir != "" && (req.Dir != api.DescDir && req.Dir != api.AscDir) {
|
||||
return apiutil.ErrInvalidDirection
|
||||
}
|
||||
|
||||
if req.ConnectionType != "" {
|
||||
if _, err := connections.ParseConnType(req.ConnectionType); err != nil {
|
||||
return apiutil.ErrValidation
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelReq struct {
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if len(req.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelTagsReq struct {
|
||||
id string
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateChannelTagsReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type setChannelParentGroupReq struct {
|
||||
id string
|
||||
ParentGroupID string `json:"parent_group_id"`
|
||||
}
|
||||
|
||||
func (req setChannelParentGroupReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.ParentGroupID == "" {
|
||||
return apiutil.ErrMissingParentGroupID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type removeChannelParentGroupReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req removeChannelParentGroupReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeChannelStatusReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req changeChannelStatusReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type connectChannelClientsRequest struct {
|
||||
channelID string
|
||||
ClientIDs []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *connectChannelClientsRequest) validate() error {
|
||||
if req.channelID == "" || strings.TrimSpace(req.channelID) == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.ClientIDs) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIDs {
|
||||
if err := api.ValidateUUID(tid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type disconnectChannelClientsRequest struct {
|
||||
channelID string
|
||||
ClientIds []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *disconnectChannelClientsRequest) validate() error {
|
||||
if req.channelID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if err := api.ValidateUUID(req.channelID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.ClientIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIds {
|
||||
if err := api.ValidateUUID(tid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type connectRequest struct {
|
||||
ChannelIds []string `json:"channel_ids,omitempty"`
|
||||
ClientIds []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *connectRequest) validate() error {
|
||||
if len(req.ChannelIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
for _, cid := range req.ChannelIds {
|
||||
if strings.TrimSpace(cid) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.ClientIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIds {
|
||||
if strings.TrimSpace(tid) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type disconnectRequest struct {
|
||||
ChannelIds []string `json:"channel_ids,omitempty"`
|
||||
ClientIds []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *disconnectRequest) validate() error {
|
||||
if len(req.ChannelIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
for _, cid := range req.ChannelIds {
|
||||
if err := api.ValidateUUID(cid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.ClientIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIds {
|
||||
if err := api.ValidateUUID(tid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type deleteChannelReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req deleteChannelReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,628 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateChannelReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req createChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "long name",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "invalid route",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: "__invalid",
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "uuid as route",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
ID: " ",
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrMissingChannelID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelsReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req createChannelsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
Name: valid,
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "long name",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
ID: " ",
|
||||
},
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrMissingChannelID,
|
||||
},
|
||||
{
|
||||
desc: "empty list",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{},
|
||||
},
|
||||
err: apiutil.ErrEmptyList,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannelReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req viewChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: viewChannelReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: viewChannelReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannelsReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req listChannelsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: 10},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "limit is 0",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: 0},
|
||||
},
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "limit is greater than max limit",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: api.MaxLimitSize + 1},
|
||||
},
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "name is too long",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: 10, Name: strings.Repeat("a", api.MaxNameSize+1)},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req updateChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: updateChannelReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: updateChannelReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "name is too long",
|
||||
req: updateChannelReq{
|
||||
id: valid,
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelTagsReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req updateChannelTagsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: updateChannelTagsReq{
|
||||
id: valid,
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: updateChannelTagsReq{
|
||||
id: "",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChannelsParentGroupReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req setChannelParentGroupReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: setChannelParentGroupReq{
|
||||
id: valid,
|
||||
ParentGroupID: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: setChannelParentGroupReq{
|
||||
id: "",
|
||||
ParentGroupID: valid,
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing parent group ID",
|
||||
req: setChannelParentGroupReq{
|
||||
id: valid,
|
||||
ParentGroupID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingParentGroupID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannelParentGroupReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req removeChannelParentGroupReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: removeChannelParentGroupReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: removeChannelParentGroupReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeChannelStatusReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req changeChannelStatusReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: changeChannelStatusReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: changeChannelStatusReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectChannelClientsReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req connectChannelClientsRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: "",
|
||||
ClientIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
{
|
||||
desc: "invalid client ID",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{"client1", "invalid"},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectChannelClientReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req disconnectChannelClientsRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: "",
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid channel ID",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: "invalid",
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
{
|
||||
desc: "invalid client ID",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{"client1", "invalid"},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req connectRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel IDs",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req disconnectRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel IDs",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
{
|
||||
desc: "invalid client ID",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{"client1", "invalid"},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
{
|
||||
desc: "invalid channel ID",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{"invalid", testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteChannelReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req deleteChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: deleteChannelReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: deleteChannelReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
)
|
||||
|
||||
var (
|
||||
_ magistrala.Response = (*createChannelRes)(nil)
|
||||
_ magistrala.Response = (*viewChannelRes)(nil)
|
||||
_ magistrala.Response = (*channelsPageRes)(nil)
|
||||
_ magistrala.Response = (*updateChannelRes)(nil)
|
||||
_ magistrala.Response = (*deleteChannelRes)(nil)
|
||||
_ magistrala.Response = (*connectChannelClientsRes)(nil)
|
||||
_ magistrala.Response = (*disconnectChannelClientsRes)(nil)
|
||||
_ magistrala.Response = (*connectRes)(nil)
|
||||
_ magistrala.Response = (*disconnectRes)(nil)
|
||||
_ magistrala.Response = (*changeChannelStatusRes)(nil)
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
}
|
||||
|
||||
type createChannelRes struct {
|
||||
channels.Channel
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res createChannelRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res createChannelRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/channels/%s", res.ID),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res createChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
channels.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelsPageRes struct {
|
||||
pageRes
|
||||
Channels []viewChannelRes `json:"channels,omitempty"`
|
||||
}
|
||||
|
||||
func (res channelsPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type changeChannelStatusRes struct {
|
||||
channels.Channel
|
||||
}
|
||||
|
||||
func (res changeChannelStatusRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res changeChannelStatusRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res changeChannelStatusRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type updateChannelRes struct {
|
||||
channels.Channel
|
||||
}
|
||||
|
||||
func (res updateChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type setChannelParentGroupRes struct{}
|
||||
|
||||
func (res setChannelParentGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res setChannelParentGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res setChannelParentGroupRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type removeChannelParentGroupRes struct{}
|
||||
|
||||
func (res removeChannelParentGroupRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeChannelParentGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeChannelParentGroupRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type deleteChannelRes struct{}
|
||||
|
||||
func (res deleteChannelRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res deleteChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deleteChannelRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type connectChannelClientsRes struct{}
|
||||
|
||||
func (res connectChannelClientsRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res connectChannelClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectChannelClientsRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectChannelClientsRes struct{}
|
||||
|
||||
func (res disconnectChannelClientsRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectChannelClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectChannelClientsRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type connectRes struct{}
|
||||
|
||||
func (res connectRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res connectRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectRes struct{}
|
||||
|
||||
func (res disconnectRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
roleManagerHttp "github.com/absmach/magistrala/pkg/roles/rolemanager/api"
|
||||
"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"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for Channels API endpoints.
|
||||
func MakeHandler(svc channels.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, logger *slog.Logger, instanceID string, idp magistrala.IDProvider) *chi.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
d := roleManagerHttp.NewDecoder("channelID")
|
||||
|
||||
mux.Route("/{domainID}/channels", func(r chi.Router) {
|
||||
r.Use(authn.Middleware())
|
||||
r.Use(api.RequestIDMiddleware(idp))
|
||||
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
createChannelEndpoint(svc),
|
||||
decodeCreateChannelReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "create_channel").ServeHTTP)
|
||||
|
||||
r.Post("/bulk", otelhttp.NewHandler(kithttp.NewServer(
|
||||
createChannelsEndpoint(svc),
|
||||
decodeCreateChannelsReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "create_channels").ServeHTTP)
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listChannelsEndpoint(svc),
|
||||
decodeListChannels,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "list_channels").ServeHTTP)
|
||||
|
||||
r.Post("/connect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
connectEndpoint(svc),
|
||||
decodeConnectRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "connect").ServeHTTP)
|
||||
|
||||
r.Post("/disconnect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disconnectEndpoint(svc),
|
||||
decodeDisconnectRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disconnect").ServeHTTP)
|
||||
|
||||
r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts)
|
||||
|
||||
r.Route("/{channelID}", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewChannelEndpoint(svc),
|
||||
decodeViewChannel,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "view_channel").ServeHTTP)
|
||||
|
||||
r.Patch("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateChannelEndpoint(svc),
|
||||
decodeUpdateChannel,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "update_channel_name_and_metadata").ServeHTTP)
|
||||
|
||||
r.Patch("/tags", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateChannelTagsEndpoint(svc),
|
||||
decodeUpdateChannelTags,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "update_channel_tag").ServeHTTP)
|
||||
|
||||
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
deleteChannelEndpoint(svc),
|
||||
decodeDeleteChannelReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "delete_channel").ServeHTTP)
|
||||
|
||||
r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer(
|
||||
enableChannelEndpoint(svc),
|
||||
decodeChangeChannelStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "enable_channel").ServeHTTP)
|
||||
|
||||
r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disableChannelEndpoint(svc),
|
||||
decodeChangeChannelStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disable_channel").ServeHTTP)
|
||||
|
||||
r.Post("/parent", otelhttp.NewHandler(kithttp.NewServer(
|
||||
setChannelParentGroupEndpoint(svc),
|
||||
decodeSetChannelParentGroupStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "set_channel_parent_group").ServeHTTP)
|
||||
|
||||
r.Delete("/parent", otelhttp.NewHandler(kithttp.NewServer(
|
||||
removeChannelParentGroupEndpoint(svc),
|
||||
decodeRemoveChannelParentGroupStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "remove_channel_parent_group").ServeHTTP)
|
||||
|
||||
r.Post("/connect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
connectChannelClientEndpoint(svc),
|
||||
decodeConnectChannelClientRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "connect_channel_client").ServeHTTP)
|
||||
|
||||
r.Post("/disconnect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disconnectChannelClientsEndpoint(svc),
|
||||
decodeDisconnectChannelClientsRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disconnect_channel_client").ServeHTTP)
|
||||
|
||||
roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts)
|
||||
})
|
||||
})
|
||||
|
||||
mux.Get("/health", magistrala.Health("channels", instanceID))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user