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

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:
Arvindh
2026-06-27 01:36:57 +05:30
committed by GitHub
parent 19ed0de788
commit 16ba29cf4a
611 changed files with 10691 additions and 189916 deletions
+51 -48
View File
@@ -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"
+3 -11
View File
@@ -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)
+11 -44
View File
@@ -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
+6
View File
@@ -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
+87 -18
View File
@@ -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)
+133 -49
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+97
View File
@@ -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)
}
+83
View File
@@ -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
}
+2 -1
View File
@@ -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 {
+37 -12
View File
@@ -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,
+109
View File
@@ -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
}
+2 -2
View File
@@ -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())
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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),
-72
View File
@@ -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
View File
@@ -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
}
-29
View File
@@ -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 {
-209
View File
@@ -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")
-10
View File
@@ -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
View File
@@ -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 {
+2 -2
View File
@@ -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
View File
@@ -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)
+12
View File
@@ -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,
+2
View File
@@ -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:
-401
View File
@@ -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()
})
}
}
-24
View File
@@ -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)
}
-245
View File
@@ -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()
}
}
-24
View File
@@ -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)
}
+1 -1
View File
@@ -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}
}
+1 -1
View File
@@ -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,
-376
View File
@@ -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")
}
-122
View File
@@ -1,122 +0,0 @@
# BOOTSTRAP SERVICE
New devices need to be configured properly and connected to the Magistrala. Bootstrap service is used in order to accomplish that. This service provides the following features:
1. Creating new Magistrala Clients
2. Providing basic configuration for the newly created Clients
3. Enabling/disabling 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).
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package api contains implementation of bootstrap service HTTP API.
package api
-506
View File
@@ -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
-280
View File
@@ -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
}
-245
View File
@@ -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))
}
}
-223
View File
@@ -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 }
-512
View File
@@ -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
}
-109
View File
@@ -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
}
-81
View File
@@ -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
}
-73
View File
@@ -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
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package bootstrap contains the domain concept definitions needed to support
// Magistrala bootstrap service functionality.
package bootstrap
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package events provides the domain concept definitions needed to support
// bootstrap events functionality.
package events
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package producer contains the domain events needed to support
// event sourcing of Bootstrap service actions.
package producer
-288
View File
@@ -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
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package producer_test
import (
"context"
"fmt"
"log"
"os"
"testing"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/redis/go-redis/v9"
)
var (
redisClient *redis.Client
redisURL string
)
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
container, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "redis",
Tag: "7.2.4-alpine",
}, func(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
})
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
redisURL = fmt.Sprintf("redis://localhost:%s/0", container.GetPort("6379/tcp"))
opts, err := redis.ParseURL(redisURL)
if err != nil {
log.Fatalf("Could not parse redis URL: %s", err)
}
if err := pool.Retry(func() error {
redisClient = redis.NewClient(opts)
return redisClient.Ping(context.Background()).Err()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
code := m.Run()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}
-284
View File
@@ -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)
}
-914
View File
@@ -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))
}
}
-14
View File
@@ -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
}
-94
View File
@@ -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
}
-219
View File
@@ -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
}
-355
View File
@@ -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)
}
-192
View File
@@ -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)
}
-111
View File
@@ -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
}
-237
View File
@@ -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
}
-109
View File
@@ -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
}
-670
View File
@@ -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
}
-394
View File
@@ -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
}
-115
View File
@@ -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
-146
View File
@@ -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
}
-420
View File
@@ -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
}
-471
View File
@@ -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")
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package postgres contains repository implementations using PostgreSQL as
// the underlying database.
package postgres
-329
View File
@@ -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`,
},
},
},
}
}
-263
View File
@@ -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
}
-88
View File
@@ -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)
}
-69
View File
@@ -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
}
-80
View File
@@ -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
}
-102
View File
@@ -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.")
}
}
-174
View File
@@ -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
},
}
}
-88
View File
@@ -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))
})
}
}
-115
View File
@@ -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
}
-100
View File
@@ -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
}
-532
View File
@@ -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
-101
View File
@@ -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
}
-12
View File
@@ -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
-198
View File
@@ -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)
}
+1 -1
View File
@@ -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}
}
+1 -1
View File
@@ -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,
-214
View File
@@ -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 channels 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..."
}
```
-224
View File
@@ -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
}
-5
View File
@@ -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
-85
View File
@@ -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
}
}
-345
View File
@@ -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()
})
}
}
-56
View File
@@ -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
}
-25
View File
@@ -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
}
-211
View File
@@ -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())
}
}
-329
View File
@@ -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
-364
View File
@@ -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
}
}
-321
View File
@@ -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
}
-628
View File
@@ -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))
}
}
-221
View File
@@ -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
}
-149
View File
@@ -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