mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2320ec96c0 | |||
| 14d6db9684 | |||
| 513a84361b | |||
| 6ed7ca6a42 | |||
| 52645e2c39 | |||
| e078e00f73 | |||
| c3b9c20cc0 | |||
| 36fb079563 | |||
| 4d20126e75 | |||
| 09a56575c1 | |||
| 3b5f646719 | |||
| ededf5308c | |||
| bc7c9096f9 | |||
| c405028fc5 | |||
| cdc065a7b2 | |||
| 3cf6b2f0f4 | |||
| afd2665c9c | |||
| 45c426c8bf | |||
| 16ba29cf4a | |||
| 19ed0de788 | |||
| 7efeb875de | |||
| 935a0950f9 | |||
| d4f0d8fdef | |||
| aaa718bdf6 | |||
| 91e010128d | |||
| 5d7d3d412b | |||
| cb364d0426 | |||
| b68c9fe79e | |||
| 0d5048941e | |||
| 24edbe6ad8 | |||
| 610da72779 | |||
| 90672e5fc7 | |||
| 5821d2a513 | |||
| 49488738df | |||
| 493073ae49 | |||
| 5a528dd138 | |||
| 377b8dfc08 | |||
| 03b33fee9e | |||
| af75ac730c | |||
| 70d879275a | |||
| 353e050a39 | |||
| 683809dc6b | |||
| f380c8d360 | |||
| 78804278d4 | |||
| 426532099a | |||
| 7f03134d8e | |||
| f736bca7c1 | |||
| d840aeb1b9 | |||
| a0bc7c2108 | |||
| df242f6179 | |||
| 3f256ddcf6 |
@@ -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
|
||||
@@ -74,7 +72,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -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,75 +165,80 @@ 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
|
||||
base-url: ${{ env.USERS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-users
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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
|
||||
base-url: ${{ env.GROUPS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-groups
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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
|
||||
base-url: ${{ env.CLIENTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-clients
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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
|
||||
base-url: ${{ env.CHANNELS_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 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'
|
||||
coverage-artifact-name: schema-coverage-channels
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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
|
||||
base-url: ${{ env.DOMAIN_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-domains
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Journal API tests
|
||||
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -235,24 +248,30 @@ jobs:
|
||||
base-url: ${{ env.JOURNAL_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-journal
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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
|
||||
base-url: ${{ env.BOOTSTRAP_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-bootstrap
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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
|
||||
base-url: ${{ env.CERTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-certs
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Readers API tests
|
||||
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -262,6 +281,8 @@ jobs:
|
||||
base-url: ${{ env.READERS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-readers
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Rules Engine API tests
|
||||
if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -271,6 +292,8 @@ jobs:
|
||||
base-url: ${{ env.RE_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-rules
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Alarms API tests
|
||||
if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -280,6 +303,8 @@ jobs:
|
||||
base-url: ${{ env.ALARMS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-alarms
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Reports API tests
|
||||
if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -289,7 +314,9 @@ jobs:
|
||||
base-url: ${{ env.REPORTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-reports
|
||||
coverage-pr-comment: false
|
||||
|
||||
- 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"
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
PROTOC_GEN_GO_GRPC_VERSION: "v1.6.0"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -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"
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Check License Header
|
||||
run: |
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
uses: golangci/golangci-lint-action@v9.3.0
|
||||
with:
|
||||
version: v2.10.1
|
||||
args: --config ./tools/config/.golangci.yaml
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -45,20 +45,12 @@ 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@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -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)
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build Swagger UI
|
||||
run: |
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
modules: ${{ steps.set-matrix.outputs.modules }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -66,21 +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/**"
|
||||
|
||||
channels:
|
||||
- "channels/**"
|
||||
- "cmd/channels/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
- "clients/api/grpc/**"
|
||||
- "groups/api/grpc/**"
|
||||
@@ -94,10 +81,6 @@ jobs:
|
||||
|
||||
clients:
|
||||
- "clients/**"
|
||||
- "cmd/clients/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
@@ -108,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/**"
|
||||
@@ -133,9 +108,6 @@ jobs:
|
||||
journal:
|
||||
- "journal/**"
|
||||
- "cmd/journal/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
logger:
|
||||
@@ -158,7 +130,6 @@ jobs:
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/errors/**"
|
||||
- "pkg/groups/**"
|
||||
- "auth/**"
|
||||
- "internal/*"
|
||||
- "clients/**"
|
||||
- "users/**"
|
||||
@@ -182,10 +153,6 @@ jobs:
|
||||
|
||||
users:
|
||||
- "users/**"
|
||||
- "cmd/users/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
@@ -193,8 +160,6 @@ jobs:
|
||||
notifications:
|
||||
- "notifications/**"
|
||||
- "cmd/notifications/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "consumers/notifier.go"
|
||||
- "pkg/events/**"
|
||||
|
||||
@@ -281,7 +246,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -306,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
|
||||
@@ -325,7 +300,7 @@ jobs:
|
||||
if: always() && needs.run-tests.result != 'cancelled'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -335,7 +310,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
directory: ./coverage
|
||||
|
||||
@@ -19,5 +19,11 @@ coverage
|
||||
# Ignore Openbao data directory as it contains runtime-generated data
|
||||
docker/addons/certs/openbao/
|
||||
|
||||
# Ignore generated local Atom service tokens.
|
||||
docker/.env.tokens
|
||||
|
||||
# Ignore SeaweedFS data directory as it contains runtime-generated data
|
||||
docker/data/*
|
||||
|
||||
demo-ui
|
||||
node_modules
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
override MG_DOCKER_IMAGE_NAME_PREFIX := ghcr.io/absmach/magistrala
|
||||
MG_DOCKER_VOLUME_NAME_PREFIX ?= magistrala
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = auth users clients groups channels domains notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports bootstrap 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_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 migrate_atom 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,25 @@ 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:
|
||||
@:
|
||||
|
||||
# Migrate an old Magistrala (v0.30.0 / pre-Atom) deployment into Atom. Runs an
|
||||
# isolated, collision-free stack, seeds the Atom schema into the run_latest Atom
|
||||
# volume and loads the data. Default is a dry-run; pass args="--apply" to load,
|
||||
# args="--verify" to reconcile afterwards.
|
||||
# make migrate_atom # dry-run
|
||||
# make migrate_atom args="--apply" # perform the migration
|
||||
# make migrate_atom args="--apply --fresh-atom" # rebuild Atom schema first
|
||||
migrate_atom:
|
||||
DOCKER_PROJECT="$(DOCKER_PROJECT)" tools/atom-migration/migrate.sh $(args)
|
||||
|
||||
check_tls:
|
||||
ifeq ($(GRPC_TLS),true)
|
||||
@@ -284,27 +354,47 @@ 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)
|
||||
@if [ "$(or $(letsencrypt),true)" != "false" ] && [ -z "$(email)" ]; then echo "Usage: make run_tls host=example.com email=admin@example.com [letsencrypt=false] [staging=true] [force=true]"; exit 2; fi
|
||||
MG_PUBLIC_HOST="$(host)" \
|
||||
MG_LETSENCRYPT_ENABLED="$(or $(letsencrypt),true)" \
|
||||
MG_LETSENCRYPT_EMAIL="$(email)" \
|
||||
MG_LETSENCRYPT_STAGING="$(or $(staging),false)" \
|
||||
MG_LETSENCRYPT_FORCE_RENEWAL="$(or $(force),false)" \
|
||||
DOCKER_PROJECT="$(DOCKER_PROJECT)" \
|
||||
./docker/setup-tls.sh
|
||||
|
||||
run_stable: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(version)/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_addons: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) up -d atom jaeger
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) $(DOCKER_ENV_FILES) $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
done
|
||||
|
||||
run_live: check_certs
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
$(call require_atom_tokens_env)
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
@@ -46,7 +46,7 @@ It is extremely flexible and lets you build systems the way you want — from si
|
||||
|
||||
At the same time, it avoids the typical complexity of many IoT platforms, where you need to learn an entirely new set of concepts before you can even get started.
|
||||
|
||||
Magistrala is built around a small number of core concepts:
|
||||
Magistrala is built around a small number of main concepts:
|
||||
- users
|
||||
- clients (devices)
|
||||
- channels
|
||||
@@ -141,6 +141,140 @@ 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.
|
||||
|
||||
Current Docker deployments use the Atom image configured by `ATOM_IMAGE` in `docker/.env`. For compatibility with the current Magistrala integration, the generated `MG_ATOM_TOKEN_*` service credentials are unscoped Atom access tokens. Scoped Atom access tokens should not be used for these service env vars until Magistrala stops using owner-wide Atom listing APIs such as `authorizedObjectIds` in service policy paths.
|
||||
|
||||
### 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
|
||||
|
||||
+7
-11
@@ -26,16 +26,12 @@ 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_SERVICE_TOKEN` | Atom service token for authorization checks | "" |
|
||||
| `ATOM_ADMIN_USERNAME` | Atom admin login fallback when no service token is configured | `atom-admin` |
|
||||
| `ATOM_ADMIN_SECRET` | Atom admin secret fallback when no service token is configured | `change-me` |
|
||||
| `ATOM_TIMEOUT` | Atom request timeout | `5s` |
|
||||
| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` |
|
||||
|
||||
## Features
|
||||
@@ -44,7 +40,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 while alarm records stay in PostgreSQL.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
+2
-2
@@ -57,6 +57,7 @@ type PageMetadata struct {
|
||||
Limit uint64 `json:"limit" db:"limit"`
|
||||
DomainID string `json:"domain_id" db:"domain_id"`
|
||||
RuleID string `json:"rule_id" db:"rule_id"`
|
||||
RuleIDs []string `json:"rule_ids" db:"rule_ids"`
|
||||
ChannelID string `json:"channel_id" db:"channel_id"`
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
Subtopic string `json:"subtopic" db:"subtopic"`
|
||||
@@ -106,7 +107,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 +119,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
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ func (h handler) Handle(msg *messaging.Message) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.svc.CreateAlarm(context.Background(), alarm)
|
||||
_, err = h.svc.CreateAlarm(context.Background(), alarm)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h handler) Cancel() error {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/operations"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/internal/atom"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
smqauthz "github.com/absmach/magistrala/pkg/authz"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
@@ -26,11 +27,22 @@ var (
|
||||
type authorizationMiddleware struct {
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
atomAuthz atom.Authorizer
|
||||
entitiesOps permissions.EntitiesOperations[permissions.Operation]
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
const (
|
||||
atomObjectKindResource = "resource"
|
||||
atomObjectTypeResourceRule = "resource:" + atom.KindRule
|
||||
atomAuthorizedRulePageLimit = 100
|
||||
)
|
||||
|
||||
type atomAuthorizedObjectLister interface {
|
||||
AuthorizedObjectIDs(ctx context.Context, q atom.AuthorizedObjectIDsQuery) (atom.AuthorizedObjectIDs, error)
|
||||
}
|
||||
|
||||
func NewAuthorizationMiddleware(svc alarms.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (alarms.Service, error) {
|
||||
if err := entitiesOps.Validate(); err != nil {
|
||||
return nil, err
|
||||
@@ -43,43 +55,62 @@ 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)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
current, err := am.svc.ViewAlarm(ctx, session, alarm.ID)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
}
|
||||
|
||||
if len(alarm.Metadata) > 0 {
|
||||
if err := am.authorize(ctx, operations.OpUpdateAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
if err := am.authorizeAlarmOrRule(ctx, operations.OpUpdateAlarm, session, current); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.AssigneeID != "" {
|
||||
if err := am.authorize(ctx, operations.OpAssignAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
if err := am.authorizeAlarmOrRule(ctx, operations.OpAssignAlarm, session, current); 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.AcknowledgedBy != "" {
|
||||
if err := am.authorize(ctx, operations.OpAcknowledgeAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
if err := am.authorizeAlarmOrRule(ctx, operations.OpAcknowledgeAlarm, session, current); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.ResolvedBy != "" {
|
||||
if err := am.authorize(ctx, operations.OpResolveAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
if err := am.authorizeAlarmOrRule(ctx, operations.OpResolveAlarm, session, current); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +119,11 @@ func (am *authorizationMiddleware) UpdateAlarm(ctx context.Context, session auth
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
if err := am.authorize(ctx, operations.OpDeleteAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
alarm, err := am.svc.ViewAlarm(ctx, session, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := am.authorizeAlarmOrRule(ctx, operations.OpDeleteAlarm, session, alarm); err != nil {
|
||||
return errors.Wrap(errDomainDeleteAlarms, err)
|
||||
}
|
||||
|
||||
@@ -104,6 +139,26 @@ func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn
|
||||
case err == nil:
|
||||
session.SuperAdmin = true
|
||||
case errors.Contains(err, svcerr.ErrSuperAdminAction):
|
||||
if err := am.authorizeTenantAlarm(ctx, operations.OpViewAlarm, session); err != nil {
|
||||
if pm.RuleID != "" {
|
||||
if ruleErr := am.authorizeRuleAlarmRead(ctx, session, pm.RuleID); ruleErr != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
ruleIDs, ruleErr := am.authorizedReadableRuleIDs(ctx, session)
|
||||
if ruleErr != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
if len(ruleIDs) == 0 {
|
||||
return alarms.AlarmsPage{
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Alarms: []alarms.Alarm{},
|
||||
}, nil
|
||||
}
|
||||
pm.RuleIDs = ruleIDs
|
||||
}
|
||||
default:
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
@@ -112,18 +167,110 @@ func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
if err := am.authorize(ctx, operations.OpViewAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
alarm, err := am.svc.ViewAlarm(ctx, session, id)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
}
|
||||
if err := am.authorizeViewAlarm(ctx, session, alarm); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.ViewAlarm(ctx, session, id)
|
||||
return alarm, nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error {
|
||||
func (am *authorizationMiddleware) authorizeAlarmOrRule(ctx context.Context, op permissions.Operation, session authn.Session, alarm alarms.Alarm) error {
|
||||
tenantErr := am.authorizeTenantAlarm(ctx, op, session)
|
||||
if tenantErr == nil {
|
||||
return nil
|
||||
}
|
||||
if alarm.RuleID == "" {
|
||||
return tenantErr
|
||||
}
|
||||
if err := am.authorize(ctx, op, session, policies.RulesType, alarm.RuleID, atom.KindRule); err != nil {
|
||||
return tenantErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorizeViewAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) error {
|
||||
tenantErr := am.authorizeTenantAlarm(ctx, operations.OpViewAlarm, session)
|
||||
if tenantErr == nil {
|
||||
return nil
|
||||
}
|
||||
if alarm.RuleID == "" {
|
||||
return tenantErr
|
||||
}
|
||||
if err := am.authorizeRuleAlarmRead(ctx, session, alarm.RuleID); err != nil {
|
||||
return tenantErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorizeTenantAlarm(ctx context.Context, op permissions.Operation, session authn.Session) error {
|
||||
return am.authorize(ctx, op, session, policies.DomainType, session.DomainID, atom.KindAlarm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorizeRuleAlarmRead(ctx context.Context, session authn.Session, ruleID string) error {
|
||||
if am.atomAuthz != nil {
|
||||
return am.authorize(ctx, operations.OpViewAlarm, session, policies.RulesType, ruleID, atom.KindRule)
|
||||
}
|
||||
perm, err := am.entitiesOps.GetPermission(operations.EntityType, operations.OpViewAlarm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pr := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Object: ruleID,
|
||||
ObjectType: policies.RulesType,
|
||||
Permission: perm.String(),
|
||||
}
|
||||
return am.authz.Authorize(ctx, pr, nil)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorizedReadableRuleIDs(ctx context.Context, session authn.Session) ([]string, error) {
|
||||
lister, ok := am.atomAuthz.(atomAuthorizedObjectLister)
|
||||
if !ok {
|
||||
return nil, errors.ErrAuthorization
|
||||
}
|
||||
perm, err := am.entitiesOps.GetPermission(operations.EntityType, operations.OpViewAlarm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ids []string
|
||||
for offset := uint64(0); ; offset += atomAuthorizedRulePageLimit {
|
||||
page, err := lister.AuthorizedObjectIDs(ctx, atom.AuthorizedObjectIDsQuery{
|
||||
SubjectID: atom.SubjectID(session),
|
||||
Action: atom.CapabilityName(perm.String()),
|
||||
ObjectKind: atomObjectKindResource,
|
||||
ObjectType: atomObjectTypeResourceRule,
|
||||
TenantID: session.DomainID,
|
||||
Limit: atomAuthorizedRulePageLimit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids = append(ids, page.IDs...)
|
||||
if uint64(len(page.IDs)) < atomAuthorizedRulePageLimit || offset+uint64(len(page.IDs)) >= page.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj, resourceKind string) error {
|
||||
perm, err := am.entitiesOps.GetPermission(operations.EntityType, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if am.atomAuthz != nil {
|
||||
return atom.Authorize(ctx, am.atomAuthz, session, perm.String(), objType, obj, resourceKind)
|
||||
}
|
||||
|
||||
pr := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
@@ -159,6 +306,9 @@ func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session
|
||||
if session.Role != authn.SuperAdminRole {
|
||||
return svcerr.ErrSuperAdminAction
|
||||
}
|
||||
if am.atomAuthz != nil {
|
||||
return atom.Authorize(ctx, am.atomAuthz, session, policies.AdminPermission, policies.PlatformType, policies.MagistralaObject, policies.PlatformType)
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: session.UserID,
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// 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/internal/atom"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"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
|
||||
allow func(atom.AuthzRequest) bool
|
||||
authorized atom.AuthorizedObjectIDs
|
||||
reqs []atom.AuthzRequest
|
||||
queries []atom.AuthorizedObjectIDsQuery
|
||||
}
|
||||
|
||||
func (a *recordingAtomAuthorizer) CheckAuthz(_ context.Context, req atom.AuthzRequest) (atom.AuthzResponse, error) {
|
||||
a.reqs = append(a.reqs, req)
|
||||
if a.allow != nil {
|
||||
return atom.AuthzResponse{Allowed: a.allow(req)}, nil
|
||||
}
|
||||
return atom.AuthzResponse{Allowed: a.allowed}, nil
|
||||
}
|
||||
|
||||
func (a *recordingAtomAuthorizer) AuthorizedObjectIDs(_ context.Context, q atom.AuthorizedObjectIDsQuery) (atom.AuthorizedObjectIDs, error) {
|
||||
a.queries = append(a.queries, q)
|
||||
return a.authorized, nil
|
||||
}
|
||||
|
||||
func TestListAlarmsAuthorizesTenantAlarmReader(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: "alarm_read",
|
||||
ResourceID: "",
|
||||
ObjectKind: "tenant",
|
||||
ObjectID: "domain-1",
|
||||
Context: map[string]any{
|
||||
"domain_id": "domain-1",
|
||||
"legacy_object_type": "domain",
|
||||
},
|
||||
}, authz.reqs[0])
|
||||
}
|
||||
|
||||
func TestListAlarmsFiltersToReadableRulesWhenTenantAlarmReadDenied(t *testing.T) {
|
||||
svc := mocks.NewService(t)
|
||||
pm := alarms.PageMetadata{Limit: 10}
|
||||
expectedPM := pm
|
||||
expectedPM.DomainID = "domain-1"
|
||||
expectedPM.RuleIDs = []string{"rule-1", "rule-2"}
|
||||
authz := &recordingAtomAuthorizer{
|
||||
allowed: false,
|
||||
authorized: atom.AuthorizedObjectIDs{IDs: []string{"rule-1", "rule-2"}, Total: 2},
|
||||
}
|
||||
wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
svc.On("ListAlarms", mock.Anything, authn.Session{UserID: "user-1", DomainID: "domain-1"}, expectedPM).Return(alarms.AlarmsPage{Limit: 10}, nil).Once()
|
||||
_, err = wrapped.ListAlarms(context.Background(), authn.Session{UserID: "user-1", DomainID: "domain-1"}, pm)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, authz.reqs, 1)
|
||||
assert.Equal(t, "alarm_read", authz.reqs[0].Action)
|
||||
assert.Equal(t, "tenant", authz.reqs[0].ObjectKind)
|
||||
require.Len(t, authz.queries, 1)
|
||||
assert.Equal(t, atom.AuthorizedObjectIDsQuery{
|
||||
SubjectID: "user-1",
|
||||
Action: "alarm_read",
|
||||
ObjectKind: "resource",
|
||||
ObjectType: "resource:rule",
|
||||
TenantID: "domain-1",
|
||||
Limit: 100,
|
||||
}, authz.queries[0])
|
||||
}
|
||||
|
||||
func TestListAlarmsWithRuleFilterAuthorizesRuleRead(t *testing.T) {
|
||||
svc := mocks.NewService(t)
|
||||
pm := alarms.PageMetadata{Limit: 10, RuleID: "rule-1"}
|
||||
expectedPM := pm
|
||||
expectedPM.DomainID = "domain-1"
|
||||
session := authn.Session{UserID: "user-1", DomainID: "domain-1"}
|
||||
authz := &recordingAtomAuthorizer{
|
||||
allow: func(req atom.AuthzRequest) bool {
|
||||
return req.Action == "alarm_read" && req.ObjectKind == "resource" && req.ObjectID == "rule-1"
|
||||
},
|
||||
}
|
||||
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()
|
||||
_, err = wrapped.ListAlarms(context.Background(), session, pm)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, authz.reqs, 2)
|
||||
assert.Equal(t, "alarm_read", authz.reqs[0].Action)
|
||||
assert.Equal(t, "alarm_read", authz.reqs[1].Action)
|
||||
assert.Equal(t, "resource", authz.reqs[1].ObjectKind)
|
||||
assert.Equal(t, "rules", authz.reqs[1].Context["legacy_object_type"])
|
||||
}
|
||||
|
||||
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 TestAcknowledgeAlarmAuthorizesRuleAlarmActionWhenTenantDenied(t *testing.T) {
|
||||
svc := mocks.NewService(t)
|
||||
session := authn.Session{UserID: "user-1", DomainID: "domain-1"}
|
||||
current := alarms.Alarm{ID: "alarm-1", RuleID: "rule-1", DomainID: "domain-1"}
|
||||
update := alarms.Alarm{ID: "alarm-1", AcknowledgedBy: "user-1"}
|
||||
authz := &recordingAtomAuthorizer{
|
||||
allow: func(req atom.AuthzRequest) bool {
|
||||
return req.Action == "alarm_acknowledge" && req.ObjectKind == "resource" && req.ObjectID == "rule-1"
|
||||
},
|
||||
}
|
||||
wrapped, err := NewAtomAuthorizationMiddleware(svc, authz, testEntitiesOps(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
svc.On("ViewAlarm", mock.Anything, session, "alarm-1").Return(current, nil).Once()
|
||||
svc.On("UpdateAlarm", mock.Anything, session, update).Return(update, nil).Once()
|
||||
_, err = wrapped.UpdateAlarm(context.Background(), session, update)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, authz.reqs, 2)
|
||||
assert.Equal(t, atom.AuthzRequest{
|
||||
SubjectID: "user-1",
|
||||
Action: "alarm_acknowledge",
|
||||
ResourceID: "",
|
||||
ObjectKind: "tenant",
|
||||
ObjectID: "domain-1",
|
||||
Context: map[string]any{
|
||||
"domain_id": "domain-1",
|
||||
"legacy_object_type": "domain",
|
||||
},
|
||||
}, authz.reqs[0])
|
||||
assert.Equal(t, atom.AuthzRequest{
|
||||
SubjectID: "user-1",
|
||||
Action: "alarm_acknowledge",
|
||||
ResourceID: "rule-1",
|
||||
ObjectKind: "resource",
|
||||
ObjectID: "rule-1",
|
||||
Context: map[string]any{
|
||||
"domain_id": "domain-1",
|
||||
"legacy_object_type": "rules",
|
||||
},
|
||||
}, authz.reqs[1])
|
||||
}
|
||||
|
||||
func testEntitiesOps(t *testing.T) permissions.EntitiesOperations[permissions.Operation] {
|
||||
t.Helper()
|
||||
details := operations.OperationDetails()
|
||||
perms := make(map[string]permissions.Permission, len(details))
|
||||
for op, detail := range details {
|
||||
if detail.PermissionRequired {
|
||||
perms[detail.Name] = testPermission(op, detail.Name)
|
||||
}
|
||||
}
|
||||
entitiesOps, err := permissions.NewEntitiesOperations(
|
||||
permissions.EntitiesPermission{operations.EntityType: perms},
|
||||
permissions.EntitiesOperationDetails[permissions.Operation]{operations.EntityType: details},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return entitiesOps
|
||||
}
|
||||
|
||||
func testPermission(op permissions.Operation, fallback string) permissions.Permission {
|
||||
switch op {
|
||||
case operations.OpViewAlarm, operations.OpListAlarms:
|
||||
return "alarm_read_permission"
|
||||
case operations.OpUpdateAlarm:
|
||||
return "alarm_update_permission"
|
||||
case operations.OpDeleteAlarm:
|
||||
return "alarm_delete_permission"
|
||||
case operations.OpAssignAlarm:
|
||||
return "alarm_assign_permission"
|
||||
case operations.OpAcknowledgeAlarm:
|
||||
return "alarm_acknowledge_permission"
|
||||
case operations.OpResolveAlarm:
|
||||
return "alarm_resolve_permission"
|
||||
default:
|
||||
return permissions.Permission(fallback)
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func NewLoggingMiddleware(logger *slog.Logger, service alarms.Service) alarms.Se
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (err error) {
|
||||
func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (created alarms.Alarm, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
@@ -52,7 +52,7 @@ func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm
|
||||
lm.logger.Warn("Create alarm failed", args...)
|
||||
return
|
||||
}
|
||||
if alarm.ID != "" {
|
||||
if created.ID != "" {
|
||||
lm.logger.Info("Create alarm completed successfully", args...)
|
||||
}
|
||||
}(time.Now())
|
||||
|
||||
@@ -28,7 +28,7 @@ func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, se
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "create_alarm").Add(1)
|
||||
mm.latency.With("method", "create_alarm").Observe(time.Since(begin).Seconds())
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewTracingMiddleware(tracer trace.Tracer, svc alarms.Service) alarms.Servic
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "create_alarm", trace.WithAttributes(
|
||||
attribute.String("rule_id", alarm.RuleID),
|
||||
attribute.String("measurement", alarm.Measurement),
|
||||
|
||||
@@ -231,78 +231,6 @@ func (_c *Repository_ListAllAlarms_Call) RunAndReturn(run func(ctx context.Conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, userID, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, userID, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListUserAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserAlarms'
|
||||
type Repository_ListUserAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListUserAlarms(ctx interface{}, userID interface{}, pm interface{}) *Repository_ListUserAlarms_Call {
|
||||
return &Repository_ListUserAlarms_Call{Call: _e.mock.On("ListUserAlarms", ctx, userID, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Run(run func(ctx context.Context, userID string, pm alarms.PageMetadata)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 alarms.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) RunAndReturn(run func(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
+17
-8
@@ -44,20 +44,29 @@ func (_m *Service) EXPECT() *Service_Expecter {
|
||||
}
|
||||
|
||||
// CreateAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) error); ok {
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
return r0
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm'
|
||||
@@ -90,12 +99,12 @@ func (_c *Service_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alar
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) Return(err error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
func (_c *Service_CreateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) error) *Service_CreateAlarm_Call {
|
||||
func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -198,35 +198,6 @@ func (r *repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata)
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
clauses := []string{
|
||||
`(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM rules_roles rr
|
||||
JOIN rules_role_members rrm ON rrm.role_id = rr.id
|
||||
WHERE rr.entity_id = alarms.rule_id AND rrm.member_id = :user_id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM domains_roles dr
|
||||
JOIN domains_role_members drm ON drm.role_id = dr.id
|
||||
JOIN domains_role_actions dra ON dra.role_id = dr.id
|
||||
WHERE dr.entity_id = alarms.domain_id
|
||||
AND drm.member_id = :user_id
|
||||
AND dra.action LIKE 'alarm%'
|
||||
)
|
||||
)`,
|
||||
}
|
||||
|
||||
clauses = append(clauses, pageQueryConditions(pm)...)
|
||||
query := fmt.Sprintf("WHERE %s", strings.Join(clauses, " AND "))
|
||||
pm.UserID = userID
|
||||
comQuery := fmt.Sprintf(`SELECT DISTINCT %s FROM alarms %s`, alarmColumns, query)
|
||||
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) alarmsPage(ctx context.Context, comQuery string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
dir := api.DescDir
|
||||
if pm.Dir == api.AscDir {
|
||||
@@ -493,6 +464,9 @@ func pageQueryConditions(pm alarms.PageMetadata) []string {
|
||||
if pm.RuleID != "" {
|
||||
query = append(query, "alarms.rule_id = :rule_id")
|
||||
}
|
||||
if len(pm.RuleIDs) > 0 {
|
||||
query = append(query, "alarms.rule_id = ANY(:rule_ids)")
|
||||
}
|
||||
if pm.ChannelID != "" {
|
||||
query = append(query, "alarms.channel_id = :channel_id")
|
||||
}
|
||||
|
||||
@@ -415,215 +415,6 @@ func TestListAlarms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserAlarms(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM domains_role_actions")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_role_actions unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains_role_members")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_role_members unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains_roles")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_roles unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM rules")
|
||||
require.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
domainID := generateUUID(t)
|
||||
domainRoute := generateUUID(t)
|
||||
userID := generateUUID(t)
|
||||
otherUserID := generateUUID(t)
|
||||
adminUserID := generateUUID(t)
|
||||
domainUserID := generateUUID(t)
|
||||
|
||||
_, err := db.Exec(`INSERT INTO domains (id, name, route, status) VALUES ($1, $2, $3, $4)`, domainID, namegen.Generate(), domainRoute, 0)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains unexpected error: %s", err))
|
||||
|
||||
// Create 10 rules and 10 alarms referencing them.
|
||||
// Assign userID to the first 6 rules via role membership.
|
||||
var ruleIDs []string
|
||||
var createdAlarms []alarms.Alarm
|
||||
for i := range 10 {
|
||||
ruleID := generateUUID(t)
|
||||
_, err := db.Exec(`INSERT INTO rules (id, name, domain_id, status, logic_type, logic_value) VALUES ($1, $2, $3, 0, 0, '')`,
|
||||
ruleID, fmt.Sprintf("rule-%d", i), domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert rule unexpected error: %s", err))
|
||||
ruleIDs = append(ruleIDs, ruleID)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: ruleID,
|
||||
DomainID: domainID,
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC().Add(time.Duration(i) * time.Minute),
|
||||
}
|
||||
alarm, err = repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
createdAlarms = append(createdAlarms, alarm)
|
||||
}
|
||||
|
||||
// Assign userID to the first 6 rules via rules_roles + rules_role_members.
|
||||
userRoleIDs := make([]string, 6)
|
||||
for i := range 6 {
|
||||
roleID := generateUUID(t)
|
||||
userRoleIDs[i] = roleID
|
||||
_, err := db.Exec(`INSERT INTO rules_roles (id, name, entity_id) VALUES ($1, $2, $3)`, roleID, "admin", ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_roles unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO rules_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, roleID, userID, ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
|
||||
}
|
||||
|
||||
for i := range 10 {
|
||||
var roleID string
|
||||
if i < 6 {
|
||||
roleID = userRoleIDs[i]
|
||||
} else {
|
||||
roleID = generateUUID(t)
|
||||
_, err := db.Exec(`INSERT INTO rules_roles (id, name, entity_id) VALUES ($1, $2, $3)`, roleID, "admin", ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_roles unexpected error: %s", err))
|
||||
}
|
||||
_, err := db.Exec(`INSERT INTO rules_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, roleID, adminUserID, ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
|
||||
}
|
||||
|
||||
domainRoleID := generateUUID(t)
|
||||
_, err = db.Exec(`INSERT INTO domains_roles (id, name, entity_id) VALUES ($1, $2, $3)`, domainRoleID, "admin", domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_roles unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO domains_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, domainRoleID, domainUserID, domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_role_members unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO domains_role_actions (role_id, action) VALUES ($1, $2)`, domainRoleID, "alarm_read")
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_role_actions unexpected error: %s", err))
|
||||
|
||||
_ = createdAlarms
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
pm alarms.PageMetadata
|
||||
count int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list user alarms returns only accessible alarms",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with limit",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
count: 3,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with offset",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 4,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 2,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with domain filter",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
DomainID: domainID,
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with non-existing domain returns 0",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
DomainID: generateUUID(t),
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 0,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for user with no role assignments returns 0",
|
||||
userID: otherUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 0,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for admin user with role on all rules returns all alarms",
|
||||
userID: adminUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for user with domain-level rule access returns all alarms",
|
||||
userID: domainUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms ordered by created_at ascending",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Order: "created_at",
|
||||
Dir: "asc",
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
page, err := repo.ListUserAlarms(context.Background(), tc.userID, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Equal(t, tc.count, len(page.Alarms), fmt.Sprintf("%s: expected %d alarms, got %d", tc.desc, tc.count, len(page.Alarms)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
rpostgres "github.com/absmach/magistrala/re/postgres"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
@@ -54,12 +51,5 @@ func Migration() (*migrate.MemoryMigrationSource, error) {
|
||||
},
|
||||
}
|
||||
|
||||
rulesMigration, err := rpostgres.Migration()
|
||||
if err != nil {
|
||||
return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err)
|
||||
}
|
||||
|
||||
alarmsMigration.Migrations = append(alarmsMigration.Migrations, rulesMigration.Migrations...)
|
||||
|
||||
return alarmsMigration, nil
|
||||
}
|
||||
|
||||
+12
-10
@@ -26,10 +26,10 @@ func NewService(idp magistrala.IDProvider, repo Repository) Service {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error) {
|
||||
id, err := s.idp.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
return Alarm{}, err
|
||||
}
|
||||
alarm.ID = id
|
||||
if alarm.CreatedAt.IsZero() {
|
||||
@@ -37,14 +37,18 @@ func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
}
|
||||
|
||||
if err := alarm.Validate(); err != nil {
|
||||
return err
|
||||
return Alarm{}, err
|
||||
}
|
||||
|
||||
if _, err = s.repo.CreateAlarm(ctx, alarm); err != nil && err != repoerr.ErrNotFound {
|
||||
return err
|
||||
created, err := s.repo.CreateAlarm(ctx, alarm)
|
||||
if err != nil && err != repoerr.ErrNotFound {
|
||||
return Alarm{}, err
|
||||
}
|
||||
if err == repoerr.ErrNotFound {
|
||||
return Alarm{}, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID string) (Alarm, error) {
|
||||
@@ -52,10 +56,8 @@ func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID
|
||||
}
|
||||
|
||||
func (s *service) ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return s.repo.ListAllAlarms(ctx, pm)
|
||||
}
|
||||
return s.repo.ListUserAlarms(ctx, session.UserID, pm)
|
||||
pm.DomainID = session.DomainID
|
||||
return s.repo.ListAllAlarms(ctx, pm)
|
||||
}
|
||||
|
||||
func (s *service) DeleteAlarm(ctx context.Context, session authn.Session, alarmID string) error {
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestCreateAlarm(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repoCall := repo.On("CreateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err)
|
||||
err := svc.CreateAlarm(context.Background(), tc.alarm)
|
||||
_, err := svc.CreateAlarm(context.Background(), tc.alarm)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
})
|
||||
@@ -205,7 +205,7 @@ func TestListAlarms(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.pm.DomainID}
|
||||
repoCall := repo.On("ListUserAlarms", context.Background(), s.UserID, tc.pm).Return(tc.page, tc.err)
|
||||
repoCall := repo.On("ListAllAlarms", context.Background(), tc.pm).Return(tc.page, tc.err)
|
||||
_, err := svc.ListAlarms(context.Background(), s, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// protoc v7.35.1
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
@@ -104,6 +104,7 @@ type PageMetadata struct {
|
||||
Format string `protobuf:"bytes,17,opt,name=format,proto3" json:"format,omitempty"`
|
||||
Order string `protobuf:"bytes,18,opt,name=order,proto3" json:"order,omitempty"`
|
||||
Dir string `protobuf:"bytes,19,opt,name=dir,proto3" json:"dir,omitempty"`
|
||||
Publishers []string `protobuf:"bytes,20,rep,name=publishers,proto3" json:"publishers,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -271,6 +272,13 @@ func (x *PageMetadata) GetDir() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetPublishers() []string {
|
||||
if x != nil {
|
||||
return x.Publishers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReadMessagesRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
|
||||
@@ -722,7 +730,7 @@ var File_readers_v1_readers_proto protoreflect.FileDescriptor
|
||||
const file_readers_v1_readers_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x18readers/v1/readers.proto\x12\n" +
|
||||
"readers.v1\"\x8c\x04\n" +
|
||||
"readers.v1\"\xac\x04\n" +
|
||||
"\fPageMetadata\x12\x14\n" +
|
||||
"\x05limit\x18\x01 \x01(\x04R\x05limit\x12\x16\n" +
|
||||
"\x06offset\x18\x02 \x01(\x04R\x06offset\x12\x1a\n" +
|
||||
@@ -747,7 +755,10 @@ const file_readers_v1_readers_proto_rawDesc = "" +
|
||||
"comparator\x12\x16\n" +
|
||||
"\x06format\x18\x11 \x01(\tR\x06format\x12\x14\n" +
|
||||
"\x05order\x18\x12 \x01(\tR\x05order\x12\x10\n" +
|
||||
"\x03dir\x18\x13 \x01(\tR\x03dir\"\x97\x01\n" +
|
||||
"\x03dir\x18\x13 \x01(\tR\x03dir\x12\x1e\n" +
|
||||
"\n" +
|
||||
"publishers\x18\x14 \x03(\tR\n" +
|
||||
"publishers\"\x97\x01\n" +
|
||||
"\x0fReadMessagesRes\x12\x14\n" +
|
||||
"\x05total\x18\x01 \x01(\x04R\x05total\x12=\n" +
|
||||
"\rpage_metadata\x18\x02 \x01(\v2\x18.readers.v1.PageMetadataR\fpageMetadata\x12/\n" +
|
||||
|
||||
+18
-6
@@ -13,10 +13,7 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/clients"
|
||||
"github.com/absmach/magistrala/groups"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/users"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
@@ -80,9 +77,9 @@ const (
|
||||
DefStartLevel = 1
|
||||
DefEndLevel = 0
|
||||
DefStatus = "enabled"
|
||||
DefClientStatus = clients.Enabled
|
||||
DefUserStatus = users.Enabled
|
||||
DefGroupStatus = groups.Enabled
|
||||
DefClientStatus = "enabled"
|
||||
DefUserStatus = "enabled"
|
||||
DefGroupStatus = "enabled"
|
||||
|
||||
// ContentType represents JSON content type.
|
||||
ContentType = "application/json"
|
||||
@@ -184,6 +181,21 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Contains(err, errors.ErrAuthentication) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if errors.Contains(err, errors.ErrAuthorization) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch retErr := err.(type) {
|
||||
case *errors.RequestError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
@@ -260,12 +260,24 @@ func TestEncodeError(t *testing.T) {
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Generic Authentication Failed",
|
||||
err: errors.ErrAuthentication,
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "AuthZError - Authorization Failed",
|
||||
err: svcerr.ErrAuthorization,
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Generic Authorization Failed",
|
||||
err: errors.Wrap(errors.New("not authorized"), errors.ErrAuthorization),
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "AuthZError - Domain Authorization Failed",
|
||||
err: svcerr.ErrDomainAuthorization,
|
||||
|
||||
+745
-293
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -94,6 +94,9 @@ func (client authGrpcClient) Authorize(ctx context.Context, req *grpcAuthV1.Auth
|
||||
}
|
||||
|
||||
if patReq != nil {
|
||||
if patReq.GetDomain() != "" {
|
||||
authReqData.Domain = patReq.GetDomain()
|
||||
}
|
||||
authReqData.UserID = patReq.GetUserId()
|
||||
authReqData.PatID = patReq.GetPatId()
|
||||
authReqData.EntityType = patReq.GetEntityType()
|
||||
|
||||
@@ -1,358 +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
|
||||
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 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,9 @@ func decodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
}
|
||||
|
||||
if patReq != nil {
|
||||
if patReq.GetDomain() != "" {
|
||||
authRequest.Domain = patReq.GetDomain()
|
||||
}
|
||||
authRequest.UserID = patReq.GetUserId()
|
||||
authRequest.PatID = patReq.GetPatId()
|
||||
authRequest.EntityType = patReq.GetEntityType()
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcTokenV1 "github.com/absmach/magistrala/api/grpc/token/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
grpcapi "github.com/absmach/magistrala/auth/api/grpc/token"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8082
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
invalidID = "invalid"
|
||||
)
|
||||
|
||||
var (
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcTokenV1.RegisterTokenServiceServer(server, grpcapi.NewTokenServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userId string
|
||||
kind auth.KeyType
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
userId: validID,
|
||||
kind: auth.AccessKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
userId: validID,
|
||||
kind: auth.RecoveryKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthenticated",
|
||||
userId: validID,
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
userId: validID,
|
||||
kind: 32,
|
||||
issueResponse: auth.Token{},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that does notexist",
|
||||
userId: "",
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Issue(context.Background(), &grpcTokenV1.IssueReq{UserId: tc.userId, Type: uint32(tc.kind)})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "refresh token with valid token",
|
||||
token: validToken,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with invalid token",
|
||||
token: inValidToken,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with empty token",
|
||||
token: "",
|
||||
issueResponse: auth.Token{},
|
||||
err: apiutil.ErrMissingSecret,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke token with valid id",
|
||||
id: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with invalid id",
|
||||
id: invalidID,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "revoke already revoked token",
|
||||
id: validID,
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("RevokeToken", mock.Anything, mock.Anything, tc.id).Return(tc.err)
|
||||
_, err := grpcClient.Revoke(context.Background(), &grpcTokenV1.RevokeReq{TokenId: tc.id})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserRefreshTokens(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
listResponse []auth.TokenInfo
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list tokens for user with valid id",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 1"},
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens for user with empty list",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with invalid user id",
|
||||
userID: invalidID,
|
||||
listResponse: nil,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with empty user id",
|
||||
userID: "",
|
||||
listResponse: nil,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("ListUserRefreshTokens", mock.Anything, tc.userID).Return(tc.listResponse, tc.err)
|
||||
_, err := grpcClient.ListUserRefreshTokens(context.Background(), &grpcTokenV1.ListUserRefreshTokensReq{UserId: tc.userID})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package hasher contains the domain concept definitions needed to
|
||||
// support Supermq users password hasher sub-service functionality.
|
||||
// support Magistrala users password hasher sub-service functionality.
|
||||
package hasher
|
||||
|
||||
@@ -21,7 +21,7 @@ type loggingMiddleware struct {
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// NewLogging adds logging facilities to the core service.
|
||||
// NewLogging adds logging facilities to the service.
|
||||
func NewLogging(svc auth.Service, logger *slog.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type metricsMiddleware struct {
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// NewMetrics instruments core service by tracking request count and latency.
|
||||
// NewMetrics instruments service by tracking request count and latency.
|
||||
func NewMetrics(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
|
||||
+7
-1
@@ -76,6 +76,7 @@ const (
|
||||
GroupsType EntityType = iota
|
||||
ChannelsType
|
||||
ClientsType
|
||||
BootstrapType
|
||||
DashboardType
|
||||
MessagesType
|
||||
DomainsType
|
||||
@@ -88,6 +89,7 @@ const (
|
||||
GroupsScopeStr = "groups"
|
||||
ChannelsScopeStr = "channels"
|
||||
ClientsScopeStr = "clients"
|
||||
BootstrapStr = "bootstrap"
|
||||
DashboardsStr = "dashboards"
|
||||
MessagesStr = "messages"
|
||||
DomainsStr = "domains"
|
||||
@@ -104,6 +106,8 @@ func (et EntityType) String() string {
|
||||
return ChannelsScopeStr
|
||||
case ClientsType:
|
||||
return ClientsScopeStr
|
||||
case BootstrapType:
|
||||
return BootstrapStr
|
||||
case DashboardType:
|
||||
return DashboardsStr
|
||||
case MessagesType:
|
||||
@@ -129,6 +133,8 @@ func ParseEntityType(et string) (EntityType, error) {
|
||||
return ChannelsType, nil
|
||||
case ClientsScopeStr:
|
||||
return ClientsType, nil
|
||||
case BootstrapStr:
|
||||
return BootstrapType, nil
|
||||
case DashboardsStr:
|
||||
return DashboardType, nil
|
||||
case MessagesStr:
|
||||
@@ -169,7 +175,7 @@ func (et *EntityType) UnmarshalText(data []byte) (err error) {
|
||||
|
||||
func IsValidOperationForEntity(entityType EntityType, operation string) bool {
|
||||
switch entityType {
|
||||
case ClientsType, ChannelsType, GroupsType, DomainsType, RulesType, ReportsType:
|
||||
case ClientsType, ChannelsType, GroupsType, BootstrapType, DomainsType, RulesType, ReportsType:
|
||||
return true
|
||||
case DashboardType:
|
||||
return operation == OpDashboardShare || operation == OpDashboardUnshare
|
||||
|
||||
@@ -31,6 +31,11 @@ func TestEntityTypeString(t *testing.T) {
|
||||
et: auth.ClientsType,
|
||||
expected: "clients",
|
||||
},
|
||||
{
|
||||
desc: "Bootstrap entity type",
|
||||
et: auth.BootstrapType,
|
||||
expected: "bootstrap",
|
||||
},
|
||||
{
|
||||
desc: "Dashboard entity type",
|
||||
et: auth.DashboardType,
|
||||
@@ -91,6 +96,12 @@ func TestParseEntityType(t *testing.T) {
|
||||
expected: auth.ClientsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse bootstrap",
|
||||
et: "bootstrap",
|
||||
expected: auth.BootstrapType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse dashboards",
|
||||
et: "dashboards",
|
||||
@@ -155,6 +166,12 @@ func TestEntityTypeMarshalJSON(t *testing.T) {
|
||||
expected: []byte(`"clients"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal bootstrap",
|
||||
et: auth.BootstrapType,
|
||||
expected: []byte(`"bootstrap"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal rules",
|
||||
et: auth.RulesType,
|
||||
@@ -197,6 +214,12 @@ func TestEntityTypeUnmarshalJSON(t *testing.T) {
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal bootstrap",
|
||||
data: []byte(`"bootstrap"`),
|
||||
expected: auth.BootstrapType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal rules",
|
||||
data: []byte(`"rules"`),
|
||||
@@ -250,6 +273,12 @@ func TestEntityTypeMarshalText(t *testing.T) {
|
||||
expected: []byte("channels"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal bootstrap as text",
|
||||
et: auth.BootstrapType,
|
||||
expected: []byte("bootstrap"),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -280,6 +309,12 @@ func TestEntityTypeUnmarshalText(t *testing.T) {
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal bootstrap from text",
|
||||
data: []byte("bootstrap"),
|
||||
expected: auth.BootstrapType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal unknown from text",
|
||||
data: []byte("unknown"),
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
channelsOps "github.com/absmach/magistrala/channels/operations"
|
||||
clientsOps "github.com/absmach/magistrala/clients/operations"
|
||||
groupsOps "github.com/absmach/magistrala/groups/operations"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopeAuthorized(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
entityType auth.EntityType
|
||||
domainID string
|
||||
operation string
|
||||
entityID string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Authorized with matching entity type, domain, operation and entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "any-entity",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: "view",
|
||||
EntityID: "client1",
|
||||
},
|
||||
entityType: auth.ClientsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "client1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity type",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.ChannelsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain2",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different operation",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "delete",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity2",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with nil scope",
|
||||
scope: nil,
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
result := tc.scope.Authorized(tc.entityType, tc.domainID, tc.operation, tc.entityID)
|
||||
assert.Equal(t, tc.expected, result, "Authorized() = %v, expected %v", result, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Valid scope for groups with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for channels with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for clients with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "update",
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for messages with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "domain1",
|
||||
Operation: "message_publish",
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for dashboard with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "domain1",
|
||||
Operation: "dashboard_share",
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil scope",
|
||||
scope: nil,
|
||||
err: assert.AnError, // Will be checked with Contains
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope without entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingEntityID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for groups without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for channels without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "",
|
||||
Operation: channelsOps.OperationDetails()[channelsOps.OpViewChannel].Name,
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for clients without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: clientsOps.OperationDetails()[clientsOps.OpViewClient].Name,
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for dashboard without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpShare,
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for messages without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpPublish,
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.scope.Validate()
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
if tc.err != assert.AnError {
|
||||
assert.Equal(t, tc.err, err, "Validate() error = %v, expected %v", err, tc.err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
pat *auth.PAT
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Valid PAT",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil PAT",
|
||||
pat: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without name",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without user",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.pat.Validate()
|
||||
if tc.err {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATMarshalUnmarshalBinary(t *testing.T) {
|
||||
pat := auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
Secret: "secret",
|
||||
IssuedAt: time.Now().UTC().Round(time.Second),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
|
||||
Status: auth.ActiveStatus,
|
||||
}
|
||||
|
||||
// Marshal
|
||||
data, err := pat.MarshalBinary()
|
||||
assert.NoError(t, err, "MarshalBinary() should not return error")
|
||||
assert.NotNil(t, data, "MarshalBinary() should return data")
|
||||
|
||||
// Unmarshal
|
||||
var newPAT auth.PAT
|
||||
err = newPAT.UnmarshalBinary(data)
|
||||
assert.NoError(t, err, "UnmarshalBinary() should not return error")
|
||||
|
||||
assert.Equal(t, pat.ID, newPAT.ID, "ID mismatch")
|
||||
assert.Equal(t, pat.User, newPAT.User, "User mismatch")
|
||||
assert.Equal(t, pat.Name, newPAT.Name, "Name mismatch")
|
||||
assert.Equal(t, pat.Description, newPAT.Description, "Description mismatch")
|
||||
assert.Equal(t, pat.Secret, newPAT.Secret, "Secret mismatch")
|
||||
assert.Equal(t, pat.Status, newPAT.Status, "Status mismatch")
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
# BOOTSTRAP SERVICE
|
||||
|
||||
New devices need to be configured properly and connected to the Magistrala. Bootstrap service is used in order to accomplish that. This service provides the following features:
|
||||
|
||||
1. Creating new Magistrala Clients
|
||||
2. Providing basic configuration for the newly created Clients
|
||||
3. Enabling/disabling Clients
|
||||
|
||||
Pre-provisioning a new Client is as simple as sending Configuration data to the Bootstrap service. Once the Client is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Clients. Only enabled Clients can exchange messages over Magistrala. Bootstrapping does not implicitly enable Clients, it has to be done manually.
|
||||
|
||||
In order to bootstrap successfully, the Client needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Client is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Client will be saved so that it can be provisioned later.
|
||||
|
||||
## Client Configuration Entity
|
||||
|
||||
Client Configuration consists of two logical parts: the custom configuration that can be interpreted by the Client itself and Magistrala-related configuration. Magistrala config contains:
|
||||
|
||||
1. corresponding Magistrala Client ID
|
||||
2. corresponding Magistrala Client key
|
||||
3. list of the Magistrala channels the Client is connected to
|
||||
|
||||
> Note: list of channels contains IDs of the Magistrala channels. These channels are _pre-provisioned_ on the Magistrala side and, unlike corresponding Magistrala Client, Bootstrap service is not able to create Magistrala Channels.
|
||||
|
||||
Enabling and disabling Client (adding Client to/from whitelist) is as simple as connecting corresponding Magistrala Client to the given list of Channels. Configuration keeps _state_ of the Client:
|
||||
|
||||
| State | What it means |
|
||||
| -------- | ---------------------------------------------- |
|
||||
| Inactive | Client is created, but isn't enabled |
|
||||
| Active | Client is able to communicate using Magistrala |
|
||||
|
||||
Switching between states `Active` and `Inactive` enables and disables Client, respectively.
|
||||
|
||||
Client configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Client. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| MG_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | info |
|
||||
| MG_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MG_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| MG_BOOTSTRAP_DB_USER | Database user | magistrala |
|
||||
| MG_BOOTSTRAP_DB_PASS | Database password | magistrala |
|
||||
| MG_BOOTSTRAP_DB_NAME | Name of the database used by the service | bootstrap |
|
||||
| MG_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
|
||||
| MG_BOOTSTRAP_HTTP_HOST | Bootstrap service HTTP host | "" |
|
||||
| MG_BOOTSTRAP_HTTP_PORT | Bootstrap service HTTP port | 9013 |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_CERT | Path to server certificate in pem format | "" |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_KEY | Path to server key in pem format | "" |
|
||||
| MG_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MG_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| MG_AUTH_GRPC_URL | Auth service Auth gRPC URL | <localhost:8181> |
|
||||
| MG_AUTH_GRPC_TIMEOUT | Auth service Auth gRPC request timeout in seconds | 1s |
|
||||
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service Auth gRPC client certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service Auth gRPC client key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server Auth gRPC server trusted CA certificate file | "" |
|
||||
| MG_CLIENTS_URL | Base URL for Magistrala Clients | <http://localhost:9000> |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:4318/v1/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| MG_BOOTSTRAP_INSTANCE_ID | Bootstrap service instance ID | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`bootstrap`](https://github.com/absmach/magistrala/blob/main/docker/addons/bootstrap/docker-compose.yaml) service section in docker-compose file to see how service is deployed.
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/magistrala
|
||||
|
||||
cd magistrala
|
||||
|
||||
# compile the servic e
|
||||
make bootstrap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MG_BOOTSTRAP_LOG_LEVEL=info \
|
||||
MG_BOOTSTRAP_DB_HOST=localhost \
|
||||
MG_BOOTSTRAP_DB_PORT=5432 \
|
||||
MG_BOOTSTRAP_DB_USER=magistrala \
|
||||
MG_BOOTSTRAP_DB_PASS=magistrala \
|
||||
MG_BOOTSTRAP_DB_NAME=bootstrap \
|
||||
MG_BOOTSTRAP_DB_SSL_MODE=disable \
|
||||
MG_BOOTSTRAP_DB_SSL_CERT="" \
|
||||
MG_BOOTSTRAP_DB_SSL_KEY="" \
|
||||
MG_BOOTSTRAP_DB_SSL_ROOT_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_HOST=localhost \
|
||||
MG_BOOTSTRAP_HTTP_PORT=9013 \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_KEY="" \
|
||||
MG_BOOTSTRAP_EVENT_CONSUMER=bootstrap \
|
||||
MG_ES_URL=nats://localhost:4222 \
|
||||
MG_AUTH_GRPC_URL=localhost:8181 \
|
||||
MG_AUTH_GRPC_TIMEOUT=1s \
|
||||
MG_AUTH_GRPC_CLIENT_CERT="" \
|
||||
MG_AUTH_GRPC_CLIENT_KEY="" \
|
||||
MG_AUTH_GRPC_SERVER_CERTS="" \
|
||||
MG_CLIENTS_URL=http://localhost:9000 \
|
||||
MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_BOOTSTRAP_INSTANCE_ID="" \
|
||||
$GOBIN/magistrala-bootstrap
|
||||
```
|
||||
|
||||
Setting `MG_BOOTSTRAP_HTTP_SERVER_CERT` and `MG_BOOTSTRAP_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
|
||||
Setting `MG_AUTH_GRPC_CLIENT_CERT` and `MG_AUTH_GRPC_CLIENT_KEY` will enable TLS against the auth service. The service expects a file in PEM format for both the certificate and the key. Setting `MG_AUTH_GRPC_SERVER_CERTS` will enable TLS against the auth service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.magistrala.absmach.eu/?urls.primaryName=bootstrap.yaml).
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of bootstrap service HTTP API.
|
||||
package api
|
||||
@@ -1,289 +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
|
||||
}
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for _, c := range req.Channels {
|
||||
channels = append(channels, bootstrap.Channel{ID: c})
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.ClientID,
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
Channels: channels,
|
||||
Name: req.Name,
|
||||
ClientCert: req.ClientCert,
|
||||
ClientKey: req.ClientKey,
|
||||
CACert: req.CACert,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(ctx, session, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: saved.ClientID,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request 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.clientID, req.ClientCert, req.ClientKey, req.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := updateConfigRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientCert: cfg.ClientCert,
|
||||
CACert: cfg.CACert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request 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
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range config.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
ClientID: config.ClientID,
|
||||
CLientSecret: config.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request 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{
|
||||
ClientID: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(ctx, session, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: config.ClientID,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateConnReq)
|
||||
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.UpdateConnections(ctx, session, req.token, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: req.id,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request 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 {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
ClientID: cfg.ClientID,
|
||||
CLientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request 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 stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeStateReq)
|
||||
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.ChangeState(ctx, session, req.token, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stateRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,163 +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
|
||||
ClientID string `json:"client_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if len(req.Channels) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
for _, channel := range req.Channels {
|
||||
if channel == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
clientID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.clientID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
token string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bootstrapReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
token string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrBootstrapState
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,313 +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/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
channel1 = testsutil.GenerateUUID(&testing.T{})
|
||||
channel2 = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
channels []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external key and external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty channels",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{},
|
||||
err: apiutil.ErrEmptyList,
|
||||
},
|
||||
{
|
||||
desc: "empty channel value",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, ""},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
Channels: tc.channels,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
id: "id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty client id",
|
||||
clientID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
clientID: tc.clientID,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateConnReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "too large limit",
|
||||
offset: 0,
|
||||
limit: maxLimitSize + 1,
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "default limit",
|
||||
offset: 0,
|
||||
limit: defLimit,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
externKey string
|
||||
externID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := bootstrapReq{
|
||||
id: tc.externID,
|
||||
key: tc.externKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
token: "token",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrBootstrapState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,144 +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 = (*stateRes)(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 configRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res configRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res configRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/clients/configs/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res configRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CLientSecret string `json:"client_secret,omitempty"`
|
||||
Channels []channelRes `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func (res viewRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type stateRes struct{}
|
||||
|
||||
func (res stateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res stateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type updateConfigRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/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/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
byteContentType = "application/octet-stream"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
fullMatch = []string{"state", "external_id", "client_id", "client_key"}
|
||||
partialMatch = []string{"name"}
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, authn smqauthn.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.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateEndpoint(svc),
|
||||
decodeUpdateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update").ServeHTTP)
|
||||
|
||||
r.Delete("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
removeEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "remove").ServeHTTP)
|
||||
|
||||
r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_cert").ServeHTTP)
|
||||
|
||||
r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateConnEndpoint(svc),
|
||||
decodeUpdateConnRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_connections").ServeHTTP)
|
||||
})
|
||||
})
|
||||
|
||||
r.With(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_state").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Route("/clients/bootstrap", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/secure/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, true),
|
||||
decodeBootstrapRequest,
|
||||
encodeSecureRes,
|
||||
opts...), "bootstrap_secure").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Get("/health", 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{
|
||||
clientID: chi.URLParam(r, "certID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "connID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.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,
|
||||
}
|
||||
|
||||
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 decodeStateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "clientID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, 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
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/clients"
|
||||
)
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding Magistrala entities.
|
||||
// MGClient represents corresponding Magistrala Client ID.
|
||||
// MGKey is key of corresponding Magistrala Client.
|
||||
// MGChannels is a list of Magistrala Channels corresponding Magistrala Client connects to.
|
||||
type Config struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
Channels []Channel `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Content string `json:"content,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// Channel represents Magistrala channel corresponding Magistrala Client is connected to.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []Config `json:"configs"`
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(ctx context.Context, cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(ctx context.Context, domainID, id string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(ctx context.Context, externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(ctx context.Context, cfg Config) error
|
||||
|
||||
// UpdateCerts updates and returns an existing Config certificate and domainID.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(ctx context.Context, domainID, id string, channels []Channel, connections []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(ctx context.Context, domainID, id string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(ctx context.Context, domainID, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(ctx context.Context, domainID string, ids []string) ([]Channel, error)
|
||||
|
||||
// Methods RemoveClient, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveClient removes Config of the Client with the given ID.
|
||||
RemoveClient(ctx context.Context, id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(ctx context.Context, c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClient changes state of the Config when the corresponding Client is connected to the Channel.
|
||||
ConnectClient(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClient changes state of the Config when the corresponding Client is disconnected from the Channel.
|
||||
DisconnectClient(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// Magistrala bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package consumer contains events consumer for events
|
||||
// published by Bootstrap service.
|
||||
package consumer
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import "time"
|
||||
|
||||
type removeEvent struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata map[string]any
|
||||
updatedAt time.Time
|
||||
updatedBy string
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
type connectionEvent struct {
|
||||
clientIDs []string
|
||||
channelID string
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
clientRemove = "client.remove"
|
||||
clientConnect = "group.assign"
|
||||
clientDisconnect = "group.unassign"
|
||||
|
||||
channelPrefix = "channels."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
channelRemove = channelPrefix + "remove"
|
||||
|
||||
memberKind = "client"
|
||||
relation = "group"
|
||||
)
|
||||
|
||||
type eventHandler struct {
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventHandler returns new event store handler.
|
||||
func NewEventHandler(svc bootstrap.Service) events.EventHandler {
|
||||
return &eventHandler{
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
|
||||
msg, err := event.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg["operation"] {
|
||||
case clientRemove:
|
||||
rte := decodeRemoveClient(msg)
|
||||
err = es.svc.RemoveConfigHandler(ctx, rte.id)
|
||||
case clientConnect:
|
||||
cte := decodeConnectClient(msg)
|
||||
if cte.channelID == "" || len(cte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range cte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
if err := es.svc.ConnectClientHandler(ctx, cte.channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clientDisconnect:
|
||||
dte := decodeDisconnectClient(msg)
|
||||
if dte.channelID == "" || len(dte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range dte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range dte.clientIDs {
|
||||
if err = es.svc.DisconnectClientHandler(ctx, dte.channelID, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(msg)
|
||||
err = es.handleUpdateChannel(ctx, uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(msg)
|
||||
err = es.svc.RemoveChannelHandler(ctx, rce.id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeRemoveClient(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]any) updateChannelEvent {
|
||||
metadata := events.Read(event, "metadata", map[string]any{})
|
||||
|
||||
return updateChannelEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
name: events.Read(event, "name", ""),
|
||||
metadata: metadata,
|
||||
updatedAt: events.Read(event, "updated_at", time.Now()),
|
||||
updatedBy: events.Read(event, "updated_by", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeConnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
return connectionEvent{
|
||||
channelID: events.Read(event, "group_id", ""),
|
||||
clientIDs: events.ReadStringSlice(event, "member_ids"),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDisconnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
return connectionEvent{
|
||||
channelID: events.Read(event, "group_id", ""),
|
||||
clientIDs: events.ReadStringSlice(event, "member_ids"),
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
UpdatedAt: uce.updatedAt,
|
||||
UpdatedBy: uce.updatedBy,
|
||||
}
|
||||
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package events provides the domain concept definitions needed to support
|
||||
// bootstrap events functionality.
|
||||
package events
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package producer contains the domain events needed to support
|
||||
// event sourcing of Bootstrap service actions.
|
||||
package producer
|
||||
@@ -1,277 +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"
|
||||
configHandlerRemove = configPrefix + "remove_handler"
|
||||
|
||||
clientPrefix = "bootstrap.client."
|
||||
clientBootstrap = clientPrefix + "bootstrap"
|
||||
clientStateChange = clientPrefix + "change_state"
|
||||
clientUpdateConnections = clientPrefix + "update_connections"
|
||||
clientConnect = clientPrefix + "connect"
|
||||
clientDisconnect = clientPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "bootstrap.channel."
|
||||
channelHandlerRemove = channelPrefix + "remove_handler"
|
||||
channelUpdateHandler = channelPrefix + "update_handler"
|
||||
|
||||
certUpdate = "bootstrap.cert.update"
|
||||
)
|
||||
|
||||
var (
|
||||
_ events.Event = (*configEvent)(nil)
|
||||
_ events.Event = (*removeConfigEvent)(nil)
|
||||
_ events.Event = (*bootstrapEvent)(nil)
|
||||
_ events.Event = (*changeStateEvent)(nil)
|
||||
_ events.Event = (*updateConnectionsEvent)(nil)
|
||||
_ events.Event = (*updateCertEvent)(nil)
|
||||
_ events.Event = (*listConfigsEvent)(nil)
|
||||
_ events.Event = (*removeHandlerEvent)(nil)
|
||||
)
|
||||
|
||||
type configEvent struct {
|
||||
bootstrap.Config
|
||||
operation string
|
||||
}
|
||||
|
||||
func (ce configEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"state": ce.State.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
if ce.ClientID != "" {
|
||||
val["client_id"] = ce.ClientID
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
if ce.DomainID != "" {
|
||||
val["domain_id "] = ce.DomainID
|
||||
}
|
||||
if ce.Name != "" {
|
||||
val["name"] = ce.Name
|
||||
}
|
||||
if ce.ExternalID != "" {
|
||||
val["external_id"] = ce.ExternalID
|
||||
}
|
||||
if len(ce.Channels) > 0 {
|
||||
channels := make([]string, len(ce.Channels))
|
||||
for i, ch := range ce.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if ce.ClientCert != "" {
|
||||
val["client_cert"] = ce.ClientCert
|
||||
}
|
||||
if ce.ClientKey != "" {
|
||||
val["client_key"] = ce.ClientKey
|
||||
}
|
||||
if ce.CACert != "" {
|
||||
val["ca_cert"] = ce.CACert
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type removeConfigEvent struct {
|
||||
client string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": rce.client,
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listConfigsEvent struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
fullMatch map[string]string
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) Encode() (map[string]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.ClientID != "" {
|
||||
val["client_id"] = be.ClientID
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
if be.DomainID != "" {
|
||||
val["domain_id "] = be.DomainID
|
||||
}
|
||||
if be.Name != "" {
|
||||
val["name"] = be.Name
|
||||
}
|
||||
if be.ExternalID != "" {
|
||||
val["external_id"] = be.ExternalID
|
||||
}
|
||||
if len(be.Channels) > 0 {
|
||||
channels := make([]string, len(be.Channels))
|
||||
for i, ch := range be.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if be.ClientCert != "" {
|
||||
val["client_cert"] = be.ClientCert
|
||||
}
|
||||
if be.ClientKey != "" {
|
||||
val["client_key"] = be.ClientKey
|
||||
}
|
||||
if be.CACert != "" {
|
||||
val["ca_cert"] = be.CACert
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type changeStateEvent struct {
|
||||
mgClient string
|
||||
state bootstrap.State
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cse.mgClient,
|
||||
"state": cse.state.String(),
|
||||
"operation": clientStateChange,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateConnectionsEvent struct {
|
||||
mgClient string
|
||||
mgChannels []string
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.mgClient,
|
||||
"channels": uce.mgChannels,
|
||||
"operation": clientUpdateConnections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateCertEvent struct {
|
||||
clientID string
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.clientID,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
"ca_cert": uce.caCert,
|
||||
"operation": certUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type removeHandlerEvent struct {
|
||||
id string
|
||||
operation string
|
||||
}
|
||||
|
||||
func (rhe removeHandlerEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": rhe.id,
|
||||
"operation": rhe.operation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateChannelHandlerEvent struct {
|
||||
bootstrap.Channel
|
||||
}
|
||||
|
||||
func (uche updateChannelHandlerEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"operation": channelUpdateHandler,
|
||||
}
|
||||
|
||||
if uche.ID != "" {
|
||||
val["channel_id"] = uche.ID
|
||||
}
|
||||
if uche.Name != "" {
|
||||
val["name"] = uche.Name
|
||||
}
|
||||
if uche.Metadata != nil {
|
||||
val["metadata"] = uche.Metadata
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type connectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (cte connectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cte.clientID,
|
||||
"channel_id": cte.channelID,
|
||||
"operation": clientConnect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type disconnectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (dte disconnectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": dte.clientID,
|
||||
"channel_id": dte.channelID,
|
||||
"operation": clientDisconnect,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
redisClient *redis.Client
|
||||
redisURL string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "redis",
|
||||
Tag: "7.2.4-alpine",
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
redisURL = fmt.Sprintf("redis://localhost:%s/0", container.GetPort("6379/tcp"))
|
||||
opts, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse redis URL: %s", err)
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
redisClient = redis.NewClient(opts)
|
||||
|
||||
return redisClient.Ping(context.Background()).Err()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,253 +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
|
||||
viewStream = magistralaPrefix + configView
|
||||
listStream = magistralaPrefix + configList
|
||||
updateStream = magistralaPrefix + configUpdate
|
||||
removeStream = magistralaPrefix + configRemove
|
||||
updateCertStream = magistralaPrefix + certUpdate
|
||||
updateConnectionsStream = magistralaPrefix + clientUpdateConnections
|
||||
removeHandlerStream = magistralaPrefix + configHandlerRemove
|
||||
bootstrapStream = magistralaPrefix + clientBootstrap
|
||||
stateChangeStream = magistralaPrefix + clientStateChange
|
||||
connectStream = magistralaPrefix + clientConnect
|
||||
disconnectStream = magistralaPrefix + clientDisconnect
|
||||
updateHandlerStream = magistralaPrefix + channelUpdateHandler
|
||||
removeChannelHandlerStream = magistralaPrefix + channelHandlerRemove
|
||||
)
|
||||
|
||||
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, 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, configUpdate, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := updateCertEvent{
|
||||
clientID: clientID,
|
||||
clientCert: clientCert,
|
||||
clientKey: clientKey,
|
||||
caCert: caCert,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, updateCertStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(ctx, session, token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConnectionsEvent{
|
||||
mgClient: id,
|
||||
mgChannels: connections,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, updateConnectionsStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
bp, err := es.svc.List(ctx, session, filter, offset, limit)
|
||||
if err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
ev := listConfigsEvent{
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
fullMatch: filter.FullMatch,
|
||||
partialMatch: filter.PartialMatch,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, 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{
|
||||
client: 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) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(ctx, session, token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := changeStateEvent{
|
||||
mgClient: id,
|
||||
state: state,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, stateChangeStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: configHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeHandlerStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: channelHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeChannelHandlerStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateChannelHandlerEvent{
|
||||
channel,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, updateStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.ConnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := connectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, connectStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.DisconnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := disconnectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, disconnectStream, ev)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,150 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/authz"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
)
|
||||
|
||||
const (
|
||||
updatePermission = "update_permission"
|
||||
readPermission = "read_permission"
|
||||
deletePermission = "delete_permission"
|
||||
)
|
||||
|
||||
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, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, readPermission, policies.ClientType, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, cfg.ClientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, clientID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
if err := am.checkSuperAdmin(ctx, session.DomainUserID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
|
||||
return am.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, deletePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
return am.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
return am.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
return am.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error {
|
||||
if err := am.authz.Authorize(ctx, authz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: adminID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.MagistralaObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error {
|
||||
req := authz.PolicyReq{
|
||||
Domain: domain,
|
||||
SubjectType: subjType,
|
||||
SubjectKind: subjKind,
|
||||
Subject: subj,
|
||||
Permission: perm,
|
||||
ObjectType: objType,
|
||||
Object: obj,
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, req, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/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("client_id", saved.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Add new bootstrap failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Add new bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View logs the view request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View client config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View client config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update logs the update request. It logs bootstrap client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("config",
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
slog.String("name", cfg.Name),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert logs the update_cert request. It logs client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config certificate failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config certificate completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections logs the update_connections request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
slog.Any("connections", connections),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update config connections failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update config connections completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List logs the list request. It logs offset, limit and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("page",
|
||||
slog.Any("filter", filter),
|
||||
slog.Uint64("offset", offset),
|
||||
slog.Uint64("limit", limit),
|
||||
slog.Uint64("total", res.Total),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List configs failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List configs completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove logs the remove request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("external_id", externalID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("id", id),
|
||||
slog.Any("state", state),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Change client state failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Change client state completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("channel",
|
||||
slog.String("id", channel.ID),
|
||||
slog.String("name", channel.Name),
|
||||
slog.Any("metadata", channel.Metadata),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove config handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove config handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Connect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Connect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Disconnect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Disconnect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/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, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections instruments UpdateConnections method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List instruments List method with metrics.
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove instruments Remove method with metrics.
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap instruments Bootstrap method with metrics.
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState instruments ChangeState method with metrics.
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler instruments ConnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "connect_client_handler").Add(1)
|
||||
mm.latency.With("method", "connect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler instruments DisconnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_client_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,771 +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/clients"
|
||||
"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/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errConnectClient = errors.New("failed to connect client in bootstrap configuration in database")
|
||||
errDisconnectClient = errors.New("failed to disconnect client in bootstrap configuration in database")
|
||||
)
|
||||
|
||||
const cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
|
||||
SELECT channel_id FROM connections c WHERE ch.magistrala_channel = c.channel_id);`
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
db postgres.Database
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewConfigRepository instantiates a PostgreSQL implementation of config
|
||||
// repository.
|
||||
func NewConfigRepository(db postgres.Database, log *slog.Logger) bootstrap.ConfigRepository {
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (clientID string, err error) {
|
||||
q := `INSERT INTO configs (magistrala_client, domain_id, name, client_cert, client_key, ca_cert, magistrala_secret, external_id, external_key, content, state)
|
||||
VALUES (:magistrala_client, :domain_id, :name, :client_cert, :client_key, :ca_cert, :magistrala_secret, :external_id, :external_key, :content, :state)`
|
||||
|
||||
tx, err := cr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
dbcfg := toDBConfig(cfg)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = cr.rollback("Save method", err, tx)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
switch pgErr := err.(type) {
|
||||
case *pgconn.PgError:
|
||||
if pgErr.Code == pgerrcode.UniqueViolation {
|
||||
err = repoerr.ErrConflict
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.DomainID, cfg.Channels, tx); err != nil {
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(ctx, cfg, chsConnIDs, tx); err != nil {
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
return "", commitErr
|
||||
}
|
||||
|
||||
return cfg.ClientID, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state, client_cert, ca_cert
|
||||
FROM configs
|
||||
WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
q = `SELECT magistrala_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
|
||||
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
chans := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
dbch.DomainID = nullString(dbcfg.DomainID)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.Channels = chans
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := buildRetrieveQueryParams(domainID, clientIDs, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state
|
||||
FROM configs %s ORDER BY magistrala_client LIMIT $%d OFFSET $%d`
|
||||
q = fmt.Sprintf(q, search, n+1, n+2)
|
||||
|
||||
rows, err := cr.db.QueryContext(ctx, q, append(params, limit, offset)...)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var name, content sql.NullString
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{DomainID: domainID}
|
||||
if err := rows.Scan(&c.ClientID, &c.ClientSecret, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
c.Name = name.String
|
||||
c.Content = content.String
|
||||
configs = append(configs, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRowxContext(ctx, q, params...).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_key, domain_id, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
WHERE external_id = :external_id`
|
||||
dbcfg := dbConfig{
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
q = `SELECT magistrala_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
|
||||
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.Channels = channels
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET name = :name, content = :content WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id `
|
||||
|
||||
dbcfg := dbConfig{
|
||||
Name: nullString(cfg.Name),
|
||||
Content: nullString(cfg.Content),
|
||||
ClientID: cfg.ClientID,
|
||||
DomainID: cfg.DomainID,
|
||||
}
|
||||
|
||||
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
|
||||
RETURNING magistrala_client, client_cert, client_key, ca_cert`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: clientID,
|
||||
ClientCert: nullString(clientCert),
|
||||
DomainID: domainID,
|
||||
ClientKey: nullString(clientKey),
|
||||
CaCert: nullString(caCert),
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if ok := row.Next(); !ok {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return toConfig(dbcfg), nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(ctx context.Context, domainID, id string, channels []bootstrap.Channel, connections []string) (err error) {
|
||||
tx, err := cr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = cr.rollback("UpdateConnections method", err, tx)
|
||||
} else {
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
err = commitErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = insertChannels(domainID, channels, tx); err != nil {
|
||||
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = updateConnections(domainID, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pgconn.PgError); ok {
|
||||
if e.Code == pgerrcode.ForeignKeyViolation {
|
||||
err = repoerr.ErrNotFound
|
||||
}
|
||||
}
|
||||
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
|
||||
q := `DELETE FROM configs WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(ctx context.Context, domainID, id string, state bootstrap.State) error {
|
||||
q := `UPDATE configs SET state = :state WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id;`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
State: state,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
|
||||
var channels []bootstrap.Channel
|
||||
if len(ids) == 0 {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
var chans pgtype.TextArray
|
||||
if err := chans.Set(ids); err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
q := "SELECT magistrala_channel, name, metadata FROM channels WHERE domain_id = $1 AND magistrala_channel = ANY ($2)"
|
||||
rows, err := cr.db.QueryxContext(ctx, q, domainID, chans)
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var dbch dbChannel
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
|
||||
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveClient(ctx context.Context, id string) error {
|
||||
q := `DELETE FROM configs WHERE magistrala_client = $1`
|
||||
_, err := cr.db.ExecContext(ctx, q, id)
|
||||
|
||||
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", c)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE magistrala_channel = :magistrala_channel`
|
||||
if _, err = cr.db.NamedExecContext(ctx, q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveChannel(ctx context.Context, id string) error {
|
||||
q := `DELETE FROM channels WHERE magistrala_channel = $1`
|
||||
if _, err := cr.db.ExecContext(ctx, q, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ConnectClient(ctx context.Context, channelID, clientID string) error {
|
||||
q := `UPDATE configs SET state = $1
|
||||
WHERE magistrala_client = $2
|
||||
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
|
||||
result, err := cr.db.ExecContext(ctx, q, bootstrap.Active, clientID, channelID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errConnectClient, err)
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) DisconnectClient(ctx context.Context, channelID, clientID string) error {
|
||||
q := `UPDATE configs SET state = $1
|
||||
WHERE magistrala_client = $2
|
||||
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
_, err := cr.db.ExecContext(ctx, q, bootstrap.Inactive, clientID, channelID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDisconnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []any) {
|
||||
params := []any{}
|
||||
queries := []string{}
|
||||
|
||||
if len(clientIDs) != 0 {
|
||||
queries = append(queries, fmt.Sprintf("magistrala_client IN ('%s')", strings.Join(clientIDs, "','")))
|
||||
} else if domainID != "" {
|
||||
params = append(params, domainID)
|
||||
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
|
||||
}
|
||||
|
||||
// Adjust the starting point for placeholders based on the current length of params
|
||||
counter := len(params) + 1
|
||||
for k, v := range filter.FullMatch {
|
||||
params = append(params, v)
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
counter++
|
||||
}
|
||||
for k, v := range filter.PartialMatch {
|
||||
params = append(params, v)
|
||||
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
|
||||
counter++
|
||||
}
|
||||
|
||||
if len(queries) > 0 {
|
||||
return "WHERE " + strings.Join(queries, " AND "), params
|
||||
}
|
||||
return "", params
|
||||
}
|
||||
|
||||
func (cr configRepository) rollback(content string, defErr error, tx *sqlx.Tx) error {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
return errors.Wrap(defErr, errors.Wrap(errors.New("failed to rollback at "+content), err))
|
||||
}
|
||||
|
||||
return defErr
|
||||
}
|
||||
|
||||
func insertChannels(domainID string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chans []dbChannel
|
||||
for _, ch := range channels {
|
||||
dbch, err := toDBChannel(domainID, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
q := `INSERT INTO channels (magistrala_channel, domain_id, name, metadata, parent_id, description, created_at, updated_at, updated_by, status)
|
||||
VALUES (:magistrala_channel, :domain_id, :name, :metadata, :parent_id, :description, :created_at, :updated_at, :updated_by, :status)`
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
|
||||
e = repoerr.ErrConflict
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertConnections(_ context.Context, cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, domain_id)
|
||||
VALUES (:config_id, :channel_id, :domain_id)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: cfg.ClientID,
|
||||
Channel: conn,
|
||||
DomainID: cfg.DomainID,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
_, err := tx.NamedExec(q, conns)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(domainID, id string, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `DELETE FROM connections
|
||||
WHERE config_id = $1 AND domain_id = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
var conn pgtype.TextArray
|
||||
if err := conn.Set(connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tx.Exec(q, id, domainID, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, domain_id)
|
||||
VALUES (:config_id, :channel_id, :domain_id)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: id,
|
||||
Channel: conn,
|
||||
DomainID: domainID,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(q, conns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(cleanupQuery)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
|
||||
return sql.NullString{
|
||||
String: s,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func nullTime(t time.Time) sql.NullTime {
|
||||
if t.IsZero() {
|
||||
return sql.NullTime{}
|
||||
}
|
||||
|
||||
return sql.NullTime{
|
||||
Time: t,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
DomainID string `db:"domain_id"`
|
||||
ClientID string `db:"magistrala_client"`
|
||||
ClientSecret string `db:"magistrala_secret"`
|
||||
Name sql.NullString `db:"name"`
|
||||
ClientCert sql.NullString `db:"client_cert"`
|
||||
ClientKey sql.NullString `db:"client_key"`
|
||||
CaCert sql.NullString `db:"ca_cert"`
|
||||
ExternalID string `db:"external_id"`
|
||||
ExternalKey string `db:"external_key"`
|
||||
Content sql.NullString `db:"content"`
|
||||
State bootstrap.State `db:"state"`
|
||||
}
|
||||
|
||||
func toDBConfig(cfg bootstrap.Config) dbConfig {
|
||||
return dbConfig{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
DomainID: cfg.DomainID,
|
||||
Name: nullString(cfg.Name),
|
||||
ClientCert: nullString(cfg.ClientCert),
|
||||
ClientKey: nullString(cfg.ClientKey),
|
||||
CaCert: nullString(cfg.CACert),
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Content: nullString(cfg.Content),
|
||||
State: cfg.State,
|
||||
}
|
||||
}
|
||||
|
||||
func toConfig(dbcfg dbConfig) bootstrap.Config {
|
||||
cfg := bootstrap.Config{
|
||||
ClientID: dbcfg.ClientID,
|
||||
ClientSecret: dbcfg.ClientSecret,
|
||||
DomainID: dbcfg.DomainID,
|
||||
ExternalID: dbcfg.ExternalID,
|
||||
ExternalKey: dbcfg.ExternalKey,
|
||||
State: dbcfg.State,
|
||||
}
|
||||
|
||||
if dbcfg.Name.Valid {
|
||||
cfg.Name = dbcfg.Name.String
|
||||
}
|
||||
|
||||
if dbcfg.Content.Valid {
|
||||
cfg.Content = dbcfg.Content.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientCert.Valid {
|
||||
cfg.ClientCert = dbcfg.ClientCert.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientKey.Valid {
|
||||
cfg.ClientKey = dbcfg.ClientKey.String
|
||||
}
|
||||
|
||||
if dbcfg.CaCert.Valid {
|
||||
cfg.CACert = dbcfg.CaCert.String
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type dbChannel struct {
|
||||
ID string `db:"magistrala_channel"`
|
||||
Name sql.NullString `db:"name"`
|
||||
DomainID sql.NullString `db:"domain_id"`
|
||||
Metadata string `db:"metadata"`
|
||||
Parent sql.NullString `db:"parent_id,omitempty"`
|
||||
Description string `db:"description,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
UpdatedBy sql.NullString `db:"updated_by,omitempty"`
|
||||
Status clients.Status `db:"status"`
|
||||
}
|
||||
|
||||
func toDBChannel(domainID string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
dbch := dbChannel{
|
||||
ID: ch.ID,
|
||||
Name: nullString(ch.Name),
|
||||
DomainID: nullString(domainID),
|
||||
Parent: nullString(ch.Parent),
|
||||
Description: ch.Description,
|
||||
CreatedAt: ch.CreatedAt,
|
||||
UpdatedAt: nullTime(ch.UpdatedAt),
|
||||
UpdatedBy: nullString(ch.UpdatedBy),
|
||||
Status: ch.Status,
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
return dbch, nil
|
||||
}
|
||||
|
||||
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
ch := bootstrap.Channel{
|
||||
ID: dbch.ID,
|
||||
Description: dbch.Description,
|
||||
CreatedAt: dbch.CreatedAt,
|
||||
Status: dbch.Status,
|
||||
}
|
||||
|
||||
if dbch.Name.Valid {
|
||||
ch.Name = dbch.Name.String
|
||||
}
|
||||
if dbch.DomainID.Valid {
|
||||
ch.DomainID = dbch.DomainID.String
|
||||
}
|
||||
if dbch.Parent.Valid {
|
||||
ch.Parent = dbch.Parent.String
|
||||
}
|
||||
if dbch.UpdatedBy.Valid {
|
||||
ch.UpdatedBy = dbch.UpdatedBy.String
|
||||
}
|
||||
if dbch.UpdatedAt.Valid {
|
||||
ch.UpdatedAt = dbch.UpdatedAt.Time
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type dbConnection struct {
|
||||
Config string `db:"config_id"`
|
||||
Channel string `db:"channel_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
}
|
||||
@@ -1,913 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/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{
|
||||
ClientID: "mg-client",
|
||||
ClientSecret: "mg-key",
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
DomainID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Channels: []bootstrap.Channel{
|
||||
{ID: "1", Name: "name 1", Metadata: map[string]any{"meta": 1.0}},
|
||||
{ID: "2", Name: "name 2", Metadata: map[string]any{"meta": 2.0}},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
}
|
||||
|
||||
channels = []string{"1", "2"}
|
||||
)
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
diff := "different"
|
||||
|
||||
duplicateClient := config
|
||||
duplicateClient.ExternalID = diff
|
||||
duplicateClient.ClientSecret = diff
|
||||
duplicateClient.Channels = []bootstrap.Channel{}
|
||||
|
||||
duplicateExternal := config
|
||||
duplicateExternal.ClientID = diff
|
||||
duplicateExternal.ClientSecret = diff
|
||||
duplicateExternal.Channels = []bootstrap.Channel{}
|
||||
|
||||
duplicateChannels := config
|
||||
duplicateChannels.ExternalID = diff
|
||||
duplicateChannels.ClientSecret = diff
|
||||
duplicateChannels.ClientID = diff
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a config",
|
||||
config: config,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Client ID",
|
||||
config: duplicateClient,
|
||||
connections: nil,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
connections: nil,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Channels",
|
||||
config: duplicateChannels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
id, err := repo.Save(context.Background(), tc.config, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
assert.Equal(t, id, tc.config.ClientID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ClientID, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
nonexistentConfID, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve config",
|
||||
domainID: c.DomainID,
|
||||
id: id,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve config with wrong domain ID ",
|
||||
domainID: "2",
|
||||
id: id,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing config",
|
||||
domainID: c.DomainID,
|
||||
id: nonexistentConfID.String(),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a config with invalid ID",
|
||||
domainID: c.DomainID,
|
||||
id: "invalid",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(context.Background(), tc.domainID, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
clientIDs := make([]string, numConfigs)
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
c := config
|
||||
|
||||
// Use UUID to prevent conflict errors.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = uid.String()
|
||||
c.Name = fmt.Sprintf("name %d", i)
|
||||
c.ClientID = uid.String()
|
||||
c.ClientSecret = uid.String()
|
||||
|
||||
clientIDs[i] = c.ClientID
|
||||
|
||||
if i%2 == 0 {
|
||||
c.State = bootstrap.Active
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
c.Channels = nil
|
||||
}
|
||||
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
clientID []string
|
||||
offset uint64
|
||||
limit uint64
|
||||
filter bootstrap.Filter
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all configs",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset of configs",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with wrong domain ID ",
|
||||
domainID: "2",
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all active configs ",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all with partial match filter",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve by valid clientIDs",
|
||||
domainID: config.DomainID,
|
||||
clientID: clientIDs,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
desc: "retrieve by non-existing clientID",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{"non-existing"},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(context.Background(), tc.domainID, tc.clientID, tc.filter, tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByExternalID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(context.Background(), tc.externalID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
config bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domainID ",
|
||||
config: wrongDomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
config: c,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.Update(context.Background(), tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
domainID string
|
||||
cert string
|
||||
certKey string
|
||||
ca string
|
||||
expectedConfig bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domain ID ",
|
||||
clientID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
domainID: wrongDomainID.DomainID,
|
||||
expectedConfig: bootstrap.Config{},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
clientID: c.ClientID,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
domainID: c.DomainID,
|
||||
expectedConfig: bootstrap.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientCert: "cert",
|
||||
CACert: "ca",
|
||||
ClientKey: "certKey",
|
||||
DomainID: c.DomainID,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.clientID, tc.cert, tc.certKey, tc.ca)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.expectedConfig, cfg, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.expectedConfig, cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err = uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.Channels = []bootstrap.Channel{}
|
||||
c2, err := repo.Save(context.Background(), c, []string{channels[0]})
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
domainID: config.DomainID,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
domainID: config.DomainID,
|
||||
id: c.ClientID,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections with existing channels",
|
||||
domainID: config.DomainID,
|
||||
id: c2,
|
||||
channels: nil,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
domainID: config.DomainID,
|
||||
id: c.ClientID,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateConnections(context.Background(), tc.domainID, tc.id, tc.channels, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
// Removal works the same for both existing and non-existing
|
||||
// (removed) config
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.Remove(context.Background(), c.DomainID, id)
|
||||
assert.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(context.Background(), c.DomainID, id)
|
||||
assert.True(t, errors.Contains(err, repoerr.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, repoerr.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong domain ID ",
|
||||
id: saved,
|
||||
domainID: "2",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state with wrong id",
|
||||
id: "wrong",
|
||||
domainID: c.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
state: bootstrap.Inactive,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeState(context.Background(), tc.domainID, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListExisting(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var chs []bootstrap.Channel
|
||||
chs = append(chs, config.Channels...)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
domainID: c.DomainID,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
domainID: c.DomainID,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
domainID: c.DomainID,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(context.Background(), tc.domainID, tc.connections)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: Got non-matching elements.", tc.desc))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.RemoveClient(context.Background(), saved)
|
||||
assert.Nil(t, err, fmt.Sprintf("an unexpected error occurred: %s\n", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
id := c.Channels[0].ID
|
||||
update := bootstrap.Channel{
|
||||
ID: id,
|
||||
Name: "update name",
|
||||
Metadata: map[string]any{"update": "metadata update"},
|
||||
}
|
||||
err = repo.UpdateChannel(context.Background(), update)
|
||||
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
var retrieved bootstrap.Channel
|
||||
for _, c := range cfg.Channels {
|
||||
if c.ID == id {
|
||||
retrieved = c
|
||||
break
|
||||
}
|
||||
}
|
||||
update.DomainID = retrieved.DomainID
|
||||
assert.Equal(t, update, retrieved, fmt.Sprintf("expected %s, go %s", update, retrieved))
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.RemoveChannel(context.Background(), c.Channels[0].ID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.NotContains(t, cfg.Channels, c.Channels[0], fmt.Sprintf("expected to remove channel %s from %s", c.Channels[0], cfg.Channels))
|
||||
}
|
||||
|
||||
func TestConnectClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.State = bootstrap.Inactive
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
wrongID := testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
connectedClient := c
|
||||
|
||||
randomClient := c
|
||||
randomClientID, _ := uuid.NewV4()
|
||||
randomClient.ClientID = randomClientID.String()
|
||||
|
||||
emptyClient := c
|
||||
emptyClient.ClientID = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "connect disconnected client",
|
||||
domainID: c.DomainID,
|
||||
id: saved,
|
||||
state: bootstrap.Inactive,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "connect already connected client",
|
||||
domainID: c.DomainID,
|
||||
id: connectedClient.ClientID,
|
||||
state: connectedClient.State,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "connect non-existent client",
|
||||
domainID: c.DomainID,
|
||||
id: wrongID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "connect random client",
|
||||
domainID: c.DomainID,
|
||||
id: randomClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "connect empty client",
|
||||
domainID: c.DomainID,
|
||||
id: emptyClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
for i, ch := range tc.channels {
|
||||
if i == 0 {
|
||||
err = repo.ConnectClient(context.Background(), ch.ID, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
|
||||
} else {
|
||||
_ = repo.ConnectClient(context.Background(), ch.ID, tc.id)
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.State = bootstrap.Inactive
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
wrongID := testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
connectedClient := c
|
||||
|
||||
randomClient := c
|
||||
randomClientID, _ := uuid.NewV4()
|
||||
randomClient.ClientID = randomClientID.String()
|
||||
|
||||
emptyClient := c
|
||||
emptyClient.ClientID = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "disconnect connected client",
|
||||
domainID: c.DomainID,
|
||||
id: connectedClient.ClientID,
|
||||
state: connectedClient.State,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect already disconnected client",
|
||||
domainID: c.DomainID,
|
||||
id: saved,
|
||||
state: bootstrap.Inactive,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect invalid client",
|
||||
domainID: c.DomainID,
|
||||
id: wrongID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect random client",
|
||||
domainID: c.DomainID,
|
||||
id: randomClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect empty client",
|
||||
domainID: c.DomainID,
|
||||
id: emptyClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
for _, ch := range tc.channels {
|
||||
err = repo.DisconnectClient(context.Background(), ch.ID, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected to be inactive when a connection is removed from %s", cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannels(ctx context.Context, repo bootstrap.ConfigRepository) error {
|
||||
for _, ch := range channels {
|
||||
if err := repo.RemoveChannel(ctx, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of bootstrap service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "configs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
mainflux_client TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
mainflux_key CHAR(36) UNIQUE NOT NULL,
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
content TEXT,
|
||||
client_cert TEXT,
|
||||
client_key TEXT,
|
||||
ca_cert TEXT,
|
||||
state BIGINT NOT NULL,
|
||||
PRIMARY KEY (mainflux_client, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS unknown_configs (
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
PRIMARY KEY (external_id, external_key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS channels (
|
||||
mainflux_channel TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
metadata JSON,
|
||||
PRIMARY KEY (mainflux_channel, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS connections (
|
||||
channel_id TEXT,
|
||||
channel_owner VARCHAR(256),
|
||||
config_id TEXT,
|
||||
config_owner VARCHAR(256),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_client, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE configs",
|
||||
"DROP TABLE channels",
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_2",
|
||||
Up: []string{
|
||||
"DROP TABLE IF EXISTS unknown_configs",
|
||||
},
|
||||
Down: []string{
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_3",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS parent_id VARCHAR(36)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS description VARCHAR(1024)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS created_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_by VARCHAR(254)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_4",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_client TO magistrala_client`,
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_key TO magistrala_secret`,
|
||||
`ALTER TABLE IF EXISTS channels RENAME COLUMN mainflux_channel TO magistrala_channel`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_5",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN owner TO domain_id`,
|
||||
`ALTER TABLE IF EXISTS channels RENAME COLUMN owner TO domain_id`,
|
||||
`ALTER TABLE IF EXISTS configs ADD CONSTRAINT configs_name_domain_id_key UNIQUE (name, domain_id)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_6",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS connections DROP CONSTRAINT IF EXISTS connections_pkey`,
|
||||
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS channel_owner`,
|
||||
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS config_owner`,
|
||||
`ALTER TABLE IF EXISTS connections ADD COLUMN IF NOT EXISTS domain_id VARCHAR(256) NOT NULL`,
|
||||
`ALTER TABLE IF EXISTS connections ADD CONSTRAINT connections_pkey PRIMARY KEY (channel_id, config_id, domain_id)`,
|
||||
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (channel_id, domain_id) REFERENCES channels (magistrala_channel, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (config_id, domain_id) REFERENCES configs (magistrala_client, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/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: "",
|
||||
}
|
||||
|
||||
if db, err = pgclient.Setup(dbConfig, *postgres.Migration()); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// bootstrapRes represent Magistrala Response to the Bootatrap request.
|
||||
// This is used as a response from ConfigReader and can easily be
|
||||
// replace with any other response format.
|
||||
type bootstrapRes struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Channels []channelRes `json:"channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
encKey []byte
|
||||
}
|
||||
|
||||
// NewConfigReader return new reader which is used to generate response
|
||||
// from the config.
|
||||
func NewConfigReader(encKey []byte) ConfigReader {
|
||||
return reader{encKey: encKey}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config, secure bool) (any, error) {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
}
|
||||
|
||||
res := bootstrapRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
Content: cfg.Content,
|
||||
ClientCert: cfg.ClientCert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
CACert: cfg.CACert,
|
||||
}
|
||||
if secure {
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.encrypt(b)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r reader) encrypt(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(r.encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(in))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
|
||||
return ciphertext, nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type readChan struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type readResp struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Channels []readChan `json:"channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func dec(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, errors.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(in, in)
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
cfg := bootstrap.Config{
|
||||
ClientID: "smq_id",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
ClientSecret: "smq_key",
|
||||
Channels: []bootstrap.Channel{
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]any{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
}
|
||||
ret := readResp{
|
||||
ClientID: "smq_id",
|
||||
ClientSecret: "smq_key",
|
||||
Channels: []readChan{
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]any{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
}
|
||||
|
||||
bin, err := json.Marshal(ret)
|
||||
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
|
||||
reader := bootstrap.NewConfigReader(encKey)
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
enc []byte
|
||||
secret bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "read a config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
desc: "read encrypted config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res, err := reader.ReadConfig(tc.config, tc.secret)
|
||||
assert.Nil(t, err, fmt.Sprintf("Reading config to succeed: %s.\n", err))
|
||||
|
||||
if tc.secret {
|
||||
d, err := dec(res.([]byte))
|
||||
assert.Nil(t, err, fmt.Sprintf("Decrypting expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, d, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, d))
|
||||
continue
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, b, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, b))
|
||||
resp, ok := res.(magistrala.Response)
|
||||
assert.True(t, ok, "If not encrypted, reader should return response.")
|
||||
assert.False(t, resp.Empty(), fmt.Sprintf("Response should not be empty %s.", err))
|
||||
assert.Equal(t, http.StatusOK, resp.Code(), "Default config response code should be 200.")
|
||||
}
|
||||
}
|
||||
@@ -1,503 +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"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
mgsdk "github.com/absmach/magistrala/pkg/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrClients indicates failure to communicate with Magistrala Clients service.
|
||||
// It can be due to networking error or invalid/unauthenticated request.
|
||||
ErrClients = errors.New("failed to receive response from Clients service")
|
||||
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
|
||||
ErrExternalKey = errors.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")
|
||||
|
||||
// ErrBootstrapState indicates an invalid bootstrap state.
|
||||
ErrBootstrapState = errors.NewRequestError("invalid bootstrap state")
|
||||
|
||||
// ErrNotInSameDomain indicates entities are not in the same domain.
|
||||
errNotInSameDomain = errors.New("entities are not in the same domain")
|
||||
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
errRemoveChannel = errors.New("failed to remove channel")
|
||||
errCreateClient = errors.New("failed to create client")
|
||||
errConnectClient = errors.New("failed to connect client")
|
||||
errDisconnectClient = errors.New("failed to disconnect client")
|
||||
errCheckChannels = errors.New("failed to check if channels exists")
|
||||
errConnectionChannels = errors.New("failed to check channels connections")
|
||||
errClientNotFound = errors.New("failed to find client")
|
||||
errUpdateCert = errors.New("failed to update cert")
|
||||
)
|
||||
|
||||
var _ Service = (*bootstrapService)(nil)
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Add adds new Client Config to the user identified by the provided token.
|
||||
Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error)
|
||||
|
||||
// View returns Client Config with given ID belonging to the user identified by the given token.
|
||||
View(ctx context.Context, session smqauthn.Session, id string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(ctx context.Context, session smqauthn.Session, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error
|
||||
|
||||
// List returns subset of Configs with given search params that belong to the
|
||||
// user identified by the given token.
|
||||
List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
Remove(ctx context.Context, session smqauthn.Session, id string) error
|
||||
|
||||
// Bootstrap returns Config to the Client with provided external ID using external key.
|
||||
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Client with given client ID and domain ID.
|
||||
ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error
|
||||
|
||||
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
|
||||
// handlers for events. That's why these methods surpass ownership check.
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(ctx context.Context, channel Channel) error
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(ctx context.Context, id string) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClientHandler changes state of the Config to active when connect event occurs.
|
||||
ConnectClientHandler(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClientHandler changes state of the Config to inactive when disconnect event occurs.
|
||||
DisconnectClientHandler(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
// as a JSON and consumed from the client side. The purpose of this interface
|
||||
// is to provide convenient way to generate custom configuration response
|
||||
// based on the specific Config which will be consumed by the client.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config, bool) (any, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
policies policies.Service
|
||||
configs ConfigRepository
|
||||
sdk mgsdk.SDK
|
||||
encKey []byte
|
||||
idProvider magistrala.IDProvider
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(policyService policies.Service, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp magistrala.IDProvider) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
policies: policyService,
|
||||
encKey: encKey,
|
||||
idProvider: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
|
||||
toConnect := bs.toIDList(cfg.Channels)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(ctx, session.DomainID, toConnect)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errCheckChannels, err)
|
||||
}
|
||||
|
||||
cfg.Channels, err = bs.connectionChannels(ctx, toConnect, bs.toIDList(existing), session.DomainID, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errConnectionChannels, err)
|
||||
}
|
||||
|
||||
id := cfg.ClientID
|
||||
mgClient, err := bs.client(ctx, session.DomainID, id, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errClientNotFound, err)
|
||||
}
|
||||
|
||||
for _, channel := range cfg.Channels {
|
||||
if channel.DomainID != mgClient.DomainID {
|
||||
return Config{}, errors.Wrap(svcerr.ErrMalformedEntity, errNotInSameDomain)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.ClientID = mgClient.ID
|
||||
cfg.DomainID = session.DomainID
|
||||
cfg.State = Inactive
|
||||
cfg.ClientSecret = mgClient.Credentials.Secret
|
||||
|
||||
saved, err := bs.configs.Save(ctx, cfg, toConnect)
|
||||
if err != nil {
|
||||
// If id is empty, then a new client has been created function - bs.client(id, token)
|
||||
// So, on bootstrap config save error , delete the newly created client.
|
||||
if id == "" {
|
||||
if errT := bs.sdk.DeleteClient(ctx, cfg.ClientID, cfg.DomainID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
return Config{}, errors.Wrap(ErrAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.ClientID = saved
|
||||
cfg.Channels = append(cfg.Channels, existing...)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(ctx context.Context, session smqauthn.Session, cfg Config) error {
|
||||
cfg.DomainID = session.DomainID
|
||||
if err := bs.configs.Update(ctx, cfg); err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error) {
|
||||
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(ctx, session.DomainID, connections)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
channels, err := bs.connectionChannels(ctx, connections, bs.toIDList(existing), session.DomainID, token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
cfg.Channels = channels
|
||||
var connect, disconnect []string
|
||||
|
||||
if cfg.State == Active {
|
||||
connect = add
|
||||
disconnect = remove
|
||||
}
|
||||
|
||||
for _, c := range disconnect {
|
||||
if err := bs.sdk.DisconnectClients(ctx, c, []string{id}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
if errors.Contains(err, repoerr.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range connect {
|
||||
conIDs := mgsdk.Connection{
|
||||
ChannelIDs: []string{c},
|
||||
ClientIDs: []string{id},
|
||||
Types: []string{"Publish", "Subscribe"},
|
||||
}
|
||||
if err := bs.sdk.Connect(ctx, conIDs, session.DomainID, token); err != nil {
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
if err := bs.configs.UpdateConnections(ctx, session.DomainID, id, channels, connections); err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) listClientIDs(ctx context.Context, userID string) ([]string, error) {
|
||||
tids, err := bs.policies.ListAllObjects(ctx, policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: userID,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(svcerr.ErrNotFound, err)
|
||||
}
|
||||
return tids.Policies, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, []string{}, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
// Handle non-admin users
|
||||
clientIDs, err := bs.listClientIDs(ctx, session.DomainUserID)
|
||||
if err != nil {
|
||||
return ConfigsPage{}, errors.Wrap(svcerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
if len(clientIDs) == 0 {
|
||||
return ConfigsPage{
|
||||
Total: 0,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: []Config{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, clientIDs, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := bs.configs.Remove(ctx, session.DomainID, id); err != nil {
|
||||
return errors.Wrap(errRemoveBootstrap, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(ctx, externalID)
|
||||
if err != nil {
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrExternalKeySecure, err)
|
||||
}
|
||||
externalKey = dec
|
||||
}
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, ErrExternalKey
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
|
||||
if cfg.State == state {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range cfg.Channels {
|
||||
if err := bs.sdk.ConnectClients(ctx, c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
// Ignore conflict errors as they indicate the connection already exists.
|
||||
if errors.Contains(err, svcerr.ErrConflict) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
case Inactive:
|
||||
for _, c := range cfg.Channels {
|
||||
if err := bs.sdk.DisconnectClients(ctx, c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
if errors.Contains(err, repoerr.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := bs.configs.ChangeState(ctx, session.DomainID, id, state); err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
|
||||
if err := bs.configs.UpdateChannel(ctx, channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveClient(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveChannel(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := bs.configs.ConnectClient(ctx, channelID, clientID); err != nil {
|
||||
return errors.Wrap(errConnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := bs.configs.DisconnectClient(ctx, channelID, clientID); err != nil {
|
||||
return errors.Wrap(errDisconnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Method client retrieves Magistrala Client creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) client(ctx context.Context, domainID, id, token string) (mgsdk.Client, error) {
|
||||
// If Client ID is not provided, then create new client.
|
||||
if id == "" {
|
||||
id, err := bs.idProvider.ID()
|
||||
if err != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(errCreateClient, err)
|
||||
}
|
||||
client, sdkErr := bs.sdk.CreateClient(ctx, mgsdk.Client{ID: id, Name: "Bootstrapped Client " + id}, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(errCreateClient, sdkErr)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
// If Client ID is provided, then retrieve client
|
||||
client, sdkErr := bs.sdk.Client(ctx, id, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(ErrClients, sdkErr)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(ctx context.Context, channels, existing []string, domainID, token string) ([]Channel, error) {
|
||||
add := make(map[string]bool, len(channels))
|
||||
for _, ch := range channels {
|
||||
add[ch] = true
|
||||
}
|
||||
|
||||
for _, ch := range existing {
|
||||
if add[ch] {
|
||||
delete(add, ch)
|
||||
}
|
||||
}
|
||||
|
||||
var ret []Channel
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(ctx, id, domainID, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
DomainID: ch.DomainID,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Method updateList accepts config and channel IDs and returns three lists:
|
||||
// 1) IDs of Channels to be added
|
||||
// 2) IDs of Channels to be removed
|
||||
// 3) IDs of common Channels for these two configs.
|
||||
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
|
||||
disconnect := make(map[string]bool, len(cfg.Channels))
|
||||
for _, c := range cfg.Channels {
|
||||
disconnect[c.ID] = true
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
if disconnect[c] {
|
||||
// Don't disconnect common elements.
|
||||
delete(disconnect, c)
|
||||
continue
|
||||
}
|
||||
// Connect new elements.
|
||||
add = append(add, c)
|
||||
}
|
||||
|
||||
for v := range disconnect {
|
||||
remove = append(remove, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
var ret []string
|
||||
for _, ch := range channels {
|
||||
ret = append(ret, ch.ID)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (bs bootstrapService) dec(in string) (string, error) {
|
||||
ciphertext, err := hex.DecodeString(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", err
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return string(ciphertext), nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
// Inactive Client is created, but not able to exchange messages using Magistrala.
|
||||
Inactive State = iota
|
||||
// Active Client is created, configured, and whitelisted.
|
||||
Active
|
||||
)
|
||||
|
||||
// State represents corresponding Magistrala Client state. The possible Config States
|
||||
// as well as description of what that State represents are given in the table:
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Client is created, but isn't able to communicate over Magistrala |
|
||||
// | Active | Client is able to communicate using Magistrala |.
|
||||
type State int
|
||||
|
||||
// String returns string representation of State.
|
||||
func (s State) String() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing provides tracing instrumentation for Magistrala Users service.
|
||||
//
|
||||
// This package provides tracing middleware for Magistrala Users service.
|
||||
// It can be used to trace incoming requests and add tracing capabilities to
|
||||
// Magistrala Users service.
|
||||
//
|
||||
// For more details about tracing instrumentation for Magistrala messaging refer
|
||||
// to the documentation at https://magistrala.absmach.eu/docs/.
|
||||
package tracing
|
||||
@@ -1,182 +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("client_id", cfg.ClientID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("external_id", cfg.ExternalID),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("state", cfg.State.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View traces the "View" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_view_user", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update traces the "Update" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_user", trace.WithAttributes(
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("client_id", cfg.ClientID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert traces the "UpdateCert" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_cert", trace.WithAttributes(
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections traces the "UpdateConnections" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_connections", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
attribute.StringSlice("connections", connections),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List traces the "List" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_list_users", trace.WithAttributes(
|
||||
attribute.Int64("offset", int64(offset)),
|
||||
attribute.Int64("limit", int64(limit)),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove traces the "Remove" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_user", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap traces the "Bootstrap" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_bootstrap_user", trace.WithAttributes(
|
||||
attribute.String("external_key", externalKey),
|
||||
attribute.String("external_id", externalID),
|
||||
attribute.Bool("secure", secure),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState traces the "ChangeState" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_change_state", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
attribute.String("state", state.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler traces the "UpdateChannelHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_channel_handler", trace.WithAttributes(
|
||||
attribute.String("id", channel.ID),
|
||||
attribute.String("name", channel.Name),
|
||||
attribute.String("description", channel.Description),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler traces the "RemoveConfigHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_config_handler", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler traces the "RemoveChannelHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_channel_handler", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler traces the "ConnectClientHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_connect_client_handler", trace.WithAttributes(
|
||||
attribute.String("channel_id", channelID),
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler traces the "DisconnectClientHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_disconnect_client_handler", trace.WithAttributes(
|
||||
attribute.String("channel_id", channelID),
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type loggingMiddleware struct {
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
// LoggingMiddleware adds logging facilities to the service.
|
||||
func LoggingMiddleware(svc certs.Service, logger *slog.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type metricsMiddleware struct {
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
// MetricsMiddleware instruments service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metrics.Histogram) certs.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
# Channels
|
||||
|
||||
The Channels service is a core component of Magistrala that manages communication channels between devices and applications. It handles channel creation, configuration, access control and message routing within the Magistrala ecosystem.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the following environment variables (unset variables use default values):
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------- | --------------------------------------------- | ------------------------------ |
|
||||
| `MG_CHANNELS_LOG_LEVEL` | Log level (debug, info, warn, error) | info |
|
||||
| `MG_CHANNELS_HTTP_HOST` | HTTP host for Channels service | localhost |
|
||||
| `MG_CHANNELS_HTTP_PORT` | HTTP port for Channels service | 9005 |
|
||||
| `MG_CHANNELS_SERVER_CERT` | Path to PEM encoded server certificate | "" |
|
||||
| `MG_CHANNELS_SERVER_KEY` | Path to PEM encoded server key file | "" |
|
||||
| `MG_CHANNELS_GRPC_HOST` | gRPC host for Channels service | localhost |
|
||||
| `MG_CHANNELS_GRPC_PORT` | gRPC port for Channels service | 7005 |
|
||||
| `MG_CHANNELS_DB_HOST` | Database host address | localhost |
|
||||
| `MG_CHANNELS_DB_PORT` | Database port | 5432 |
|
||||
| `MG_CHANNELS_DB_USER` | Database user | magistrala |
|
||||
| `MG_CHANNELS_DB_PASS` | Database password | magistrala |
|
||||
| `MG_CHANNELS_DB_NAME` | Name of the database used by the service | channels |
|
||||
| `MG_CHANNELS_DB_SSL_MODE` | Database connection SSL mode | disable |
|
||||
| `MG_CHANNELS_CACHE_URL` | Cache database URL | <redis://localhost:6379/0> |
|
||||
| `MG_JAEGER_URL` | Jaeger tracing server URL | <http://jaeger:4318/v1/traces> |
|
||||
| `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | true |
|
||||
|
||||
## Features
|
||||
|
||||
- **Channel Management**: Create, update, delete and list channels
|
||||
- **Access Control**: Manage channel permissions and user access
|
||||
- **Message Routing**: Route messages between connected devices and services
|
||||
- **Channel Groups**: Organize channels into logical groups
|
||||
- **Metadata Support**: Attach custom metadata to channels
|
||||
- **Real-time Updates**: Live channel state synchronization
|
||||
|
||||
## Architecture
|
||||
|
||||
The service is built using:
|
||||
|
||||
- **Go**: Core service implementation
|
||||
- **gRPC**: Inter-service communication
|
||||
- **PostgreSQL**: Primary data storage
|
||||
- **Redis**: Caching and pub/sub messaging
|
||||
- **Docker**: Containerized deployment
|
||||
|
||||
### Channels Table
|
||||
|
||||
| Column | Type | Description |
|
||||
| ----------------- | ------------- | ----------------------------------------------------- |
|
||||
| `id` | VARCHAR(36) | UUID of the channel (primary key) |
|
||||
| `name` | VARCHAR(1024) | Human-readable name |
|
||||
| `domain_id` | VARCHAR(36) | Domain to which the channel belongs |
|
||||
| `parent_group_id` | VARCHAR(36) | Optional group parent |
|
||||
| `tags` | TEXT[] | Array of tags |
|
||||
| `metadata` | JSONB | Free-form structured metadata |
|
||||
| `created_by` | VARCHAR(254) | User that created the channel |
|
||||
| `created_at` | TIMESTAMPTZ | Timestamp of creation |
|
||||
| `updated_at` | TIMESTAMPTZ | Timestamp of last update |
|
||||
| `updated_by` | VARCHAR(254) | User that performed last update |
|
||||
| `status` | SMALLINT | 0 = enabled, 1 = disabled |
|
||||
| `route` | VARCHAR(36) | Optional route identifier unique within domain if set |
|
||||
|
||||
### Connections Table
|
||||
|
||||
| Column | Type | Description |
|
||||
| ------------ | ----------- | ----------------------------------------------- |
|
||||
| `channel_id` | VARCHAR(36) | Channel UUID |
|
||||
| `domain_id` | VARCHAR(36) | Domain of channel and client |
|
||||
| `client_id` | VARCHAR(36) | Client UUID |
|
||||
| `type` | SMALLINT | Connection type: `1 = Publish`, `2 = Subscribe` |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service is available as a Docker container. Refer to the Docker Compose section for the `channels` service in `docker-compose.yaml` for deployment configuration.
|
||||
|
||||
To build and run locally:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/magistrala
|
||||
cd magistrala
|
||||
|
||||
# compile the channels
|
||||
make channels
|
||||
|
||||
make install
|
||||
|
||||
MG_CHANNELS_HTTP_HOST=localhost \
|
||||
MG_CHANNELS_HTTP_PORT=9005 \
|
||||
MG_CHANNELS_DB_HOST=localhost \
|
||||
MG_CHANNELS_DB_PORT=5432 \
|
||||
MG_CHANNELS_DB_USER=magistrala \MG_CHANNELS_DB_PASS=magistrala \MG_CHANNELS_DB_NAME=channels \
|
||||
$GOBIN/magistrala-channels
|
||||
```
|
||||
|
||||
### Running the Service
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export MQ_CHANNELS_DB_HOST=localhost
|
||||
export MQ_CHANNELS_DB_PORT=5432
|
||||
|
||||
# Run the service
|
||||
go run cmd/main.go
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```bash
|
||||
docker run -p 8180:8180 magistrala/channels
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run unit tests
|
||||
go test ./...
|
||||
|
||||
# Run integration tests
|
||||
make test-integration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Channels service supports the following operations:
|
||||
|
||||
| Operation | Description |
|
||||
| --------------- | -------------------------------------------- |
|
||||
| `create` | Create a new channel |
|
||||
| `list` | Retrieve all channels (paged) |
|
||||
| `get` | Retrieve a single channel by ID |
|
||||
| `update` | Update a channel’s name & metadata |
|
||||
| `delete` | Permanently delete a channel |
|
||||
| `enable` | Enable a previously disabled channel |
|
||||
| `disable` | Disable an active channel |
|
||||
| `set-parent` | Assign a parent group to a channel |
|
||||
| `remove-parent` | Remove parent group from a channel |
|
||||
| `connect` | Connect one or more clients to channels |
|
||||
| `disconnect` | Disconnect one or more clients from channels |
|
||||
|
||||
### Example: Create a Channel
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9005/<domainID>/channels \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "myChannel",
|
||||
"metadata": { "location": "lab" },
|
||||
"route": "sensor-data",
|
||||
"tags": ["sensor","edge"],
|
||||
"status": "enabled"
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Connect Clients & Channels
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9005/<domainID>/channels/connect \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"channel_ids": ["<chanID1>", "<chanID2>"],
|
||||
"client_ids": ["<clientID1>", "<clientID2>"],
|
||||
"types": ["publish", "subscribe"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Disconnect Clients from a Channel
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9005/<domainID>/channels/disconnect \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"channel_ids": ["<chanID>"],
|
||||
"client_ids": ["<clientID>"],
|
||||
"types": ["publish"]
|
||||
}'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use tags and metadata to manage and categorize channels (e.g., environment, region, purpose).
|
||||
- Assign `route` thoughtfully when channels need a predictable identifier.
|
||||
- Keep channel hierarchies shallow for easier navigation (avoid deep nesting unless required).
|
||||
- Use `disable` rather than immediate delete when you want to suspend a channel temporarily.
|
||||
- Clean up unused connections: regularly review which clients are connected to channels and remove stale links.
|
||||
- Enforce minimal privileges: only allow clients to connect to channels they truly need.
|
||||
- Monitoring: use the `/health` endpoint and version metadata for service stability.
|
||||
|
||||
## Versioning & Health Check
|
||||
|
||||
The Channels service exposes a `/health` endpoint to provide operational status and version info.
|
||||
|
||||
### Health Check Request
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:9005/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "pass",
|
||||
"version": "0.18.0",
|
||||
"commit": "<commit-hash>",
|
||||
"description": "channels service",
|
||||
"build_time": "2025-11-19T..."
|
||||
}
|
||||
```
|
||||
@@ -1,224 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
|
||||
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const svcName = "channels.v1.ChannelsService"
|
||||
|
||||
var _ grpcChannelsV1.ChannelsServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
timeout time.Duration
|
||||
authorize endpoint.Endpoint
|
||||
removeClientConnections endpoint.Endpoint
|
||||
unsetParentGroupFromChannels endpoint.Endpoint
|
||||
retrieveEntity endpoint.Endpoint
|
||||
retrieveIDByRoute endpoint.Endpoint
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(conn *grpc.ClientConn, timeout time.Duration) grpcChannelsV1.ChannelsServiceClient {
|
||||
return &grpcClient{
|
||||
authorize: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Authorize",
|
||||
encodeAuthorizeRequest,
|
||||
decodeAuthorizeResponse,
|
||||
grpcChannelsV1.AuthzRes{},
|
||||
).Endpoint(),
|
||||
removeClientConnections: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RemoveClientConnections",
|
||||
encodeRemoveClientConnectionsRequest,
|
||||
decodeRemoveClientConnectionsResponse,
|
||||
grpcChannelsV1.RemoveClientConnectionsRes{},
|
||||
).Endpoint(),
|
||||
unsetParentGroupFromChannels: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"UnsetParentGroupFromChannels",
|
||||
encodeUnsetParentGroupFromChannelsRequest,
|
||||
decodeUnsetParentGroupFromChannelsResponse,
|
||||
grpcChannelsV1.UnsetParentGroupFromChannelsRes{},
|
||||
).Endpoint(),
|
||||
retrieveEntity: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RetrieveEntity",
|
||||
encodeRetrieveEntityRequest,
|
||||
decodeRetrieveEntityResponse,
|
||||
grpcCommonV1.RetrieveEntityRes{},
|
||||
).Endpoint(),
|
||||
retrieveIDByRoute: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RetrieveIDByRoute",
|
||||
encodeRetrieveIDByRouteRequest,
|
||||
decodeRetrieveIDByRouteResponse,
|
||||
grpcCommonV1.RetrieveEntityRes{},
|
||||
).Endpoint(),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client grpcClient) Authorize(ctx context.Context, req *grpcChannelsV1.AuthzReq, _ ...grpc.CallOption) (r *grpcChannelsV1.AuthzRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authorize(ctx, authorizeReq{
|
||||
domainID: req.GetDomainId(),
|
||||
clientID: req.GetClientId(),
|
||||
clientType: req.GetClientType(),
|
||||
channelID: req.GetChannelId(),
|
||||
connType: connections.ConnType(req.GetType()),
|
||||
})
|
||||
if err != nil {
|
||||
return &grpcChannelsV1.AuthzRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
|
||||
return &grpcChannelsV1.AuthzRes{Authorized: ar.authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(authorizeReq)
|
||||
|
||||
return &grpcChannelsV1.AuthzReq{
|
||||
DomainId: req.domainID,
|
||||
ClientId: req.clientID,
|
||||
ClientType: req.clientType,
|
||||
ChannelId: req.channelID,
|
||||
Type: uint32(req.connType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(*grpcChannelsV1.AuthzRes)
|
||||
|
||||
return authorizeRes{authorized: res.GetAuthorized()}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) RemoveClientConnections(ctx context.Context, req *grpcChannelsV1.RemoveClientConnectionsReq, _ ...grpc.CallOption) (r *grpcChannelsV1.RemoveClientConnectionsRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
if _, err := client.removeClientConnections(ctx, req); err != nil {
|
||||
return &grpcChannelsV1.RemoveClientConnectionsRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return &grpcChannelsV1.RemoveClientConnectionsRes{}, nil
|
||||
}
|
||||
|
||||
func encodeRemoveClientConnectionsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcChannelsV1.RemoveClientConnectionsReq), nil
|
||||
}
|
||||
|
||||
func decodeRemoveClientConnectionsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcChannelsV1.RemoveClientConnectionsRes), nil
|
||||
}
|
||||
|
||||
func (client grpcClient) UnsetParentGroupFromChannels(ctx context.Context, req *grpcChannelsV1.UnsetParentGroupFromChannelsReq, _ ...grpc.CallOption) (r *grpcChannelsV1.UnsetParentGroupFromChannelsRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
if _, err := client.unsetParentGroupFromChannels(ctx, req); err != nil {
|
||||
return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, nil
|
||||
}
|
||||
|
||||
func encodeUnsetParentGroupFromChannelsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcChannelsV1.UnsetParentGroupFromChannelsReq), nil
|
||||
}
|
||||
|
||||
func decodeUnsetParentGroupFromChannelsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcChannelsV1.UnsetParentGroupFromChannelsRes), nil
|
||||
}
|
||||
|
||||
func (client grpcClient) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntityRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.retrieveEntity(ctx, req)
|
||||
if err != nil {
|
||||
return &grpcCommonV1.RetrieveEntityRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func encodeRetrieveEntityRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcCommonV1.RetrieveEntityReq), nil
|
||||
}
|
||||
|
||||
func decodeRetrieveEntityResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func (client grpcClient) RetrieveIDByRoute(ctx context.Context, req *grpcCommonV1.RetrieveIDByRouteReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntityRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.retrieveIDByRoute(ctx, req)
|
||||
if err != nil {
|
||||
return &grpcCommonV1.RetrieveEntityRes{}, decodeError(err)
|
||||
}
|
||||
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func encodeRetrieveIDByRouteRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
return grpcReq.(*grpcCommonV1.RetrieveIDByRouteReq), nil
|
||||
}
|
||||
|
||||
func decodeRetrieveIDByRouteResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func decodeError(err error) error {
|
||||
if st, ok := status.FromError(err); ok {
|
||||
switch st.Code() {
|
||||
case codes.Unauthenticated:
|
||||
return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message()))
|
||||
case codes.PermissionDenied:
|
||||
return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message()))
|
||||
case codes.InvalidArgument:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message()))
|
||||
case codes.FailedPrecondition:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message()))
|
||||
case codes.NotFound:
|
||||
return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message()))
|
||||
case codes.AlreadyExists:
|
||||
return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message()))
|
||||
case codes.OK:
|
||||
if msg := st.Message(); msg != "" {
|
||||
return errors.Wrap(errors.ErrUnidentified, errors.New(msg))
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message()))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of Auth service gRPC API.
|
||||
package grpc
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ch "github.com/absmach/magistrala/channels"
|
||||
channels "github.com/absmach/magistrala/channels/private"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func authorizeEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(authorizeReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.Authorize(ctx, ch.AuthzReq{
|
||||
DomainID: req.domainID,
|
||||
ClientID: req.clientID,
|
||||
ClientType: req.clientType,
|
||||
ChannelID: req.channelID,
|
||||
Type: req.connType,
|
||||
}); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
return authorizeRes{authorized: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeClientConnectionsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(removeClientConnectionsReq)
|
||||
|
||||
if err := svc.RemoveClientConnections(ctx, req.clientID); err != nil {
|
||||
return removeClientConnectionsRes{}, err
|
||||
}
|
||||
|
||||
return removeClientConnectionsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func unsetParentGroupFromChannelsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(unsetParentGroupFromChannelsReq)
|
||||
|
||||
if err := svc.UnsetParentGroupFromChannels(ctx, req.parentGroupID); err != nil {
|
||||
return unsetParentGroupFromChannelsRes{}, err
|
||||
}
|
||||
|
||||
return unsetParentGroupFromChannelsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveEntityEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(retrieveEntityReq)
|
||||
channel, err := svc.RetrieveByID(ctx, req.Id)
|
||||
if err != nil {
|
||||
return retrieveEntityRes{}, err
|
||||
}
|
||||
|
||||
return retrieveEntityRes{id: channel.ID, domain: channel.Domain, parentGroup: channel.ParentGroup, status: uint8(channel.Status)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveIDByRouteEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(retrieveIDByRouteReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return retrieveIDByRouteRes{}, err
|
||||
}
|
||||
|
||||
id, err := svc.RetrieveIDByRoute(ctx, req.route, req.domainID)
|
||||
if err != nil {
|
||||
return retrieveIDByRouteRes{}, err
|
||||
}
|
||||
|
||||
return retrieveIDByRouteRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
|
||||
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
ch "github.com/absmach/magistrala/channels"
|
||||
grpcapi "github.com/absmach/magistrala/channels/api/grpc"
|
||||
"github.com/absmach/magistrala/channels/private/mocks"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const port = 7005
|
||||
|
||||
var (
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
validChannel = ch.Channel{
|
||||
ID: validID,
|
||||
Domain: testsutil.GenerateUUID(&testing.T{}),
|
||||
Status: channels.EnabledStatus,
|
||||
}
|
||||
)
|
||||
|
||||
func startGRPCServer(svc *mocks.Service, port int) *grpc.Server {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to obtain port: %s", err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
grpcChannelsV1.RegisterChannelsServiceServer(server, grpcapi.NewServer(svc))
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil {
|
||||
panic(fmt.Sprintf("failed to serve: %s", err))
|
||||
}
|
||||
}()
|
||||
return server
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
clientID string
|
||||
clientType string
|
||||
channelID string
|
||||
connType connections.ConnType
|
||||
err error
|
||||
authzErr error
|
||||
res *grpcChannelsV1.AuthzRes
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "authorize successfully",
|
||||
domainID: validID,
|
||||
clientID: validID,
|
||||
clientType: policies.UserType,
|
||||
channelID: validID,
|
||||
connType: connections.Publish,
|
||||
res: &grpcChannelsV1.AuthzRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize with authorization error",
|
||||
domainID: validID,
|
||||
clientID: validID,
|
||||
clientType: policies.UserType,
|
||||
channelID: validID,
|
||||
connType: connections.Publish,
|
||||
res: &grpcChannelsV1.AuthzRes{Authorized: false},
|
||||
authzErr: svcerr.ErrAuthorization,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize withnot found error",
|
||||
domainID: validID,
|
||||
clientID: validID,
|
||||
clientType: policies.UserType,
|
||||
channelID: validID,
|
||||
connType: connections.Publish,
|
||||
res: &grpcChannelsV1.AuthzRes{Authorized: false},
|
||||
authzErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
authReq := ch.AuthzReq{
|
||||
DomainID: tc.domainID,
|
||||
ClientID: tc.clientID,
|
||||
ClientType: tc.clientType,
|
||||
ChannelID: tc.channelID,
|
||||
Type: tc.connType,
|
||||
}
|
||||
svcCall := svc.On("Authorize", mock.Anything, authReq).Return(tc.authzErr)
|
||||
res, err := client.Authorize(context.Background(), &grpcChannelsV1.AuthzReq{
|
||||
DomainId: tc.domainID,
|
||||
ClientId: tc.clientID,
|
||||
ClientType: tc.clientType,
|
||||
ChannelId: tc.channelID,
|
||||
Type: uint32(tc.connType),
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.res, res, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.res, res))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClientConnections(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "remove client connections successfully",
|
||||
clientID: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove client connections with error",
|
||||
clientID: validID,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("RemoveClientConnections", mock.Anything, tc.clientID).Return(tc.err)
|
||||
res, err := client.RemoveClientConnections(context.Background(), &grpcChannelsV1.RemoveClientConnectionsReq{
|
||||
ClientId: tc.clientID,
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, &grpcChannelsV1.RemoveClientConnectionsRes{}, res)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsetParentGroupFromChannelsEndpoint(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
parentGroupID string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "unset parent group from channels successfully",
|
||||
parentGroupID: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "unset parent group from channels authorization error",
|
||||
parentGroupID: validID,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "unset parent group from channels with not found error",
|
||||
parentGroupID: validID,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("UnsetParentGroupFromChannels", mock.Anything, tc.parentGroupID).Return(tc.err)
|
||||
res, err := client.UnsetParentGroupFromChannels(context.Background(), &grpcChannelsV1.UnsetParentGroupFromChannelsReq{
|
||||
ParentGroupId: tc.parentGroupID,
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, res)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveEntity(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
svcRes ch.Channel
|
||||
resp *grpcCommonV1.RetrieveEntityRes
|
||||
code codes.Code
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve entity successfully",
|
||||
id: validID,
|
||||
svcRes: validChannel,
|
||||
resp: &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: validChannel.ID,
|
||||
DomainId: validChannel.Domain,
|
||||
ParentGroupId: validChannel.ParentGroup,
|
||||
Status: uint32(validChannel.Status),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity with error",
|
||||
id: validID,
|
||||
resp: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("RetrieveByID", mock.Anything, tc.id).Return(tc.svcRes, tc.err)
|
||||
res, err := client.RetrieveEntity(context.Background(), &grpcCommonV1.RetrieveEntityReq{
|
||||
Id: tc.id,
|
||||
})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.resp.Entity, res.Entity)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveIDByRoute(t *testing.T) {
|
||||
svc := new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
defer server.GracefulStop()
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
client := grpcapi.NewClient(conn, time.Second)
|
||||
|
||||
validRoute := "validRoute"
|
||||
domainID := testsutil.GenerateUUID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
retrieveReq *grpcCommonV1.RetrieveIDByRouteReq
|
||||
svcRes string
|
||||
svcErr error
|
||||
retrieveRes *grpcCommonV1.RetrieveEntityRes
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve entity by route successfully",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: validRoute,
|
||||
DomainId: domainID,
|
||||
},
|
||||
svcRes: validID,
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: validID,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity by route with empty route",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: "",
|
||||
DomainId: domainID,
|
||||
},
|
||||
svcRes: "",
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: apiutil.ErrMissingRoute,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity by route with empty domain ID",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: validRoute,
|
||||
DomainId: "",
|
||||
},
|
||||
svcRes: "",
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "retrieve entity by route with invalid route",
|
||||
retrieveReq: &grpcCommonV1.RetrieveIDByRouteReq{
|
||||
Route: "invalidRoute",
|
||||
DomainId: domainID,
|
||||
},
|
||||
svcRes: "",
|
||||
svcErr: svcerr.ErrNotFound,
|
||||
retrieveRes: &grpcCommonV1.RetrieveEntityRes{},
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("RetrieveIDByRoute", mock.Anything, tc.retrieveReq.Route, tc.retrieveReq.DomainId).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := client.RetrieveIDByRoute(context.Background(), tc.retrieveReq)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.retrieveRes.Entity, res.Entity)
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
)
|
||||
|
||||
var errDomainID = errors.New("domain id required for users")
|
||||
|
||||
type authorizeReq struct {
|
||||
domainID string
|
||||
channelID string
|
||||
clientID string
|
||||
clientType string
|
||||
connType connections.ConnType
|
||||
}
|
||||
|
||||
func (req authorizeReq) validate() error {
|
||||
if req.clientType == policies.UserType && req.domainID == "" {
|
||||
return errDomainID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type removeClientConnectionsReq struct {
|
||||
clientID string
|
||||
}
|
||||
|
||||
type unsetParentGroupFromChannelsReq struct {
|
||||
parentGroupID string
|
||||
}
|
||||
|
||||
type retrieveEntityReq struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
type retrieveIDByRouteReq struct {
|
||||
route string
|
||||
domainID string
|
||||
}
|
||||
|
||||
func (req retrieveIDByRouteReq) validate() error {
|
||||
if req.route == "" {
|
||||
return apiutil.ErrMissingRoute
|
||||
}
|
||||
if req.domainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
type authorizeRes struct {
|
||||
authorized bool
|
||||
}
|
||||
|
||||
type removeClientConnectionsRes struct{}
|
||||
|
||||
type unsetParentGroupFromChannelsRes struct{}
|
||||
|
||||
type channelBasic struct {
|
||||
id string
|
||||
domain string
|
||||
parentGroup string
|
||||
status uint8
|
||||
}
|
||||
|
||||
type retrieveEntityRes channelBasic
|
||||
|
||||
type retrieveIDByRouteRes struct {
|
||||
id string
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
|
||||
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
smqauth "github.com/absmach/magistrala/auth"
|
||||
channels "github.com/absmach/magistrala/channels/private"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ grpcChannelsV1.ChannelsServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
grpcChannelsV1.UnimplementedChannelsServiceServer
|
||||
authorize kitgrpc.Handler
|
||||
removeClientConnections kitgrpc.Handler
|
||||
unsetParentGroupFromChannels kitgrpc.Handler
|
||||
retrieveEntity kitgrpc.Handler
|
||||
retrieveIDByRoute kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new AuthServiceServer instance.
|
||||
func NewServer(svc channels.Service) grpcChannelsV1.ChannelsServiceServer {
|
||||
return &grpcServer{
|
||||
authorize: kitgrpc.NewServer(
|
||||
authorizeEndpoint(svc),
|
||||
decodeAuthorizeRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
removeClientConnections: kitgrpc.NewServer(
|
||||
removeClientConnectionsEndpoint(svc),
|
||||
decodeRemoveClientConnectionsRequest,
|
||||
encodeRemoveClientConnectionsResponse,
|
||||
),
|
||||
unsetParentGroupFromChannels: kitgrpc.NewServer(
|
||||
unsetParentGroupFromChannelsEndpoint(svc),
|
||||
decodeUnsetParentGroupFromChannelsRequest,
|
||||
encodeUnsetParentGroupFromChannelsResponse,
|
||||
),
|
||||
retrieveEntity: kitgrpc.NewServer(
|
||||
retrieveEntityEndpoint(svc),
|
||||
decodeRetrieveEntityRequest,
|
||||
encodeRetrieveEntityResponse,
|
||||
),
|
||||
retrieveIDByRoute: kitgrpc.NewServer(
|
||||
retrieveIDByRouteEndpoint(svc),
|
||||
decodeRetrieveIDByRouteRequest,
|
||||
encodeRetrieveIDByRouteResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) Authorize(ctx context.Context, req *grpcChannelsV1.AuthzReq) (*grpcChannelsV1.AuthzRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcChannelsV1.AuthzRes), nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcChannelsV1.AuthzReq)
|
||||
|
||||
connType := connections.ConnType(req.GetType())
|
||||
if err := connections.CheckConnType(connType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorizeReq{
|
||||
domainID: req.GetDomainId(),
|
||||
clientID: req.GetClientId(),
|
||||
clientType: req.GetClientType(),
|
||||
channelID: req.GetChannelId(),
|
||||
connType: connType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &grpcChannelsV1.AuthzRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) RemoveClientConnections(ctx context.Context, req *grpcChannelsV1.RemoveClientConnectionsReq) (*grpcChannelsV1.RemoveClientConnectionsRes, error) {
|
||||
_, res, err := s.removeClientConnections.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcChannelsV1.RemoveClientConnectionsRes), nil
|
||||
}
|
||||
|
||||
func decodeRemoveClientConnectionsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcChannelsV1.RemoveClientConnectionsReq)
|
||||
|
||||
return removeClientConnectionsReq{
|
||||
clientID: req.GetClientId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRemoveClientConnectionsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
_ = grpcRes.(removeClientConnectionsRes)
|
||||
return &grpcChannelsV1.RemoveClientConnectionsRes{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UnsetParentGroupFromChannels(ctx context.Context, req *grpcChannelsV1.UnsetParentGroupFromChannelsReq) (*grpcChannelsV1.UnsetParentGroupFromChannelsRes, error) {
|
||||
_, res, err := s.unsetParentGroupFromChannels.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcChannelsV1.UnsetParentGroupFromChannelsRes), nil
|
||||
}
|
||||
|
||||
func decodeUnsetParentGroupFromChannelsRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcChannelsV1.UnsetParentGroupFromChannelsReq)
|
||||
|
||||
return unsetParentGroupFromChannelsReq{
|
||||
parentGroupID: req.GetParentGroupId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeUnsetParentGroupFromChannelsResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
_ = grpcRes.(unsetParentGroupFromChannelsRes)
|
||||
return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq) (*grpcCommonV1.RetrieveEntityRes, error) {
|
||||
_, res, err := s.retrieveEntity.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func decodeRetrieveEntityRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcCommonV1.RetrieveEntityReq)
|
||||
return retrieveEntityReq{
|
||||
Id: req.GetId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRetrieveEntityResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(retrieveEntityRes)
|
||||
|
||||
return &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: res.id,
|
||||
DomainId: res.domain,
|
||||
ParentGroupId: res.parentGroup,
|
||||
Status: uint32(res.status),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeRetrieveIDByRouteRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcCommonV1.RetrieveIDByRouteReq)
|
||||
return retrieveIDByRouteReq{
|
||||
route: req.GetRoute(),
|
||||
domainID: req.GetDomainId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRetrieveIDByRouteResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(retrieveIDByRouteRes)
|
||||
|
||||
return &grpcCommonV1.RetrieveEntityRes{
|
||||
Entity: &grpcCommonV1.EntityBasic{
|
||||
Id: res.id,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) RetrieveIDByRoute(ctx context.Context, req *grpcCommonV1.RetrieveIDByRouteReq) (*grpcCommonV1.RetrieveEntityRes, error) {
|
||||
_, res, err := s.retrieveIDByRoute.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*grpcCommonV1.RetrieveEntityRes), nil
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrInvalidAuthKey,
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMalformedPolicyAct:
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrAuthentication),
|
||||
errors.Contains(err, smqauth.ErrKeyExpired),
|
||||
err == apiutil.ErrMissingEmail,
|
||||
err == apiutil.ErrBearerToken:
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrAuthorization):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/internal/nullable"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func decodeViewChannel(_ context.Context, r *http.Request) (any, error) {
|
||||
roles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false)
|
||||
if err != nil {
|
||||
return viewChannelReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
req := viewChannelReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
roles: roles,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCreateChannelReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := createChannelReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Channel); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCreateChannelsReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := createChannelsReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Channels); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListChannels(_ context.Context, r *http.Request) (any, error) {
|
||||
name, err := apiutil.ReadStringQuery(r, api.NameKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
tags, err := apiutil.ReadStringQuery(r, api.TagsKey, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
var tq channels.TagsQuery
|
||||
if tags != "" {
|
||||
tq = channels.ToTagsQuery(tags)
|
||||
}
|
||||
|
||||
s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefGroupStatus)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
status, err := channels.ToStatus(s)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
meta, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
dir, err := apiutil.ReadStringQuery(r, api.DirKey, api.DefDir)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
allActions, err := apiutil.ReadStringQuery(r, api.ActionsKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
actions := []string{}
|
||||
|
||||
allActions = strings.TrimSpace(allActions)
|
||||
if allActions != "" {
|
||||
actions = strings.Split(allActions, ",")
|
||||
}
|
||||
roleID, err := apiutil.ReadStringQuery(r, api.RoleIDKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
roleName, err := apiutil.ReadStringQuery(r, api.RoleNameKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
accessType, err := apiutil.ReadStringQuery(r, api.AccessTypeKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
userID, err := apiutil.ReadStringQuery(r, api.UserKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
groupID, err := nullable.Parse(r.URL.Query(), api.GroupKey, nullable.ParseString)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
clientID, err := apiutil.ReadStringQuery(r, api.ClientKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
id, err := apiutil.ReadStringQuery(r, api.IDOrder, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
ot, err := apiutil.ReadBoolQuery(r, api.OnlyTotal, false)
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
connectionType, err := apiutil.ReadStringQuery(r, api.ConnTypeKey, "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
|
||||
if err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
var createdFrom, createdTo time.Time
|
||||
if cfrom != "" {
|
||||
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
|
||||
}
|
||||
}
|
||||
if cto != "" {
|
||||
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
|
||||
return listChannelsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
|
||||
}
|
||||
}
|
||||
|
||||
req := listChannelsReq{
|
||||
Page: channels.Page{
|
||||
Name: name,
|
||||
Tags: tq,
|
||||
Status: status,
|
||||
Metadata: meta,
|
||||
RoleName: roleName,
|
||||
RoleID: roleID,
|
||||
Actions: actions,
|
||||
AccessType: accessType,
|
||||
Order: order,
|
||||
Dir: dir,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Group: groupID,
|
||||
Client: clientID,
|
||||
ConnectionType: connectionType,
|
||||
ID: id,
|
||||
OnlyTotal: ot,
|
||||
CreatedFrom: createdFrom,
|
||||
CreatedTo: createdTo,
|
||||
},
|
||||
userID: userID,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateChannelReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateChannelTags(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateChannelTagsReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeSetChannelParentGroupStatus(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := setChannelParentGroupReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRemoveChannelParentGroupStatus(_ context.Context, r *http.Request) (any, error) {
|
||||
req := removeChannelParentGroupReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeChangeChannelStatus(_ context.Context, r *http.Request) (any, error) {
|
||||
req := changeChannelStatusReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDeleteChannelReq(_ context.Context, r *http.Request) (any, error) {
|
||||
req := deleteChannelReq{
|
||||
id: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeConnectChannelClientRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
req := connectChannelClientsRequest{
|
||||
channelID: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDisconnectChannelClientsRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
req := disconnectChannelClientsRequest{
|
||||
channelID: chi.URLParam(r, "channelID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeConnectRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := connectRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDisconnectRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := disconnectRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func createChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(createChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
channels, _, err := svc.CreateChannels(ctx, session, req.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createChannelRes{
|
||||
Channel: channels[0],
|
||||
created: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(createChannelsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
channels, _, err := svc.CreateChannels(ctx, session, req.Channels...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := channelsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: uint64(len(channels)),
|
||||
},
|
||||
Channels: []viewChannelRes{},
|
||||
}
|
||||
for _, c := range channels {
|
||||
res.Channels = append(res.Channels, viewChannelRes{Channel: c})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(viewChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
c, err := svc.ViewChannel(ctx, session, req.id, req.roles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return viewChannelRes{Channel: c}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listChannelsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listChannelsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
var page channels.ChannelsPage
|
||||
var err error
|
||||
switch req.userID != "" {
|
||||
case true:
|
||||
page, err = svc.ListUserChannels(ctx, session, req.userID, req.Page)
|
||||
default:
|
||||
page, err = svc.ListChannels(ctx, session, req.Page)
|
||||
}
|
||||
if err != nil {
|
||||
return channelsPageRes{}, err
|
||||
}
|
||||
|
||||
res := channelsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Channels: []viewChannelRes{},
|
||||
}
|
||||
for _, c := range page.Channels {
|
||||
res.Channels = append(res.Channels, viewChannelRes{Channel: c})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch := channels.Channel{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
ch, err := svc.UpdateChannel(ctx, session, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updateChannelRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelTagsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateChannelTagsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch := channels.Channel{
|
||||
ID: req.id,
|
||||
Tags: req.Tags,
|
||||
}
|
||||
ch, err := svc.UpdateChannelTags(ctx, session, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updateChannelRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func setChannelParentGroupEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(setChannelParentGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.SetParentGroup(ctx, session, req.ParentGroupID, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return setChannelParentGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelParentGroupEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(removeChannelParentGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.RemoveParentGroup(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeChannelParentGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func enableChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeChannelStatusReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch, err := svc.EnableChannel(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changeChannelStatusRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disableChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeChannelStatusReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
ch, err := svc.DisableChannel(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changeChannelStatusRes{Channel: ch}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func connectChannelClientEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(connectChannelClientsRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Connect(ctx, session, []string{req.channelID}, req.ClientIDs, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connectChannelClientsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectChannelClientsEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(disconnectChannelClientsRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(ctx, session, []string{req.channelID}, req.ClientIds, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectChannelClientsRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func connectEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(connectRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Connect(ctx, session, req.ChannelIds, req.ClientIds, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connectRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(disconnectRequest)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(ctx, session, req.ChannelIds, req.ClientIds, req.Types); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannelEndpoint(svc channels.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(deleteChannelReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err := svc.RemoveChannel(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deleteChannelRes{}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
)
|
||||
|
||||
type createChannelReq struct {
|
||||
Channel channels.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if len(req.Channel.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
if req.Channel.ID != "" {
|
||||
if strings.TrimSpace(req.Channel.ID) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
if req.Channel.Route != "" {
|
||||
if err := api.ValidateRoute(req.Channel.Route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.ValidateUUID(req.Channel.Route); err == nil {
|
||||
return apiutil.ErrInvalidRouteFormat
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type createChannelsReq struct {
|
||||
Channels []channels.Channel
|
||||
}
|
||||
|
||||
func (req createChannelsReq) validate() error {
|
||||
if len(req.Channels) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
for _, channel := range req.Channels {
|
||||
if channel.ID != "" {
|
||||
if strings.TrimSpace(channel.ID) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
if len(channel.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
if channel.Route != "" {
|
||||
if err := api.ValidateRoute(channel.Route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.ValidateUUID(channel.Route); err == nil {
|
||||
return apiutil.ErrInvalidRouteFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewChannelReq struct {
|
||||
id string
|
||||
roles bool
|
||||
}
|
||||
|
||||
func (req viewChannelReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listChannelsReq struct {
|
||||
channels.Page
|
||||
userID string
|
||||
}
|
||||
|
||||
func (req listChannelsReq) validate() error {
|
||||
if req.Limit > api.MaxLimitSize || req.Limit < 1 {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
if len(req.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
switch req.Order {
|
||||
case "", api.NameOrder, api.CreatedAtOrder, api.UpdatedAtOrder:
|
||||
default:
|
||||
return apiutil.ErrInvalidOrder
|
||||
}
|
||||
|
||||
if req.Dir != "" && (req.Dir != api.DescDir && req.Dir != api.AscDir) {
|
||||
return apiutil.ErrInvalidDirection
|
||||
}
|
||||
|
||||
if req.ConnectionType != "" {
|
||||
if _, err := connections.ParseConnType(req.ConnectionType); err != nil {
|
||||
return apiutil.ErrValidation
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelReq struct {
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if len(req.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelTagsReq struct {
|
||||
id string
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateChannelTagsReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type setChannelParentGroupReq struct {
|
||||
id string
|
||||
ParentGroupID string `json:"parent_group_id"`
|
||||
}
|
||||
|
||||
func (req setChannelParentGroupReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.ParentGroupID == "" {
|
||||
return apiutil.ErrMissingParentGroupID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type removeChannelParentGroupReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req removeChannelParentGroupReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeChannelStatusReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req changeChannelStatusReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type connectChannelClientsRequest struct {
|
||||
channelID string
|
||||
ClientIDs []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *connectChannelClientsRequest) validate() error {
|
||||
if req.channelID == "" || strings.TrimSpace(req.channelID) == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.ClientIDs) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIDs {
|
||||
if err := api.ValidateUUID(tid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type disconnectChannelClientsRequest struct {
|
||||
channelID string
|
||||
ClientIds []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *disconnectChannelClientsRequest) validate() error {
|
||||
if req.channelID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if err := api.ValidateUUID(req.channelID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.ClientIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIds {
|
||||
if err := api.ValidateUUID(tid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type connectRequest struct {
|
||||
ChannelIds []string `json:"channel_ids,omitempty"`
|
||||
ClientIds []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *connectRequest) validate() error {
|
||||
if len(req.ChannelIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
for _, cid := range req.ChannelIds {
|
||||
if strings.TrimSpace(cid) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.ClientIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIds {
|
||||
if strings.TrimSpace(tid) == "" {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type disconnectRequest struct {
|
||||
ChannelIds []string `json:"channel_ids,omitempty"`
|
||||
ClientIds []string `json:"client_ids,omitempty"`
|
||||
Types []connections.ConnType `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
func (req *disconnectRequest) validate() error {
|
||||
if len(req.ChannelIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
for _, cid := range req.ChannelIds {
|
||||
if err := api.ValidateUUID(cid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.ClientIds) == 0 {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
for _, tid := range req.ClientIds {
|
||||
if err := api.ValidateUUID(tid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Types) == 0 {
|
||||
return apiutil.ErrMissingConnectionType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type deleteChannelReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req deleteChannelReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,628 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/connections"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateChannelReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req createChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "long name",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "invalid route",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: "__invalid",
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "uuid as route",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
ID: " ",
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrMissingChannelID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelsReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req createChannelsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
Name: valid,
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "long name",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
ID: " ",
|
||||
},
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrMissingChannelID,
|
||||
},
|
||||
{
|
||||
desc: "empty list",
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{},
|
||||
},
|
||||
err: apiutil.ErrEmptyList,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannelReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req viewChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: viewChannelReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: viewChannelReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannelsReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req listChannelsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: 10},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "limit is 0",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: 0},
|
||||
},
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "limit is greater than max limit",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: api.MaxLimitSize + 1},
|
||||
},
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "name is too long",
|
||||
req: listChannelsReq{
|
||||
Page: channels.Page{Limit: 10, Name: strings.Repeat("a", api.MaxNameSize+1)},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req updateChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: updateChannelReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: updateChannelReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "name is too long",
|
||||
req: updateChannelReq{
|
||||
id: valid,
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelTagsReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req updateChannelTagsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: updateChannelTagsReq{
|
||||
id: valid,
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: updateChannelTagsReq{
|
||||
id: "",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChannelsParentGroupReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req setChannelParentGroupReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: setChannelParentGroupReq{
|
||||
id: valid,
|
||||
ParentGroupID: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: setChannelParentGroupReq{
|
||||
id: "",
|
||||
ParentGroupID: valid,
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing parent group ID",
|
||||
req: setChannelParentGroupReq{
|
||||
id: valid,
|
||||
ParentGroupID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingParentGroupID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannelParentGroupReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req removeChannelParentGroupReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: removeChannelParentGroupReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: removeChannelParentGroupReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeChannelStatusReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req changeChannelStatusReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: changeChannelStatusReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: changeChannelStatusReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectChannelClientsReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req connectChannelClientsRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: "",
|
||||
ClientIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
{
|
||||
desc: "invalid client ID",
|
||||
req: connectChannelClientsRequest{
|
||||
channelID: valid,
|
||||
ClientIDs: []string{"client1", "invalid"},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectChannelClientReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req disconnectChannelClientsRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: "",
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid channel ID",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: "invalid",
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
{
|
||||
desc: "invalid client ID",
|
||||
req: disconnectChannelClientsRequest{
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
ClientIds: []string{"client1", "invalid"},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req connectRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel IDs",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: connectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req disconnectRequest
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing channel IDs",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing client IDs",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "missing connection types",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{},
|
||||
},
|
||||
err: apiutil.ErrMissingConnectionType,
|
||||
},
|
||||
{
|
||||
desc: "invalid client ID",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{"client1", "invalid"},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
{
|
||||
desc: "invalid channel ID",
|
||||
req: disconnectRequest{
|
||||
ChannelIds: []string{"invalid", testsutil.GenerateUUID(t)},
|
||||
ClientIds: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)},
|
||||
Types: []connections.ConnType{connections.Publish},
|
||||
},
|
||||
err: apiutil.ErrInvalidIDFormat,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteChannelReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req deleteChannelReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: deleteChannelReq{
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing ID",
|
||||
req: deleteChannelReq{
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
)
|
||||
|
||||
var (
|
||||
_ magistrala.Response = (*createChannelRes)(nil)
|
||||
_ magistrala.Response = (*viewChannelRes)(nil)
|
||||
_ magistrala.Response = (*channelsPageRes)(nil)
|
||||
_ magistrala.Response = (*updateChannelRes)(nil)
|
||||
_ magistrala.Response = (*deleteChannelRes)(nil)
|
||||
_ magistrala.Response = (*connectChannelClientsRes)(nil)
|
||||
_ magistrala.Response = (*disconnectChannelClientsRes)(nil)
|
||||
_ magistrala.Response = (*connectRes)(nil)
|
||||
_ magistrala.Response = (*disconnectRes)(nil)
|
||||
_ magistrala.Response = (*changeChannelStatusRes)(nil)
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
}
|
||||
|
||||
type createChannelRes struct {
|
||||
channels.Channel
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res createChannelRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res createChannelRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/channels/%s", res.ID),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res createChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
channels.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelsPageRes struct {
|
||||
pageRes
|
||||
Channels []viewChannelRes `json:"channels,omitempty"`
|
||||
}
|
||||
|
||||
func (res channelsPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type changeChannelStatusRes struct {
|
||||
channels.Channel
|
||||
}
|
||||
|
||||
func (res changeChannelStatusRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res changeChannelStatusRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res changeChannelStatusRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type updateChannelRes struct {
|
||||
channels.Channel
|
||||
}
|
||||
|
||||
func (res updateChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type setChannelParentGroupRes struct{}
|
||||
|
||||
func (res setChannelParentGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res setChannelParentGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res setChannelParentGroupRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type removeChannelParentGroupRes struct{}
|
||||
|
||||
func (res removeChannelParentGroupRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeChannelParentGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeChannelParentGroupRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type deleteChannelRes struct{}
|
||||
|
||||
func (res deleteChannelRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res deleteChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deleteChannelRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type connectChannelClientsRes struct{}
|
||||
|
||||
func (res connectChannelClientsRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res connectChannelClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectChannelClientsRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectChannelClientsRes struct{}
|
||||
|
||||
func (res disconnectChannelClientsRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectChannelClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectChannelClientsRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type connectRes struct{}
|
||||
|
||||
func (res connectRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res connectRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectRes struct{}
|
||||
|
||||
func (res disconnectRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/channels"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
roleManagerHttp "github.com/absmach/magistrala/pkg/roles/rolemanager/api"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for Channels API endpoints.
|
||||
func MakeHandler(svc channels.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, logger *slog.Logger, instanceID string, idp magistrala.IDProvider) *chi.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
d := roleManagerHttp.NewDecoder("channelID")
|
||||
|
||||
mux.Route("/{domainID}/channels", func(r chi.Router) {
|
||||
r.Use(authn.Middleware())
|
||||
r.Use(api.RequestIDMiddleware(idp))
|
||||
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
createChannelEndpoint(svc),
|
||||
decodeCreateChannelReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "create_channel").ServeHTTP)
|
||||
|
||||
r.Post("/bulk", otelhttp.NewHandler(kithttp.NewServer(
|
||||
createChannelsEndpoint(svc),
|
||||
decodeCreateChannelsReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "create_channels").ServeHTTP)
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listChannelsEndpoint(svc),
|
||||
decodeListChannels,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "list_channels").ServeHTTP)
|
||||
|
||||
r.Post("/connect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
connectEndpoint(svc),
|
||||
decodeConnectRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "connect").ServeHTTP)
|
||||
|
||||
r.Post("/disconnect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disconnectEndpoint(svc),
|
||||
decodeDisconnectRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disconnect").ServeHTTP)
|
||||
|
||||
r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts)
|
||||
|
||||
r.Route("/{channelID}", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewChannelEndpoint(svc),
|
||||
decodeViewChannel,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "view_channel").ServeHTTP)
|
||||
|
||||
r.Patch("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateChannelEndpoint(svc),
|
||||
decodeUpdateChannel,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "update_channel_name_and_metadata").ServeHTTP)
|
||||
|
||||
r.Patch("/tags", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateChannelTagsEndpoint(svc),
|
||||
decodeUpdateChannelTags,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "update_channel_tag").ServeHTTP)
|
||||
|
||||
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
deleteChannelEndpoint(svc),
|
||||
decodeDeleteChannelReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "delete_channel").ServeHTTP)
|
||||
|
||||
r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer(
|
||||
enableChannelEndpoint(svc),
|
||||
decodeChangeChannelStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "enable_channel").ServeHTTP)
|
||||
|
||||
r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disableChannelEndpoint(svc),
|
||||
decodeChangeChannelStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disable_channel").ServeHTTP)
|
||||
|
||||
r.Post("/parent", otelhttp.NewHandler(kithttp.NewServer(
|
||||
setChannelParentGroupEndpoint(svc),
|
||||
decodeSetChannelParentGroupStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "set_channel_parent_group").ServeHTTP)
|
||||
|
||||
r.Delete("/parent", otelhttp.NewHandler(kithttp.NewServer(
|
||||
removeChannelParentGroupEndpoint(svc),
|
||||
decodeRemoveChannelParentGroupStatus,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "remove_channel_parent_group").ServeHTTP)
|
||||
|
||||
r.Post("/connect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
connectChannelClientEndpoint(svc),
|
||||
decodeConnectChannelClientRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "connect_channel_client").ServeHTTP)
|
||||
|
||||
r.Post("/disconnect", otelhttp.NewHandler(kithttp.NewServer(
|
||||
disconnectChannelClientsEndpoint(svc),
|
||||
decodeDisconnectChannelClientsRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disconnect_channel_client").ServeHTTP)
|
||||
|
||||
roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts)
|
||||
})
|
||||
})
|
||||
|
||||
mux.Get("/health", magistrala.Health("channels", instanceID))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package channels
|
||||
|
||||
import "github.com/absmach/magistrala/pkg/roles"
|
||||
|
||||
const BuiltInRoleAdmin roles.BuiltInRoleName = "admin"
|
||||
Vendored
-82
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyDomainID = errors.New("domain ID is empty")
|
||||
ErrEmptyChannelID = errors.New("channel ID is empty")
|
||||
ErrEmptyChannelRoute = errors.New("channel route is empty")
|
||||
)
|
||||
|
||||
type channelsCache struct {
|
||||
client *redis.Client
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func NewChannelsCache(client *redis.Client, duration time.Duration) channels.Cache {
|
||||
return &channelsCache{
|
||||
client: client,
|
||||
duration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *channelsCache) Save(ctx context.Context, route, domainID, channelID string) error {
|
||||
key, err := encodeKey(domainID, route)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
if channelID == "" {
|
||||
return errors.Wrap(repoerr.ErrCreateEntity, ErrEmptyChannelID)
|
||||
}
|
||||
if err := cc.client.Set(ctx, key, channelID, cc.duration).Err(); err != nil {
|
||||
return errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *channelsCache) ID(ctx context.Context, channelRoute, domainID string) (string, error) {
|
||||
key, err := encodeKey(domainID, channelRoute)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrNotFound, err)
|
||||
}
|
||||
id, err := cc.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (cc *channelsCache) Remove(ctx context.Context, channelRoute, domainID string) error {
|
||||
key, err := encodeKey(domainID, channelRoute)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
if err := cc.client.Del(ctx, key).Err(); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeKey(domainID, channelRoute string) (string, error) {
|
||||
if domainID == "" {
|
||||
return "", ErrEmptyDomainID
|
||||
}
|
||||
if channelRoute == "" {
|
||||
return "", ErrEmptyChannelRoute
|
||||
}
|
||||
return domainID + ":" + channelRoute, nil
|
||||
}
|
||||
Vendored
-186
@@ -1,186 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/channels"
|
||||
"github.com/absmach/magistrala/channels/cache"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testRoute = "test-route"
|
||||
nonExistent = "non-existing"
|
||||
)
|
||||
|
||||
func setupChannelsClient(t *testing.T) channels.Cache {
|
||||
opts, err := redis.ParseURL(redisURL)
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error on parsing redis URL: %s", err))
|
||||
redisClient := redis.NewClient(opts)
|
||||
|
||||
return cache.NewChannelsCache(redisClient, 10*time.Minute)
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
cc := setupChannelsClient(t)
|
||||
|
||||
route := testRoute
|
||||
domainID := testsutil.GenerateUUID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
channelID string
|
||||
channelRoute string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Save successfully",
|
||||
domainID: domainID,
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
channelRoute: route,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Save with empty domain ID",
|
||||
domainID: "",
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
channelRoute: route,
|
||||
err: cache.ErrEmptyDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Save with empty channel ID",
|
||||
domainID: domainID,
|
||||
channelID: "",
|
||||
channelRoute: route,
|
||||
err: cache.ErrEmptyChannelID,
|
||||
},
|
||||
{
|
||||
desc: "Save with empty channel route",
|
||||
domainID: domainID,
|
||||
channelID: testsutil.GenerateUUID(t),
|
||||
channelRoute: "",
|
||||
err: cache.ErrEmptyChannelRoute,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := cc.Save(context.Background(), tc.channelRoute, tc.domainID, tc.channelID)
|
||||
assert.True(t, errors.Contains(err, tc.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestID(t *testing.T) {
|
||||
cc := setupChannelsClient(t)
|
||||
|
||||
domainID := testsutil.GenerateUUID(t)
|
||||
route := testRoute
|
||||
id := testsutil.GenerateUUID(t)
|
||||
|
||||
err := cc.Save(context.Background(), route, domainID, id)
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error on saving channel ID: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
channelRoute string
|
||||
channelID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Retrieve existing channel",
|
||||
domainID: domainID,
|
||||
channelRoute: route,
|
||||
channelID: id,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Retrieve non-existing channel",
|
||||
domainID: domainID,
|
||||
channelRoute: nonExistent,
|
||||
channelID: "",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "Retrieve with empty domain ID",
|
||||
domainID: "",
|
||||
channelRoute: route,
|
||||
channelID: "",
|
||||
err: cache.ErrEmptyDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
id, err := cc.ID(context.Background(), tc.channelRoute, tc.domainID)
|
||||
assert.Equal(t, tc.channelID, id, fmt.Sprintf("expected channel ID '%s' got '%s'", tc.channelID, id))
|
||||
assert.True(t, errors.Contains(err, tc.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
cc := setupChannelsClient(t)
|
||||
|
||||
domainID := testsutil.GenerateUUID(t)
|
||||
route := testRoute
|
||||
id := testsutil.GenerateUUID(t)
|
||||
|
||||
err := cc.Save(context.Background(), domainID, route, id)
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error on saving channel ID: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
channelRoute string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Remove existing channel",
|
||||
domainID: domainID,
|
||||
channelRoute: route,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Remove non-existing channel",
|
||||
domainID: domainID,
|
||||
channelRoute: nonExistent,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Remove with empty domain ID",
|
||||
domainID: "",
|
||||
channelRoute: route,
|
||||
err: cache.ErrEmptyDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Remove with empty channel route",
|
||||
domainID: domainID,
|
||||
channelRoute: "",
|
||||
err: cache.ErrEmptyChannelRoute,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := cc.Remove(context.Background(), tc.channelRoute, tc.domainID)
|
||||
assert.True(t, errors.Contains(err, tc.err))
|
||||
|
||||
if tc.err == nil {
|
||||
id, err := cc.ID(context.Background(), tc.channelRoute, tc.domainID)
|
||||
assert.Equal(t, "", id, fmt.Sprintf("expected channel ID to be empty after removal, got '%s'", id))
|
||||
assert.True(t, errors.Contains(err, repoerr.ErrNotFound))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user