mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
Compare commits
8 Commits
ee
...
v0.14.0-dave
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ffc710b6a | |||
| f762b91669 | |||
| 6e8ca15f56 | |||
| d3fe7d1063 | |||
| ad6619f0ac | |||
| 4bee424289 | |||
| f55d7640eb | |||
| 221c1212bb |
+1
-1
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.git
|
||||
.github
|
||||
.gitlab
|
||||
build
|
||||
docker
|
||||
metrics
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Bug Report
|
||||
description: File a bug/issue report. Make sure to search to see if an issue already exists for the bug you encountered.
|
||||
title: "Bug: <title>"
|
||||
labels: ["bug", "needs-review", "help wanted"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What were you trying to achieve?
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What are the expected results?
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What are the received results?
|
||||
description: A concise description of what you received.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: What are the steps to reproduce the issue?
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: In what environment did you encounter the issue?
|
||||
description: A concise description of the environment you encountered the issue in.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information you deem important
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,11 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Google group
|
||||
url: https://groups.google.com/forum/#!forum/mainflux
|
||||
about: Join the Mainflux community on Google group.
|
||||
- name: Gitter
|
||||
url: https://gitter.im/mainflux/mainflux
|
||||
about: Join the Mainflux community on Gitter.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Feature Request
|
||||
description: File a feature request. Make sure to search to see if a request already exists for the feature you are requesting.
|
||||
title: "Feature: <title>"
|
||||
labels: ["enchancement", "needs-review"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you are requesting, as well as the possible use case(s) for it.
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Indicate the importance of this feature to you.
|
||||
description: This will help us prioritize the feature request.
|
||||
options:
|
||||
- Must-have
|
||||
- Should-have
|
||||
- Nice-to-have
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the feature that you are requesting.
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,58 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
directory: ./coverage/
|
||||
name: codecov-umbrella
|
||||
verbose: true
|
||||
|
||||
- name: Set up Docker Build
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Compile check for rabbitmq
|
||||
run: |
|
||||
MG_MESSAGE_BROKER_TYPE=rabbitmq make mqtt
|
||||
|
||||
- name: Compile check for redis
|
||||
run: |
|
||||
MG_ES_TYPE=redis make mqtt
|
||||
|
||||
- name: Build and push Dockers
|
||||
run: |
|
||||
make latest -j $(nproc)
|
||||
@@ -1,155 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Check the consistency of generated files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-generated-files:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Check for changes in go.mod
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v2
|
||||
id: changes
|
||||
with:
|
||||
base: main
|
||||
filters: |
|
||||
proto:
|
||||
- ".github/workflows/check-generated-files.yml"
|
||||
- "auth.proto"
|
||||
- "auth/*.pb.go"
|
||||
- "pkg/messaging/message.proto"
|
||||
- "pkg/messaging/*.pb.go"
|
||||
|
||||
mocks:
|
||||
- ".github/workflows/check-generated-files.yml"
|
||||
- "pkg/sdk/go/sdk.go"
|
||||
- "users/postgres/clients.go"
|
||||
- "users/clients.go"
|
||||
- "pkg/clients/clients.go"
|
||||
- "pkg/messaging/pubsub.go"
|
||||
- "things/postgres/clients.go"
|
||||
- "things/things.go"
|
||||
- "auth/domains.go"
|
||||
- "auth/keys.go"
|
||||
- "auth/policies.go"
|
||||
- "auth/service.go"
|
||||
- "pkg/events/events.go"
|
||||
- "provision/service.go"
|
||||
- "pkg/groups/groups.go"
|
||||
|
||||
- name: Set up protoc
|
||||
if: steps.changes.outputs.proto == 'true'
|
||||
run: |
|
||||
PROTOC_VERSION=25.3
|
||||
PROTOC_GEN_VERSION=v1.32.0
|
||||
PROTOC_GRPC_VERSION=v1.3.0
|
||||
|
||||
# Download and install protoc
|
||||
PROTOC_ZIP=protoc-$PROTOC_VERSION-linux-x86_64.zip
|
||||
curl -0L -o $PROTOC_ZIP https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/$PROTOC_ZIP
|
||||
unzip -o $PROTOC_ZIP -d protoc3
|
||||
sudo mv protoc3/bin/* /usr/local/bin/
|
||||
sudo mv protoc3/include/* /usr/local/include/
|
||||
rm -rf $PROTOC_ZIP protoc3
|
||||
|
||||
# Install protoc-gen-go and protoc-gen-go-grpc
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@$PROTOC_GEN_VERSION
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$PROTOC_GRPC_VERSION
|
||||
|
||||
# Add protoc to the PATH
|
||||
export PATH=$PATH:/usr/local/bin/protoc
|
||||
|
||||
- name: Check Protobuf is up to Date
|
||||
if: steps.changes.outputs.proto == 'true'
|
||||
run: |
|
||||
for p in $(find . -name "*.pb.go"); do
|
||||
mv $p $p.tmp
|
||||
done
|
||||
|
||||
make proto
|
||||
|
||||
for p in $(find . -name "*.pb.go"); do
|
||||
if ! cmp -s $p $p.tmp; then
|
||||
echo "Error: Proto file and generated Go file $p are out of sync!"
|
||||
echo "Please run 'make proto' with protoc version $PROTOC_VERSION, protoc-gen-go version $PROTOC_GEN_VERSION and protoc-gen-go-grpc version $PROTOC_GRPC_VERSION and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Check Mocks are up to Date
|
||||
if: steps.changes.outputs.mocks == 'true'
|
||||
run: |
|
||||
MOCKERY_VERSION=v2.38.0
|
||||
go install github.com/vektra/mockery/v2@$MOCKERY_VERSION
|
||||
|
||||
mv ./pkg/sdk/mocks/sdk.go ./pkg/sdk/mocks/sdk.go.tmp
|
||||
mv ./users/mocks/repository.go ./users/mocks/repository.go.tmp
|
||||
mv ./users/mocks/service.go ./users/mocks/service.go.tmp
|
||||
mv ./pkg/messaging/mocks/pubsub.go ./pkg/messaging/mocks/pubsub.go.tmp
|
||||
mv ./things/mocks/repository.go ./things/mocks/repository.go.tmp
|
||||
mv ./things/mocks/service.go ./things/mocks/service.go.tmp
|
||||
mv ./things/mocks/cache.go ./things/mocks/cache.go.tmp
|
||||
mv ./auth/mocks/agent.go ./auth/mocks/agent.go.tmp
|
||||
mv ./auth/mocks/authz.go ./auth/mocks/authz.go.tmp
|
||||
mv ./auth/mocks/domains.go ./auth/mocks/domains.go.tmp
|
||||
mv ./auth/mocks/keys.go ./auth/mocks/keys.go.tmp
|
||||
mv ./auth/mocks/service.go ./auth/mocks/service.go.tmp
|
||||
mv ./pkg/events/mocks/publisher.go ./pkg/events/mocks/publisher.go.tmp
|
||||
mv ./pkg/events/mocks/subscriber.go ./pkg/events/mocks/subscriber.go.tmp
|
||||
mv ./provision/mocks/service.go ./provision/mocks/service.go.tmp
|
||||
mv ./pkg/groups/mocks/repository.go ./pkg/groups/mocks/repository.go.tmp
|
||||
mv ./pkg/groups/mocks/service.go ./pkg/groups/mocks/service.go.tmp
|
||||
|
||||
make mocks
|
||||
|
||||
check_mock_changes() {
|
||||
local file_path=$1
|
||||
local tmp_file_path=$1.tmp
|
||||
local entity_name=$2
|
||||
|
||||
if ! cmp -s "$file_path" "$tmp_file_path"; then
|
||||
echo "Error: Generated mocks for $entity_name are out of sync!"
|
||||
echo "Please run 'make mocks' with mockery version $MOCKERY_VERSION and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_mock_changes ./pkg/sdk/mocks/sdk.go "SDK ./pkg/sdk/mocks/sdk.go"
|
||||
check_mock_changes ./users/mocks/repository.go "Users Repository ./users/mocks/repository.go"
|
||||
check_mock_changes ./users/mocks/service.go "Users Service ./users/mocks/service.go"
|
||||
check_mock_changes ./pkg/messaging/mocks/pubsub.go "PubSub ./pkg/messaging/mocks/pubsub.go"
|
||||
check_mock_changes ./things/mocks/repository.go "Things Repository ./things/mocks/repository.go"
|
||||
check_mock_changes ./things/mocks/service.go "Things Service ./things/mocks/service.go"
|
||||
check_mock_changes ./things/mocks/cache.go "Things Cache ./things/mocks/cache.go"
|
||||
check_mock_changes ./auth/mocks/agent.go "Auth Agent ./auth/mocks/agent.go"
|
||||
check_mock_changes ./auth/mocks/authz.go "Auth Authz ./auth/mocks/authz.go"
|
||||
check_mock_changes ./auth/mocks/domains.go "Auth Domains ./auth/mocks/domains.go"
|
||||
check_mock_changes ./auth/mocks/keys.go "Auth Keys ./auth/mocks/keys.go"
|
||||
check_mock_changes ./auth/mocks/service.go "Auth Service ./auth/mocks/service.go"
|
||||
check_mock_changes ./pkg/events/mocks/publisher.go "ES Publisher ./pkg/events/mocks/publisher.go"
|
||||
check_mock_changes ./pkg/events/mocks/subscriber.go "EE Subscriber ./pkg/events/mocks/subscriber.go"
|
||||
check_mock_changes ./provision/mocks/service.go "Provision Service ./provision/mocks/service.go"
|
||||
check_mock_changes ./pkg/groups/mocks/repository.go "Groups Repository ./pkg/groups/mocks/repository.go"
|
||||
check_mock_changes ./pkg/groups/mocks/service.go "Groups Service ./pkg/groups/mocks/service.go"
|
||||
@@ -1,31 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Check License Header
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-license:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check License Header
|
||||
run: |
|
||||
CHECK=$(grep -rcL --exclude-dir={.git,build,**vernemq**} \
|
||||
--exclude=\*.{crt,key,pem,zed,hcl,md,json,csv,mod,sum,tmpl,args} \
|
||||
--exclude={CODEOWNERS,LICENSE,MAINTAINERS} \
|
||||
--regexp "Copyright (c) Abstract Machines" .)
|
||||
if [ "$CHECK" ]; then
|
||||
echo "License header check failed. Fix the following files:"
|
||||
echo "$CHECK"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,31 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
swagger-ui:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Swagger UI action
|
||||
id: swagger-ui-action
|
||||
uses: blokovi/swagger-ui-action@main
|
||||
with:
|
||||
dir: "./api/openapi"
|
||||
pattern: "*.yml"
|
||||
debug: "true"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: api.mainflux.io
|
||||
@@ -1,407 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-and-build: # Linting and building are combined to save time for setting up Go
|
||||
name: Lint and Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
version: v1.56.1
|
||||
|
||||
- name: Build all Binaries
|
||||
run: |
|
||||
make all -j $(nproc)
|
||||
|
||||
- name: Compile check for rabbitmq
|
||||
run: |
|
||||
MG_MESSAGE_BROKER_TYPE=rabbitmq make mqtt
|
||||
|
||||
- name: Compile check for redis
|
||||
run: |
|
||||
MG_ES_TYPE=redis make mqtt
|
||||
|
||||
run-tests:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-build
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v2
|
||||
id: changes
|
||||
with:
|
||||
base: main
|
||||
filters: |
|
||||
workflow:
|
||||
- ".github/workflows/tests.yml"
|
||||
|
||||
auth:
|
||||
- "auth/**"
|
||||
- "cmd/auth/**"
|
||||
- "auth.proto"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
|
||||
bootstrap:
|
||||
- "bootstrap/**"
|
||||
- "cmd/bootstrap/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
certs:
|
||||
- "certs/**"
|
||||
- "cmd/certs/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
coap:
|
||||
- "coap/**"
|
||||
- "cmd/coap/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "things/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
consumers:
|
||||
- "consumers/**"
|
||||
- "cmd/cassandra-writer/**"
|
||||
- "cmd/influxdb-writer/**"
|
||||
- "cmd/mongodb-writer/**"
|
||||
- "cmd/postgres-writer/**"
|
||||
- "cmd/timescale-writer/**"
|
||||
- "cmd/smpp-notifier/**"
|
||||
- "cmd/smtp-notifier/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
http:
|
||||
- "http/**"
|
||||
- "cmd/http/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "things/**"
|
||||
- "pkg/messaging/**"
|
||||
- "logger/**"
|
||||
|
||||
internal:
|
||||
- "internal/**"
|
||||
|
||||
invitations:
|
||||
- "invitations/**"
|
||||
- "cmd/invitations/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
lora:
|
||||
- "lora/**"
|
||||
- "cmd/lora/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
logger:
|
||||
- "logger/**"
|
||||
|
||||
mqtt:
|
||||
- "mqtt/**"
|
||||
- "cmd/mqtt/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "things/**"
|
||||
- "pkg/messaging/**"
|
||||
- "logger/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
opcua:
|
||||
- "opcua/**"
|
||||
- "cmd/opcua/**"
|
||||
- "logger/**"
|
||||
|
||||
pkg-auth:
|
||||
- "pkg/auth/**"
|
||||
|
||||
pkg-clients:
|
||||
- "pkg/clients/**"
|
||||
|
||||
pkg-errors:
|
||||
- "pkg/errors/**"
|
||||
|
||||
pkg-events:
|
||||
- "pkg/events/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
pkg-messaging:
|
||||
- "pkg/messaging/**"
|
||||
|
||||
pkg-sdk:
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/clients/**"
|
||||
- "pkg/errors/**"
|
||||
- "auth/**"
|
||||
- "bootstrap/**"
|
||||
- "certs/**"
|
||||
- "consumers/**"
|
||||
- "http/**"
|
||||
- "invitations/**"
|
||||
- "provision/**"
|
||||
- "readers/**"
|
||||
- "things/**"
|
||||
- "twins/**"
|
||||
- "users/**"
|
||||
|
||||
pkg-transformers:
|
||||
- "pkg/transformers/**"
|
||||
|
||||
pkg-ulid:
|
||||
- "pkg/ulid/**"
|
||||
|
||||
pkg-uuid:
|
||||
- "pkg/uuid/**"
|
||||
|
||||
provision:
|
||||
- "provision/**"
|
||||
- "cmd/provision/**"
|
||||
- "logger/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
readers:
|
||||
- "readers/**"
|
||||
- "cmd/cassandra-reader/**"
|
||||
- "cmd/influxdb-reader/**"
|
||||
- "cmd/mongodb-reader/**"
|
||||
- "cmd/postgres-reader/**"
|
||||
- "cmd/timescale-reader/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "things/**"
|
||||
- "auth/**"
|
||||
|
||||
things:
|
||||
- "things/**"
|
||||
- "cmd/things/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/clients/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
twins:
|
||||
- "twins/**"
|
||||
- "cmd/twins/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/messaging/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "logger/**"
|
||||
|
||||
users:
|
||||
- "users/**"
|
||||
- "cmd/users/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/clients/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
ws:
|
||||
- "ws/**"
|
||||
- "cmd/ws/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "things/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
- name: Create coverage directory
|
||||
run: |
|
||||
mkdir coverage
|
||||
|
||||
- name: Run auth tests
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/auth.out ./auth/...
|
||||
|
||||
- name: Run bootstrap tests
|
||||
if: steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/bootstrap.out ./bootstrap/...
|
||||
|
||||
- name: Run certs tests
|
||||
if: steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/certs.out ./certs/...
|
||||
|
||||
- name: Run CoAP tests
|
||||
if: steps.changes.outputs.coap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/coap.out ./coap/...
|
||||
|
||||
- name: Run consumers tests
|
||||
if: steps.changes.outputs.consumers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/consumers.out ./consumers/...
|
||||
|
||||
- name: Run HTTP tests
|
||||
if: steps.changes.outputs.http == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/http.out ./http/...
|
||||
|
||||
- name: Run internal tests
|
||||
if: steps.changes.outputs.internal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/internal.out ./internal/...
|
||||
|
||||
- name: Run invitations tests
|
||||
if: steps.changes.outputs.invitations == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/invitations.out ./invitations/...
|
||||
|
||||
- name: Run logger tests
|
||||
if: steps.changes.outputs.logger == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/logger.out ./logger/...
|
||||
|
||||
- name: Run LoRa tests
|
||||
if: steps.changes.outputs.lora == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/lora.out ./lora/...
|
||||
|
||||
- name: Run MQTT tests
|
||||
if: steps.changes.outputs.mqtt == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/mqtt.out ./mqtt/...
|
||||
|
||||
- name: Run OPC-UA tests
|
||||
if: steps.changes.outputs.opcua == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/opcua.out ./opcua/...
|
||||
|
||||
- name: Run pkg auth tests
|
||||
if: steps.changes.outputs.pkg-auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-auth.out ./pkg/auth/...
|
||||
|
||||
- name: Run pkg clients tests
|
||||
if: steps.changes.outputs.pkg-clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-clients.out ./pkg/clients/...
|
||||
|
||||
- name: Run pkg errors tests
|
||||
if: steps.changes.outputs.pkg-errors == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-errors.out ./pkg/errors/...
|
||||
|
||||
- name: Run pkg events tests
|
||||
if: steps.changes.outputs.pkg-events == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-events.out ./pkg/events/...
|
||||
|
||||
- name: Run pkg messaging tests
|
||||
if: steps.changes.outputs.pkg-messaging == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-messaging.out ./pkg/messaging/...
|
||||
|
||||
- name: Run pkg sdk tests
|
||||
if: steps.changes.outputs.pkg-sdk == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-sdk.out ./pkg/sdk/...
|
||||
|
||||
- name: Run pkg transformers tests
|
||||
if: steps.changes.outputs.pkg-transformers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-transformers.out ./pkg/transformers/...
|
||||
|
||||
- name: Run pkg ulid tests
|
||||
if: steps.changes.outputs.pkg-ulid == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-ulid.out ./pkg/ulid/...
|
||||
|
||||
- name: Run pkg uuid tests
|
||||
if: steps.changes.outputs.pkg-uuid == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-uuid.out ./pkg/uuid/...
|
||||
|
||||
- name: Run provision tests
|
||||
if: steps.changes.outputs.provision == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/provision.out ./provision/...
|
||||
|
||||
- name: Run readers tests
|
||||
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/readers.out ./readers/...
|
||||
|
||||
- name: Run things tests
|
||||
if: steps.changes.outputs.things == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/things.out ./things/...
|
||||
|
||||
- name: Run twins tests
|
||||
if: steps.changes.outputs.twins == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/twins.out ./twins/...
|
||||
|
||||
- name: Run users tests
|
||||
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/users.out ./users/...
|
||||
|
||||
- name: Run WebSocket tests
|
||||
if: steps.changes.outputs.ws == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/ws.out ./ws/...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
directory: ./coverage/
|
||||
name: codecov-umbrella
|
||||
verbose: true
|
||||
@@ -0,0 +1,78 @@
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375/
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
GOLANGCI_LINT_VERSION: "v1.55.2"
|
||||
PROTOC_VERSION: "25.3"
|
||||
PROTOC_GEN_VERSION: "v1.32.0"
|
||||
PROTOC_GRPC_VERSION: "v1.3.0"
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- publish
|
||||
|
||||
lint:
|
||||
image: golang:1.21.6-bookworm
|
||||
stage: test
|
||||
before_script:
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VERSION
|
||||
- export PATH=$PATH:$HOME/go/bin
|
||||
script:
|
||||
- golangci-lint run -v
|
||||
|
||||
test:
|
||||
image: golang:1.21.6-bookworm
|
||||
stage: test
|
||||
before_script:
|
||||
- go mod download
|
||||
- make mocks
|
||||
- mkdir -p coverage
|
||||
- touch coverage/coverage.out
|
||||
script:
|
||||
- go test -v --race -count 1 -tags test -coverprofile=coverage/coverage.out $(go list ./... | grep -v 'consumers\|readers\|events\|cache\|messaging\|postgres\|mongodb\|internal\|opcua\|cmd')
|
||||
|
||||
check-generated-files:
|
||||
image: golang:1.21.6-bookworm
|
||||
stage: test
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y unzip curl
|
||||
- PROTOC_ZIP=protoc-$PROTOC_VERSION-linux-x86_64.zip
|
||||
- curl -0L -o $PROTOC_ZIP https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/$PROTOC_ZIP
|
||||
- unzip -o $PROTOC_ZIP -d protoc3
|
||||
- mv protoc3/bin/* /usr/local/bin/
|
||||
- mv protoc3/include/* /usr/local/include/
|
||||
- rm -rf $PROTOC_ZIP protoc3
|
||||
- go install google.golang.org/protobuf/cmd/protoc-gen-go@$PROTOC_GEN_VERSION
|
||||
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$PROTOC_GRPC_VERSION
|
||||
- export PATH=$PATH:/usr/local/bin/protoc
|
||||
- go mod download
|
||||
script:
|
||||
- make check-generated-files
|
||||
|
||||
build:
|
||||
image: golang:1.21.6-bookworm
|
||||
stage: build
|
||||
before_script:
|
||||
- go mod download
|
||||
script:
|
||||
- MG_MESSAGE_BROKER_TYPE=rabbitmq make mqtt
|
||||
- MG_ES_TYPE=redis make mqtt
|
||||
- make all -j $(nproc)
|
||||
|
||||
publish:
|
||||
image: docker:latest
|
||||
stage: publish
|
||||
services:
|
||||
- name: docker:dind
|
||||
entrypoint: ["env", "-u", "DOCKER_HOST"]
|
||||
command: ["dockerd-entrypoint.sh"]
|
||||
before_script:
|
||||
- apk add --no-cache make go git
|
||||
- echo $CI_REGISTRY_PASSWORD | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
|
||||
- go mod download
|
||||
script:
|
||||
- make all -j $(nproc)
|
||||
- MG_DOCKER_IMAGE_NAME_PREFIX=registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala make latest -j $(nproc)
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
@@ -3,11 +3,6 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -0,0 +1,36 @@
|
||||
<!---
|
||||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for existing issues and verify the issue you're about to submit isn't a duplicate.
|
||||
--->
|
||||
|
||||
## What were you trying to achieve?
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
## What are the expected results?
|
||||
|
||||
<!-- A concise description of what you expected to happen. -->
|
||||
|
||||
## What are the received results?
|
||||
|
||||
<!-- A concise description of what actually happened. -->
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
<!-- Describe how one can reproduce the issue - this is very important. Please use an ordered list. -->
|
||||
|
||||
## In what environment did you encounter the issue?
|
||||
|
||||
<!-- Describe the environment you encountered the issue in. -->
|
||||
|
||||
## Additional information you deem important
|
||||
|
||||
<!-- List any additional information that you think might help us debug the issue.
|
||||
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
-->
|
||||
|
||||
/label ~"type::bug"
|
||||
@@ -0,0 +1,25 @@
|
||||
<!---
|
||||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for existing issues and verify the issue you're about to submit isn't a duplicate.
|
||||
--->
|
||||
|
||||
## Is your feature request related to a problem? Please describe?
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
## Describe the feature you'd like
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. Add any considered drawbacks. -->
|
||||
|
||||
## Describe alternatives you've considered
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
## Indicate the importance of this feature to you.
|
||||
|
||||
<!-- How important is this feature to you? How much would you appreciate it? -->
|
||||
|
||||
## Additional context
|
||||
|
||||
<!-- Is there anything else you can add about the proposal? E.g. links to other issues, PRs, etc. -->
|
||||
@@ -1,12 +1,9 @@
|
||||
<!-- Copyright (c) Abstract Machines
|
||||
SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
<!--
|
||||
|
||||
Pull request title should be `MG-XXX - description` or `NOISSUE - description` where XXX is ID of the issue that this PR relate to.
|
||||
Please review the [CONTRIBUTING.md](https://github.com/absmach/magistrala/blob/master/CONTRIBUTING.md) file for detailed contributing guidelines.
|
||||
Please review the [CONTRIBUTING.md](https://gitlab.com/DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/-/blob/main/CONTRIBUTING.md?ref_type=heads) file for detailed contributing guidelines.
|
||||
|
||||
For Work In Progress Pull Requests, please use the Draft PR feature, see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
|
||||
For Work In Progress Pull Requests, please use the Draft PR feature.
|
||||
|
||||
For a timely review/response, please avoid force-pushing additional commits if your PR already received reviews or comments.
|
||||
|
||||
@@ -17,17 +14,17 @@ For a timely review/response, please avoid force-pushing additional commits if y
|
||||
- Update any related documentation.
|
||||
-->
|
||||
|
||||
# What type of PR is this?
|
||||
## What type of PR is this?
|
||||
|
||||
<!--This represents the type of PR you are submitting.
|
||||
|
||||
For example:
|
||||
This is a bug fix because it fixes the following issue: #1234
|
||||
This is a feature because it adds the following functionality: ...
|
||||
This is a refactor because it changes the following functionality: ...
|
||||
This is a documentation update because it updates the following documentation: ...
|
||||
This is a dependency update because it updates the following dependencies: ...
|
||||
This is an optimization because it improves the following functionality: ...
|
||||
This PR is a bug fix because it fixes the following issue: #1234
|
||||
This PR is a feature because it adds the following functionality: ...
|
||||
This PR is a refactor because it changes the following functionality: ...
|
||||
This PR is a documentation update because it updates the following documentation: ...
|
||||
This PR is a dependency update because it updates the following dependencies: ...
|
||||
This PR is an optimization because it improves the following functionality: ...
|
||||
-->
|
||||
|
||||
## What does this do?
|
||||
@@ -45,8 +42,10 @@ For pull requests that relate or close an issue, please include them below. We
|
||||
For example having the text: "Resolves #1234" would connect the current pull request to issue 1234. And when we merge the pull request, Github will automatically close the issue.
|
||||
-->
|
||||
|
||||
<!--
|
||||
- Related Issue #
|
||||
- Resolves #
|
||||
-->
|
||||
|
||||
## Have you included tests for your changes?
|
||||
|
||||
@@ -54,10 +54,6 @@ linters-settings:
|
||||
- "mosquitto"
|
||||
stylecheck:
|
||||
checks: ["-ST1000", "-ST1003", "-ST1020", "-ST1021", "-ST1022"]
|
||||
goheader:
|
||||
template: |-
|
||||
Copyright (c) Abstract Machines
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
@@ -78,7 +74,6 @@ linters:
|
||||
- gofmt
|
||||
- goimports
|
||||
- loggercheck
|
||||
- goheader
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
# Adopters
|
||||
|
||||
As Magistrala Community grows, we'd like to keep track of Magistrala adopters to grow the community, contact other users, share experiences and best practices.
|
||||
|
||||
To accomplish this, we created a public ledger. The list of organizations and users who consider themselves as Magistrala adopters and that **publicly/officially** shared information and/or details of their adoption journey(optional).
|
||||
Where users themselves directly maintain the list.
|
||||
|
||||
## Adding yourself as an adopter
|
||||
If you are using Magistrala, please consider adding yourself as an adopter with a brief description of your use case by opening a pull request to this file and adding a section describing your adoption of Magistrala technology.
|
||||
|
||||
**Please send PRs to add or remove organizations/users**
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
N: Name of user (company or individual)
|
||||
D: Short Use Case Description (optional)
|
||||
L: Link with further information (optional)
|
||||
T: Type of adaptation: Evaluation, Core Technology, Production Usage (optional)
|
||||
```
|
||||
|
||||
## Requirements
|
||||
* You must represent the user or organization listed. Do NOT add entries on behalf of other organizations or individuals.
|
||||
Pull request commit must be [signed](https://docs.github.com/en/github/authenticating-to-github/signing-commits) and auto-checked with [ Developer Certificate of Origin (DCO)](https://probot.github.io/apps/dco/)
|
||||
* There is no minimum requirement or adaptation size, but we request to list permanent deployments only, i.e., no demo or trial deployments. Commercial or production use is not required. A well-done home lab setup can be equally impressive as a large-scale commercial deployment.
|
||||
|
||||
|
||||
**The list of organizations/users that have publicly shared the usage of Magistrala:**
|
||||
|
||||
**Note**: Several other organizations/users couldn't publicly share their usage details but are active project contributors and Magistrala Community members.
|
||||
|
||||
|
||||
## Adopters list (alphabetical)
|
||||
|
||||
|
||||
**Note:** The list is maintained by the users themselves. If you find yourself on this list, and you think it's inappropriate. Please contact [project maintainers](https://github.com/absmach/magistrala/blob/master/MAINTAINERS) and you will be permanently removed from the list.
|
||||
+20
-38
@@ -1,19 +1,14 @@
|
||||
# Contributing to Magistrala
|
||||
|
||||
The following is a set of guidelines to contribute to Magistrala and its libraries, which are
|
||||
hosted on the [Abstract Machines Organization](https://github.com/absmach) on GitHub.
|
||||
The following is a set of guidelines to contribute to Magistrala and its libraries, which are hosted on the [Abstract Machines Organization](https://gitlab.com/DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines) on Gitlab.
|
||||
|
||||
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to
|
||||
[abuse@magistrala.com](mailto:abuse@magistrala.com).
|
||||
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0). By participating, you are expected to uphold this code. Please report unacceptable behavior to [abuse@magistrala.com](mailto:abuse@magistrala.com).
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written,
|
||||
thorough bug report.
|
||||
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written, thorough bug report.
|
||||
|
||||
Before raising a new issue, check [our issue
|
||||
list](https://github.com/absmach/magistrala/issues) to determine if it already contains the
|
||||
Before raising a new issue, check [our issue list](https://gitlab.com/DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/-/issues) to determine if it already contains the
|
||||
problem that you are facing.
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you for more information. Please be as detailed as possible. The following questions might serve as a template for writing a detailed
|
||||
@@ -27,61 +22,48 @@ report:
|
||||
|
||||
## Pull requests
|
||||
|
||||
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should
|
||||
remain focused in scope and avoid unrelated commits.
|
||||
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should remain focused in scope and avoid unrelated commits.
|
||||
|
||||
**Please ask first** before embarking on any significant pull request (e.g. implementing new features,
|
||||
refactoring code etc.), otherwise you risk spending a lot of time working on something that the
|
||||
maintainers might not want to merge into the project.
|
||||
**Please ask first** before embarking on any significant pull request (e.g. implementing new features, refactoring code etc.), otherwise you risk spending a lot of time working on something that the maintainers might not want to merge into the project.
|
||||
|
||||
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
|
||||
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
|
||||
Please adhere to the coding conventions used throughout the project. If in doubt, consult the [Effective Go](https://golang.org/doc/effective_go.html) style guide.
|
||||
|
||||
To contribute to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
|
||||
clone your fork repository, and configure the remotes:
|
||||
To contribute to the project, clone the repository:
|
||||
|
||||
```
|
||||
git clone https://github.com/<your-username>/magistrala.git
|
||||
```bash
|
||||
git clone https://gitlab.com/DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala.git
|
||||
cd magistrala
|
||||
git remote add upstream https://github.com/absmach/magistrala.git
|
||||
```
|
||||
|
||||
If your cloned repository is behind the upstream commits, then get the latest changes from upstream:
|
||||
|
||||
```
|
||||
git checkout master
|
||||
```bash
|
||||
git checkout main
|
||||
git pull --rebase upstream main
|
||||
```
|
||||
|
||||
Create a new topic branch from `master` using the naming convention `MG-[issue-number]`
|
||||
to help us keep track of your contribution scope:
|
||||
Create a new topic branch from `main` using the naming convention `MG-[issue-number]` to help us keep track of your contribution scope:
|
||||
|
||||
```
|
||||
```bash
|
||||
git checkout -b MG-[issue-number]
|
||||
```
|
||||
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're unsure of what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of work before making it public.
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages) if you're unsure of what constitutes a Good Commit Message™. Use [interactive rebase](https://docs.gitlab.com/ee/topics/git/git_rebase.html) to group your commits into logical units of work before making it public.
|
||||
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you
|
||||
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
|
||||
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
|
||||
Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```
|
||||
```bash
|
||||
git pull --rebase upstream main
|
||||
```
|
||||
|
||||
Push your topic branch up to your fork:
|
||||
|
||||
```
|
||||
```bash
|
||||
git push origin MG-[issue-number]
|
||||
```
|
||||
|
||||
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
and detailed description.
|
||||
[Open a Pull Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) with a clear title and detailed description.
|
||||
|
||||
@@ -116,18 +116,29 @@ install:
|
||||
cp $$file $(GOBIN)/magistrala-`basename $$file`; \
|
||||
done
|
||||
|
||||
check-generated-files:
|
||||
$(MAKE) proto
|
||||
@if git diff --name-only | grep .pb.go; then \
|
||||
echo "Error generated files are out of sync. Please run 'make proto' and commit the changes."; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(MAKE) mocks
|
||||
@if git diff --name-only | grep mocks; then \
|
||||
echo "Error mocks are out of sync. Please run 'make mocks' and commit the changes."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
mocks:
|
||||
@which mockery > /dev/null || go install github.com/vektra/mockery/v2@$(MOCKERY_VERSION)
|
||||
@unset MOCKERY_VERSION && go generate ./...
|
||||
|
||||
|
||||
DIRS = consumers readers postgres internal opcua
|
||||
DIRS = consumers readers events cache messaging postgres internal opcua
|
||||
test: mocks
|
||||
mkdir -p coverage
|
||||
@for dir in $(DIRS); do \
|
||||
go test -v --race -count 1 -tags test -coverprofile=coverage/$$dir.out $$(go list ./... | grep $$dir | grep -v 'cmd'); \
|
||||
done
|
||||
go test -v --race -count 1 -tags test -coverprofile=coverage/coverage.out $$(go list ./... | grep -v 'consumers\|readers\|postgres\|internal\|opcua\|cmd')
|
||||
go test -v --race -count 1 -tags test -coverprofile=coverage/coverage.out $$(go list ./... | grep -v 'consumers\|readers\|events\|cache\|messaging\|postgres\|mongodb\|internal\|opcua\|cmd')
|
||||
|
||||
proto:
|
||||
protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto
|
||||
@@ -154,7 +165,8 @@ endef
|
||||
changelog:
|
||||
git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s"
|
||||
|
||||
latest: dockers
|
||||
# using dev since it takes less time to build for CI
|
||||
latest: dockers_dev
|
||||
$(call docker_push,latest)
|
||||
|
||||
release:
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Magistrala
|
||||
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/check-license.yaml)
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/check-generated-files.yml)
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/build.yml)
|
||||
[![go report card][grc-badge]][grc-url]
|
||||
[![coverage][cov-badge]][cov-url]
|
||||
[![license][license]](LICENSE)
|
||||
@@ -60,12 +57,11 @@ This will bring up the Magistrala docker services and interconnect them. This co
|
||||
make run
|
||||
```
|
||||
|
||||
If you want to run services from specific release checkout code from github and make sure that
|
||||
`MG_RELEASE_TAG` in [.env](.env) is being set to match the release version
|
||||
If you want to run services from specific release checkout code from github and make sure that `MG_RELEASE_TAG` in [.env](.env) is being set to match the release version
|
||||
|
||||
```bash
|
||||
git checkout tags/<release_number> -b <release_number>
|
||||
# e.g. `git checkout tags/0.13.0 -b 0.13.0`
|
||||
# e.g. `git checkout tags/0.14.0 -b 0.14.0`
|
||||
```
|
||||
|
||||
Check that `.env` file contains:
|
||||
@@ -121,12 +117,6 @@ Thank you for your interest in Magistrala and the desire to contribute!
|
||||
2. Checkout the [contribution guide](CONTRIBUTING.md) to learn more about our style and conventions.
|
||||
3. Make your changes compatible to our workflow.
|
||||
|
||||
### We're Hiring
|
||||
|
||||
You like Magistrala and you would like to make it your day job? We're always looking for talented engineers interested in open-source, IoT and distributed systems. If you recognize yourself, reach out to [@drasko][drasko] - he will contact you back.
|
||||
|
||||
> The best way to grab our attention is, of course, by sending PRs :sunglasses:.
|
||||
|
||||
## Community
|
||||
|
||||
- [Google group][forum]
|
||||
|
||||
+46
-34
@@ -59,40 +59,46 @@ Domain consists of the following fields:
|
||||
|
||||
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_AUTH_LOG_LEVEL | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| MG_AUTH_DB_HOST | Database host address | localhost |
|
||||
| MG_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| MG_AUTH_DB_USER | Database user | magistrala |
|
||||
| MG_AUTH_DB_PASSWORD | Database password | magistrala |
|
||||
| MG_AUTH_DB_NAME | Name of the database used by the service | auth |
|
||||
| MG_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_AUTH_HTTP_HOST | Auth service HTTP host | "" |
|
||||
| MG_AUTH_HTTP_PORT | Auth service HTTP port | 8189 |
|
||||
| MG_AUTH_HTTP_SERVER_CERT | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| MG_AUTH_HTTP_SERVER_KEY | Path to the PEM encoded HTTP server key file | "" |
|
||||
| MG_AUTH_GRPC_HOST | Auth service gRPC host | "" |
|
||||
| MG_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| MG_AUTH_GRPC_SERVER_CERT | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_KEY | Path to the PEM encoded gRPC server key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_CA_CERTS | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| MG_AUTH_SECRET_KEY | String used for signing tokens | secret |
|
||||
| MG_AUTH_ACCESS_TOKEN_DURATION | The access token expiration period | 1h |
|
||||
| MG_AUTH_REFRESH_TOKEN_DURATION | The refresh token expiration period | 24h |
|
||||
| MG_AUTH_INVITATION_DURATION | The invitation token expiration period | 168h |
|
||||
| MG_SPICEDB_HOST | SpiceDB host address | localhost |
|
||||
| MG_SPICEDB_PORT | SpiceDB host port | 50051 |
|
||||
| MG_SPICEDB_PRE_SHARED_KEY | SpiceDB pre-shared key | 12345678 |
|
||||
| MG_SPICEDB_SCHEMA_FILE | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://jaeger:14268/api/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| MG_AUTH_ADAPTER_INSTANCE_ID | Adapter instance ID | "" |
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| MG_AUTH_LOG_LEVEL | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| MG_AUTH_DB_HOST | Database host address | localhost |
|
||||
| MG_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| MG_AUTH_DB_USER | Database user | magistrala |
|
||||
| MG_AUTH_DB_PASSWORD | Database password | magistrala |
|
||||
| MG_AUTH_DB_NAME | Name of the database used by the service | auth |
|
||||
| MG_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_AUTH_HTTP_HOST | Auth service HTTP host | "" |
|
||||
| MG_AUTH_HTTP_PORT | Auth service HTTP port | 8189 |
|
||||
| MG_AUTH_HTTP_SERVER_CERT | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| MG_AUTH_HTTP_SERVER_KEY | Path to the PEM encoded HTTP server key file | "" |
|
||||
| MG_AUTH_GRPC_HOST | Auth service gRPC host | "" |
|
||||
| MG_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| MG_AUTH_GRPC_SERVER_CERT | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_KEY | Path to the PEM encoded gRPC server key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_CA_CERTS | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| MG_AUTH_SECRET_KEY | String used for signing tokens | secret |
|
||||
| MG_AUTH_ACCESS_TOKEN_DURATION | The access token expiration period | 1h |
|
||||
| MG_AUTH_REFRESH_TOKEN_DURATION | The refresh token expiration period | 24h |
|
||||
| MG_AUTH_INVITATION_DURATION | The invitation token expiration period | 168h |
|
||||
| MG_SPICEDB_HOST | SpiceDB host address | localhost |
|
||||
| MG_SPICEDB_PORT | SpiceDB host port | 50051 |
|
||||
| MG_SPICEDB_PRE_SHARED_KEY | SpiceDB pre-shared key | 12345678 |
|
||||
| MG_SPICEDB_SCHEMA_FILE | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://jaeger:14268/api/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| MG_AUTH_ADAPTER_INSTANCE_ID | Adapter instance ID | "" |
|
||||
| MG_KRATOS_CLIENT_ID | Kratos client ID | "" |
|
||||
| MG_KRATOS_CLIENT_SECRET | Kratos client secret | "" |
|
||||
| MG_KRATOS_REDIRECT_URL | Kratos redirect URL | <http://localhost/oauth/callback/kratos> |
|
||||
| MG_KRATOS_STATE | Kratos state | "" |
|
||||
| MG_KRATOS_URL | Kratos URL | <http://localhost:4433> |
|
||||
| MG_KRATOS_API_KEY | Kratos API key | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -146,6 +152,12 @@ MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_AUTH_ADAPTER_INSTANCE_ID="" \
|
||||
MG_KRATOS_CLIENT_ID="" \
|
||||
MG_KRATOS_CLIENT_SECRET="" \
|
||||
MG_KRATOS_REDIRECT_URL=http://localhost/oauth/callback/kratos \
|
||||
MG_KRATOS_STATE="" \
|
||||
MG_KRATOS_URL=http://localhost:4433 \
|
||||
MG_KRATOS_API_KEY="" \
|
||||
$GOBIN/magistrala-auth
|
||||
```
|
||||
|
||||
|
||||
+7
-5
@@ -31,7 +31,7 @@ import (
|
||||
httpserver "github.com/absmach/magistrala/internal/server/http"
|
||||
mglog "github.com/absmach/magistrala/logger"
|
||||
"github.com/absmach/magistrala/pkg/oauth2"
|
||||
"github.com/absmach/magistrala/pkg/oauth2/google"
|
||||
"github.com/absmach/magistrala/pkg/oauth2/kratos"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
|
||||
"github.com/authzed/authzed-go/v1"
|
||||
@@ -50,7 +50,7 @@ const (
|
||||
envPrefixHTTP = "MG_AUTH_HTTP_"
|
||||
envPrefixGrpc = "MG_AUTH_GRPC_"
|
||||
envPrefixDB = "MG_AUTH_DB_"
|
||||
envPrefixGoogle = "MG_GOOGLE_"
|
||||
envPrefixKratos = "MG_KRATOS_"
|
||||
defDB = "auth"
|
||||
defSvcHTTPPort = "8180"
|
||||
defSvcGRPCPort = "8181"
|
||||
@@ -70,6 +70,8 @@ type config struct {
|
||||
SpicedbSchemaFile string `env:"MG_SPICEDB_SCHEMA_FILE" envDefault:"./docker/spicedb/schema.zed"`
|
||||
SpicedbPreSharedKey string `env:"MG_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"`
|
||||
TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
KratosURL string `env:"MG_KRATOS_URL" envDefault:"http://localhost:4433"`
|
||||
KratosAPIKey string `env:"MG_KRATOS_API_KEY" envDefault:""`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -131,12 +133,12 @@ func main() {
|
||||
}
|
||||
|
||||
oauthConfig := oauth2.Config{}
|
||||
if err := env.ParseWithOptions(&oauthConfig, env.Options{Prefix: envPrefixGoogle}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load %s Google configuration : %s", svcName, err.Error()))
|
||||
if err := env.ParseWithOptions(&oauthConfig, env.Options{Prefix: envPrefixKratos}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load %s Kratos configuration : %s", svcName, err.Error()))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
oauthProvider := google.NewProvider(oauthConfig, "", "")
|
||||
oauthProvider := kratos.NewProvider(oauthConfig, cfg.KratosURL, "", "", cfg.KratosAPIKey)
|
||||
|
||||
svc := newService(db, tracer, cfg, dbConfig, logger, spicedbclient, oauthProvider)
|
||||
|
||||
|
||||
+40
-42
@@ -35,18 +35,19 @@ import (
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/groups"
|
||||
"github.com/absmach/magistrala/pkg/oauth2"
|
||||
googleoauth "github.com/absmach/magistrala/pkg/oauth2/google"
|
||||
kratosoauth "github.com/absmach/magistrala/pkg/oauth2/kratos"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
"github.com/absmach/magistrala/users"
|
||||
capi "github.com/absmach/magistrala/users/api"
|
||||
"github.com/absmach/magistrala/users/emailer"
|
||||
uevents "github.com/absmach/magistrala/users/events"
|
||||
"github.com/absmach/magistrala/users/hasher"
|
||||
clientspg "github.com/absmach/magistrala/users/postgres"
|
||||
"github.com/absmach/magistrala/users/kratos"
|
||||
ctracing "github.com/absmach/magistrala/users/tracing"
|
||||
"github.com/caarlos0/env/v10"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jmoiron/sqlx"
|
||||
ory "github.com/ory/client-go"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -56,7 +57,7 @@ const (
|
||||
envPrefixDB = "MG_USERS_DB_"
|
||||
envPrefixHTTP = "MG_USERS_HTTP_"
|
||||
envPrefixAuth = "MG_AUTH_GRPC_"
|
||||
envPrefixGoogle = "MG_GOOGLE_"
|
||||
envPrefixKratos = "MG_KRATOS_"
|
||||
defDB = "users"
|
||||
defSvcHTTPPort = "9002"
|
||||
|
||||
@@ -64,19 +65,22 @@ const (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
LogLevel string `env:"MG_USERS_LOG_LEVEL" envDefault:"info"`
|
||||
AdminEmail string `env:"MG_USERS_ADMIN_EMAIL" envDefault:"admin@example.com"`
|
||||
AdminPassword string `env:"MG_USERS_ADMIN_PASSWORD" envDefault:"12345678"`
|
||||
PassRegexText string `env:"MG_USERS_PASS_REGEX" envDefault:"^.{8,}$"`
|
||||
ResetURL string `env:"MG_TOKEN_RESET_ENDPOINT" envDefault:"/reset-request"`
|
||||
JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:14268/api/traces"`
|
||||
SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"`
|
||||
InstanceID string `env:"MG_USERS_INSTANCE_ID" envDefault:""`
|
||||
ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
SelfRegister bool `env:"MG_USERS_ALLOW_SELF_REGISTER" envDefault:"false"`
|
||||
OAuthUIRedirectURL string `env:"MG_OAUTH_UI_REDIRECT_URL" envDefault:"http://localhost:9095/domains"`
|
||||
OAuthUIErrorURL string `env:"MG_OAUTH_UI_ERROR_URL" envDefault:"http://localhost:9095/error"`
|
||||
LogLevel string `env:"MG_USERS_LOG_LEVEL" envDefault:"info"`
|
||||
AdminEmail string `env:"MG_USERS_ADMIN_EMAIL" envDefault:"admin@example.com"`
|
||||
AdminPassword string `env:"MG_USERS_ADMIN_PASSWORD" envDefault:"12345678"`
|
||||
PassRegexText string `env:"MG_USERS_PASS_REGEX" envDefault:"^.{8,}$"`
|
||||
ResetURL string `env:"MG_TOKEN_RESET_ENDPOINT" envDefault:"/reset-request"`
|
||||
JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:14268/api/traces"`
|
||||
SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"`
|
||||
InstanceID string `env:"MG_USERS_INSTANCE_ID" envDefault:""`
|
||||
ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
SelfRegister bool `env:"MG_USERS_ALLOW_SELF_REGISTER" envDefault:"false"`
|
||||
OAuthUIRedirectURL string `env:"MG_OAUTH_UI_REDIRECT_URL" envDefault:"http://localhost:9095/domains"`
|
||||
OAuthUIErrorURL string `env:"MG_OAUTH_UI_ERROR_URL" envDefault:"http://localhost:9095/error"`
|
||||
KratosURL string `env:"MG_KRATOS_URL" envDefault:"http://localhost:4433"`
|
||||
KratosAPIKey string `env:"MG_KRATOS_API_KEY" envDefault:""`
|
||||
KratosSchemaID string `env:"MG_KRATOS_SCHEMA_ID" envDefault:""`
|
||||
PassRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
@@ -123,10 +127,8 @@ func main() {
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
cm := clientspg.Migration()
|
||||
gm := gpostgres.Migration()
|
||||
cm.Migrations = append(cm.Migrations, gm.Migrations...)
|
||||
db, err := pgclient.Setup(dbConfig, *cm)
|
||||
db, err := pgclient.Setup(dbConfig, *gm)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
@@ -178,12 +180,12 @@ func main() {
|
||||
}
|
||||
|
||||
oauthConfig := oauth2.Config{}
|
||||
if err := env.ParseWithOptions(&oauthConfig, env.Options{Prefix: envPrefixGoogle}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load %s Google configuration : %s", svcName, err.Error()))
|
||||
if err := env.ParseWithOptions(&oauthConfig, env.Options{Prefix: envPrefixKratos}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load %s Kratos configuration : %s", svcName, err.Error()))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
oauthProvider := googleoauth.NewProvider(oauthConfig, cfg.OAuthUIRedirectURL, cfg.OAuthUIErrorURL)
|
||||
oauthProvider := kratosoauth.NewProvider(oauthConfig, cfg.KratosURL, cfg.OAuthUIRedirectURL, cfg.OAuthUIErrorURL, cfg.KratosAPIKey)
|
||||
|
||||
mux := chi.NewRouter()
|
||||
httpSrv := httpserver.New(ctx, cancel, svcName, httpServerConfig, capi.MakeHandler(csvc, gsvc, mux, logger, cfg.InstanceID, oauthProvider), logger)
|
||||
@@ -207,19 +209,25 @@ func main() {
|
||||
}
|
||||
|
||||
func newService(ctx context.Context, authClient magistrala.AuthServiceClient, db *sqlx.DB, dbConfig pgclient.Config, tracer trace.Tracer, c config, ec email.Config, logger *slog.Logger) (users.Service, groups.Service, error) {
|
||||
hsr := hasher.New()
|
||||
|
||||
database := postgres.NewDatabase(db, dbConfig, tracer)
|
||||
cRepo := clientspg.NewRepository(database)
|
||||
|
||||
conf := ory.NewConfiguration()
|
||||
conf.Servers = []ory.ServerConfiguration{{URL: c.KratosURL}}
|
||||
conf.AddDefaultHeader("Authorization", "Bearer "+c.KratosAPIKey)
|
||||
client := ory.NewAPIClient(conf)
|
||||
cRepo := kratos.NewRepository(client, c.KratosSchemaID, hsr)
|
||||
gRepo := gpostgres.New(database)
|
||||
|
||||
idp := uuid.New()
|
||||
hsr := hasher.New()
|
||||
|
||||
emailerClient, err := emailer.New(c.ResetURL, &ec)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error()))
|
||||
}
|
||||
|
||||
csvc := users.NewService(cRepo, authClient, emailerClient, hsr, idp, c.PassRegex, c.SelfRegister)
|
||||
csvc := users.NewService(cRepo, authClient, emailerClient, c.PassRegex, c.SelfRegister, client)
|
||||
gsvc := mggroups.NewService(gRepo, idp, authClient)
|
||||
|
||||
csvc, err = uevents.NewEventStoreMiddleware(ctx, csvc, c.ESURL)
|
||||
@@ -241,9 +249,9 @@ func newService(ctx context.Context, authClient magistrala.AuthServiceClient, db
|
||||
counter, latency = internal.MakeMetrics("groups", "api")
|
||||
gsvc = gapi.MetricsMiddleware(gsvc, counter, latency)
|
||||
|
||||
clientID, err := createAdmin(ctx, c, cRepo, hsr, csvc)
|
||||
clientID, err := createAdmin(ctx, c, cRepo, csvc)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create admin client: %s", err))
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := createAdminPolicy(ctx, clientID, authClient); err != nil {
|
||||
return nil, nil, err
|
||||
@@ -251,22 +259,12 @@ func newService(ctx context.Context, authClient magistrala.AuthServiceClient, db
|
||||
return csvc, gsvc, err
|
||||
}
|
||||
|
||||
func createAdmin(ctx context.Context, c config, crepo clientspg.Repository, hsr users.Hasher, svc users.Service) (string, error) {
|
||||
id, err := uuid.New().ID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash, err := hsr.Hash(c.AdminPassword)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func createAdmin(ctx context.Context, c config, crepo users.Repository, svc users.Service) (string, error) {
|
||||
client := mgclients.Client{
|
||||
ID: id,
|
||||
Name: "admin",
|
||||
Name: "admin-client",
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: c.AdminEmail,
|
||||
Secret: hash,
|
||||
Secret: c.AdminPassword,
|
||||
},
|
||||
Metadata: mgclients.Metadata{
|
||||
"role": "admin",
|
||||
@@ -282,10 +280,10 @@ func createAdmin(ctx context.Context, c config, crepo clientspg.Repository, hsr
|
||||
}
|
||||
|
||||
// Create an admin
|
||||
if _, err = crepo.Save(ctx, client); err != nil {
|
||||
if _, err := crepo.Save(ctx, client); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = svc.IssueToken(ctx, c.AdminEmail, c.AdminPassword, ""); err != nil {
|
||||
if _, err := svc.IssueToken(ctx, c.AdminEmail, c.AdminPassword, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return client.ID, nil
|
||||
|
||||
+10
-5
@@ -168,6 +168,11 @@ MG_USERS_ALLOW_SELF_REGISTER=true
|
||||
MG_OAUTH_UI_REDIRECT_URL="http://localhost:9095/domains"
|
||||
MG_OAUTH_UI_ERROR_URL="http://localhost:9095/error"
|
||||
|
||||
#### Kratos
|
||||
MG_KRATOS_URL=
|
||||
MG_KRATOS_SCHEMA_ID=
|
||||
MG_KRATOS_API_KEY=
|
||||
|
||||
### Email utility
|
||||
MG_EMAIL_HOST=smtp.mailtrap.io
|
||||
MG_EMAIL_PORT=2525
|
||||
@@ -177,11 +182,11 @@ MG_EMAIL_FROM_ADDRESS=from@example.com
|
||||
MG_EMAIL_FROM_NAME=Example
|
||||
MG_EMAIL_TEMPLATE=email.tmpl
|
||||
|
||||
### Google OAuth2
|
||||
MG_GOOGLE_CLIENT_ID=
|
||||
MG_GOOGLE_CLIENT_SECRET=
|
||||
MG_GOOGLE_REDIRECT_URL=
|
||||
MG_GOOGLE_STATE=
|
||||
### Kratos OAuth2
|
||||
MG_KRATOS_CLIENT_ID=
|
||||
MG_KRATOS_CLIENT_SECRET=
|
||||
MG_KRATOS_REDIRECT_URL="http://localhost/oauth/callback/kratos"
|
||||
MG_KRATOS_STATE=
|
||||
|
||||
### Things
|
||||
MG_THINGS_LOG_LEVEL=debug
|
||||
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
- magistrala-bootstrap-db-volume:/var/lib/postgresql/data
|
||||
|
||||
bootstrap:
|
||||
image: magistrala/bootstrap:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/bootstrap:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-bootstrap
|
||||
depends_on:
|
||||
- bootstrap-db
|
||||
|
||||
@@ -13,7 +13,7 @@ networks:
|
||||
|
||||
services:
|
||||
cassandra-reader:
|
||||
image: magistrala/cassandra-reader:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/cassandra-reader:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-cassandra-reader
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
- magistrala-base-net
|
||||
|
||||
cassandra-writer:
|
||||
image: magistrala/cassandra-writer:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/cassandra-writer:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-cassandra-writer
|
||||
depends_on:
|
||||
- cassandra
|
||||
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
- magistrala-certs-db-volume:/var/lib/postgresql/data
|
||||
|
||||
certs:
|
||||
image: magistrala/certs:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/certs:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-certs
|
||||
depends_on:
|
||||
- certs-db
|
||||
|
||||
@@ -15,7 +15,7 @@ networks:
|
||||
|
||||
services:
|
||||
influxdb-reader:
|
||||
image: magistrala/influxdb-reader:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/influxdb-reader:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-influxdb-reader
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
- magistrala-influxdb-volume:/var/lib/influxdb
|
||||
|
||||
influxdb-writer:
|
||||
image: magistrala/influxdb-writer:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/influxdb-writer:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-influxdb-writer
|
||||
depends_on:
|
||||
- influxdb
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- magistrala-base-net
|
||||
|
||||
lora-adapter:
|
||||
image: magistrala/lora:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/lora:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-lora
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -14,7 +14,7 @@ networks:
|
||||
|
||||
services:
|
||||
mongodb-reader:
|
||||
image: magistrala/mongodb-reader:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/mongodb-reader:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-mongodb-reader
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
- magistrala-mongodb-configdb-volume:/data/configdb
|
||||
|
||||
mongodb-writer:
|
||||
image: magistrala/mongodb-writer:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/mongodb-writer:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-mongodb-writer
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
- magistrala-opcua-redis-volume:/data
|
||||
|
||||
opcua-adapter:
|
||||
image: magistrala/opcua:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/opcua:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-opcua
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -14,7 +14,7 @@ networks:
|
||||
|
||||
services:
|
||||
postgres-reader:
|
||||
image: magistrala/postgres-reader:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/postgres-reader:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-postgres-reader
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
- magistrala-postgres-writer-volume:/var/lib/postgresql/data
|
||||
|
||||
postgres-writer:
|
||||
image: magistrala/postgres-writer:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/postgres-writer:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-postgres-writer
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
@@ -13,7 +13,7 @@ networks:
|
||||
|
||||
services:
|
||||
provision:
|
||||
image: magistrala/provision:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/provision:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-provision
|
||||
restart: on-failure
|
||||
networks:
|
||||
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
- magistrala-smpp-notifier-volume:/var/lib/postgresql/datab
|
||||
|
||||
smpp-notifier:
|
||||
image: magistrala/smpp-notifier:latest
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/smpp-notifier:latest
|
||||
container_name: magistrala-smpp-notifier
|
||||
depends_on:
|
||||
- smpp-notifier-db
|
||||
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
- magistrala-smtp-notifier-volume:/var/lib/postgresql/datab
|
||||
|
||||
smtp-notifier:
|
||||
image: magistrala/smtp-notifier:latest
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/smtp-notifier:latest
|
||||
container_name: magistrala-smtp-notifier
|
||||
depends_on:
|
||||
- smtp-notifier-db
|
||||
|
||||
@@ -14,7 +14,7 @@ networks:
|
||||
|
||||
services:
|
||||
timescale-reader:
|
||||
image: magistrala/timescale-reader:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/timescale-reader:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-timescale-reader
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
- magistrala-timescale-writer-volume:/var/lib/timescalesql/data
|
||||
|
||||
timescale-writer:
|
||||
image: magistrala/timescale-writer:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/timescale-writer:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-timescale-writer
|
||||
depends_on:
|
||||
- timescale
|
||||
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
- magistrala-twins-db-configdb-volume:/data/configdb
|
||||
|
||||
twins:
|
||||
image: magistrala/twins:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/twins:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-twins
|
||||
restart: on-failure
|
||||
environment:
|
||||
|
||||
+28
-22
@@ -81,7 +81,7 @@ services:
|
||||
- magistrala-auth-db-volume:/var/lib/postgresql/data
|
||||
|
||||
auth:
|
||||
image: magistrala/auth:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/auth:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-auth
|
||||
depends_on:
|
||||
- auth-db
|
||||
@@ -125,10 +125,12 @@ services:
|
||||
MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO}
|
||||
MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY}
|
||||
MG_AUTH_ADAPTER_INSTANCE_ID: ${MG_AUTH_ADAPTER_INSTANCE_ID}
|
||||
MG_GOOGLE_CLIENT_ID: ${MG_GOOGLE_CLIENT_ID}
|
||||
MG_GOOGLE_CLIENT_SECRET: ${MG_GOOGLE_CLIENT_SECRET}
|
||||
MG_GOOGLE_REDIRECT_URL: ${MG_GOOGLE_REDIRECT_URL}
|
||||
MG_GOOGLE_STATE: ${MG_GOOGLE_STATE}
|
||||
MG_KRATOS_CLIENT_ID: ${MG_KRATOS_CLIENT_ID}
|
||||
MG_KRATOS_CLIENT_SECRET: ${MG_KRATOS_CLIENT_SECRET}
|
||||
MG_KRATOS_REDIRECT_URL: ${MG_KRATOS_REDIRECT_URL}
|
||||
MG_KRATOS_STATE: ${MG_KRATOS_STATE}
|
||||
MG_KRATOS_URL: ${MG_KRATOS_URL}
|
||||
MG_KRATOS_API_KEY: ${MG_KRATOS_API_KEY}
|
||||
ports:
|
||||
- ${MG_AUTH_HTTP_PORT}:${MG_AUTH_HTTP_PORT}
|
||||
- ${MG_AUTH_GRPC_PORT}:${MG_AUTH_GRPC_PORT}
|
||||
@@ -176,7 +178,7 @@ services:
|
||||
- magistrala-invitations-db-volume:/var/lib/postgresql/data
|
||||
|
||||
invitations:
|
||||
image: magistrala/invitations:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/invitations:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-invitations
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -294,7 +296,7 @@ services:
|
||||
- magistrala-things-redis-volume:/data
|
||||
|
||||
things:
|
||||
image: magistrala/things:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/things:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-things
|
||||
depends_on:
|
||||
- things-db
|
||||
@@ -399,7 +401,7 @@ services:
|
||||
- magistrala-users-db-volume:/var/lib/postgresql/data
|
||||
|
||||
users:
|
||||
image: magistrala/users:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/users:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-users
|
||||
depends_on:
|
||||
- users-db
|
||||
@@ -445,12 +447,15 @@ services:
|
||||
MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
MG_AUTH_GRPC_CLIENT_KEY: ${MG_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
|
||||
MG_AUTH_GRPC_SERVER_CA_CERTS: ${MG_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
|
||||
MG_GOOGLE_CLIENT_ID: ${MG_GOOGLE_CLIENT_ID}
|
||||
MG_GOOGLE_CLIENT_SECRET: ${MG_GOOGLE_CLIENT_SECRET}
|
||||
MG_GOOGLE_REDIRECT_URL: ${MG_GOOGLE_REDIRECT_URL}
|
||||
MG_GOOGLE_STATE: ${MG_GOOGLE_STATE}
|
||||
MG_KRATOS_CLIENT_ID: ${MG_KRATOS_CLIENT_ID}
|
||||
MG_KRATOS_CLIENT_SECRET: ${MG_KRATOS_CLIENT_SECRET}
|
||||
MG_KRATOS_REDIRECT_URL: ${MG_KRATOS_REDIRECT_URL}
|
||||
MG_KRATOS_STATE: ${MG_KRATOS_STATE}
|
||||
MG_OAUTH_UI_REDIRECT_URL: ${MG_OAUTH_UI_REDIRECT_URL}
|
||||
MG_OAUTH_UI_ERROR_URL: ${MG_OAUTH_UI_ERROR_URL}
|
||||
MG_KRATOS_URL: ${MG_KRATOS_URL}
|
||||
MG_KRATOS_SCHEMA_ID: ${MG_KRATOS_SCHEMA_ID}
|
||||
MG_KRATOS_API_KEY: ${MG_KRATOS_API_KEY}
|
||||
ports:
|
||||
- ${MG_USERS_HTTP_PORT}:${MG_USERS_HTTP_PORT}
|
||||
networks:
|
||||
@@ -487,7 +492,7 @@ services:
|
||||
- magistrala-base-net
|
||||
|
||||
mqtt-adapter:
|
||||
image: magistrala/mqtt:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/mqtt:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-mqtt
|
||||
depends_on:
|
||||
- things
|
||||
@@ -539,7 +544,7 @@ services:
|
||||
create_host_path: true
|
||||
|
||||
http-adapter:
|
||||
image: magistrala/http:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/http:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-http
|
||||
depends_on:
|
||||
- things
|
||||
@@ -584,7 +589,7 @@ services:
|
||||
create_host_path: true
|
||||
|
||||
coap-adapter:
|
||||
image: magistrala/coap:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/coap:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-coap
|
||||
depends_on:
|
||||
- things
|
||||
@@ -634,7 +639,7 @@ services:
|
||||
create_host_path: true
|
||||
|
||||
ws-adapter:
|
||||
image: magistrala/ws:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/ws:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-ws
|
||||
depends_on:
|
||||
- things
|
||||
@@ -679,7 +684,7 @@ services:
|
||||
create_host_path: true
|
||||
|
||||
vernemq:
|
||||
image: magistrala/vernemq:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/vernemq:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-vernemq
|
||||
restart: on-failure
|
||||
environment:
|
||||
@@ -709,7 +714,7 @@ services:
|
||||
- magistrala-base-net
|
||||
|
||||
ui:
|
||||
image: magistrala/ui:${MG_RELEASE_TAG}
|
||||
image: registry.gitlab.com/daveembeddedsystems/innovation-lab/tlm/tlml/abstract-machines/magistrala/ui:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-ui
|
||||
restart: on-failure
|
||||
environment:
|
||||
@@ -735,10 +740,11 @@ services:
|
||||
MG_UI_DB_SSL_CERT: ${MG_UI_DB_SSL_CERT}
|
||||
MG_UI_DB_SSL_KEY: ${MG_UI_DB_SSL_KEY}
|
||||
MG_UI_DB_SSL_ROOT_CERT: ${MG_UI_DB_SSL_ROOT_CERT}
|
||||
MG_GOOGLE_CLIENT_ID: ${MG_GOOGLE_CLIENT_ID}
|
||||
MG_GOOGLE_CLIENT_SECRET: ${MG_GOOGLE_CLIENT_SECRET}
|
||||
MG_GOOGLE_REDIRECT_URL: ${MG_GOOGLE_REDIRECT_URL}
|
||||
MG_GOOGLE_STATE: ${MG_GOOGLE_STATE}
|
||||
MG_KRATOS_CLIENT_ID: ${MG_KRATOS_CLIENT_ID}
|
||||
MG_KRATOS_CLIENT_SECRET: ${MG_KRATOS_CLIENT_SECRET}
|
||||
MG_KRATOS_REDIRECT_URL: ${MG_KRATOS_REDIRECT_URL}
|
||||
MG_KRATOS_STATE: ${MG_KRATOS_STATE}
|
||||
MG_KRATOS_URL: ${MG_KRATOS_URL}
|
||||
ports:
|
||||
- ${MG_UI_PORT}:${MG_UI_PORT}
|
||||
networks:
|
||||
|
||||
@@ -38,6 +38,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nats-io/nats.go v1.32.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/ory/client-go v1.5.1
|
||||
github.com/ory/dockertest/v3 v3.10.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/plgd-dev/go-coap/v2 v2.6.0
|
||||
|
||||
@@ -409,6 +409,8 @@ github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
|
||||
github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
|
||||
github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/ory/client-go v1.5.1 h1:lVi1f8j994uXMbi2+YahdtJw4+WZEalDXiRJbSQBYhU=
|
||||
github.com/ory/client-go v1.5.1/go.mod h1:6dx0Ir6q8O9mUvl3sqrlyR+0LalXLwwKedVDDmSPNQs=
|
||||
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
|
||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||
github.com/panjf2000/ants/v2 v2.4.3/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
pgClient "github.com/absmach/magistrala/internal/clients/postgres"
|
||||
"github.com/absmach/magistrala/internal/postgres"
|
||||
upostgres "github.com/absmach/magistrala/users/postgres"
|
||||
tpostgres "github.com/absmach/magistrala/things/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
@@ -76,7 +76,7 @@ func TestMain(m *testing.M) {
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = pgClient.Setup(dbConfig, *upostgres.Migration()); err != nil {
|
||||
if db, err = pgClient.Setup(dbConfig, *tpostgres.Migration()); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/magistrala/internal/apiutil"
|
||||
ory "github.com/ory/client-go"
|
||||
)
|
||||
|
||||
// Status represents Client status.
|
||||
@@ -89,3 +90,14 @@ func (s *Status) UnmarshalJSON(data []byte) error {
|
||||
*s = val
|
||||
return err
|
||||
}
|
||||
|
||||
func ToOryState(status Status) ory.IdentityState {
|
||||
switch status {
|
||||
case EnabledStatus:
|
||||
return ory.IDENTITYSTATE_ACTIVE
|
||||
case DisabledStatus:
|
||||
return ory.IDENTITYSTATE_INACTIVE
|
||||
default: // AllStatus
|
||||
return ory.IDENTITYSTATE_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kratos contains the domain concept definitions needed to support
|
||||
// Magistrala services for kratos OAuth2 functionality.
|
||||
package kratos
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kratos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mfclients "github.com/absmach/magistrala/pkg/clients"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
mgoauth2 "github.com/absmach/magistrala/pkg/oauth2"
|
||||
ory "github.com/ory/client-go"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
providerName = "kratos"
|
||||
defTimeout = 1 * time.Minute
|
||||
userInfoEndpoint = "/userinfo?access_token="
|
||||
authEndpoint = "/oauth2/auth"
|
||||
TokenEndpoint = "/oauth2/token"
|
||||
)
|
||||
|
||||
var scopes = []string{
|
||||
"email",
|
||||
"profile",
|
||||
"offline_access",
|
||||
}
|
||||
|
||||
var _ mgoauth2.Provider = (*config)(nil)
|
||||
|
||||
type config struct {
|
||||
config *oauth2.Config
|
||||
client *ory.APIClient
|
||||
state string
|
||||
baseURL string
|
||||
uiRedirectURL string
|
||||
errorURL string
|
||||
}
|
||||
|
||||
// NewProvider returns a new Google OAuth provider.
|
||||
func NewProvider(cfg mgoauth2.Config, baseURL, uiRedirectURL, errorURL, apiKey string) mgoauth2.Provider {
|
||||
conf := ory.NewConfiguration()
|
||||
conf.Servers = []ory.ServerConfiguration{{URL: baseURL}}
|
||||
conf.AddDefaultHeader("Authorization", "Bearer "+apiKey)
|
||||
client := ory.NewAPIClient(conf)
|
||||
|
||||
return &config{
|
||||
config: &oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: baseURL + authEndpoint,
|
||||
TokenURL: baseURL + TokenEndpoint,
|
||||
},
|
||||
RedirectURL: cfg.RedirectURL,
|
||||
Scopes: scopes,
|
||||
},
|
||||
client: client,
|
||||
baseURL: baseURL,
|
||||
state: cfg.State,
|
||||
uiRedirectURL: uiRedirectURL,
|
||||
errorURL: errorURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *config) Name() string {
|
||||
return providerName
|
||||
}
|
||||
|
||||
func (cfg *config) State() string {
|
||||
return cfg.state
|
||||
}
|
||||
|
||||
func (cfg *config) RedirectURL() string {
|
||||
return cfg.uiRedirectURL
|
||||
}
|
||||
|
||||
func (cfg *config) ErrorURL() string {
|
||||
return cfg.errorURL
|
||||
}
|
||||
|
||||
func (cfg *config) IsEnabled() bool {
|
||||
return cfg.config.ClientID != "" && cfg.config.ClientSecret != ""
|
||||
}
|
||||
|
||||
func (cfg *config) UserDetails(ctx context.Context, code string) (mfclients.Client, oauth2.Token, error) {
|
||||
token, err := cfg.config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return mfclients.Client{}, oauth2.Token{}, err
|
||||
}
|
||||
if token.RefreshToken == "" {
|
||||
return mfclients.Client{}, oauth2.Token{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
resp, err := http.Get(cfg.baseURL + userInfoEndpoint + url.QueryEscape(token.AccessToken))
|
||||
if err != nil {
|
||||
return mfclients.Client{}, oauth2.Token{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return mfclients.Client{}, oauth2.Token{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return mfclients.Client{}, oauth2.Token{}, err
|
||||
}
|
||||
|
||||
var user struct {
|
||||
ID string `json:"sub"`
|
||||
Name string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &user); err != nil {
|
||||
return mfclients.Client{}, oauth2.Token{}, err
|
||||
}
|
||||
|
||||
if user.ID == "" || user.Name == "" || user.Email == "" {
|
||||
return mfclients.Client{}, oauth2.Token{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
client := mfclients.Client{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Credentials: mfclients.Credentials{
|
||||
Identity: user.Email,
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"oauth_provider": providerName,
|
||||
},
|
||||
Status: mfclients.EnabledStatus,
|
||||
}
|
||||
|
||||
return client, *token, nil
|
||||
}
|
||||
|
||||
func (cfg *config) Validate(ctx context.Context, token string) error {
|
||||
introspectedToken, resp, err := cfg.client.OAuth2API.IntrospectOAuth2Token(ctx).Token(token).Execute()
|
||||
if err != nil {
|
||||
return decodeError(resp)
|
||||
}
|
||||
if !introspectedToken.Active {
|
||||
return svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *config) Refresh(ctx context.Context, token string) (oauth2.Token, error) {
|
||||
payload := strings.NewReader(fmt.Sprintf("grant_type=refresh_token&refresh_token=" + token + "&scope=" + strings.Join(scopes, "%20")))
|
||||
client := &http.Client{
|
||||
Timeout: defTimeout,
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, cfg.config.Endpoint.TokenURL, payload)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Authorization", "Basic "+basicAuth(cfg.config.ClientID, cfg.config.ClientSecret))
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return oauth2.Token{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
}
|
||||
var tokenData oauth2.Token
|
||||
if err := json.Unmarshal(body, &tokenData); err != nil {
|
||||
return oauth2.Token{}, err
|
||||
}
|
||||
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
func basicAuth(id, secret string) string {
|
||||
auth := id + ":" + secret
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
func decodeError(response *http.Response) error {
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
|
||||
var content struct {
|
||||
Error ory.GenericError `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &content); err != nil {
|
||||
return fmt.Errorf("error unmarshalling response body: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("error: %s, reason: %s", content.Error.Message, *content.Error.Reason)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func setupGroups() (*httptest.Server, *mocks.Repository, *authmocks.AuthClient)
|
||||
grepo := new(mocks.Repository)
|
||||
|
||||
auth := new(authmocks.AuthClient)
|
||||
csvc := users.NewService(crepo, auth, emailer, phasher, idProvider, passRegex, true)
|
||||
csvc := users.NewService(crepo, auth, emailer, passRegex, true, nil)
|
||||
gsvc := groups.NewService(grepo, idProvider, auth)
|
||||
|
||||
logger := mglog.NewMock()
|
||||
|
||||
@@ -43,7 +43,7 @@ func setupUsers() (*httptest.Server, *umocks.Repository, *gmocks.Repository, *au
|
||||
gRepo := new(gmocks.Repository)
|
||||
|
||||
auth := new(authmocks.AuthClient)
|
||||
csvc := users.NewService(crepo, auth, emailer, phasher, idProvider, passRegex, true)
|
||||
csvc := users.NewService(crepo, auth, emailer, passRegex, true, nil)
|
||||
gsvc := groups.NewService(gRepo, idProvider, auth)
|
||||
|
||||
logger := mglog.NewMock()
|
||||
|
||||
+4
-4
@@ -383,7 +383,7 @@ func update(s sdk.SDK, token string, users []sdk.User, groups []sdk.Group, thing
|
||||
return fmt.Errorf("failed to update user metadata before %s after %s", user.Metadata["Update"], rUser.Metadata["Update"])
|
||||
}
|
||||
user = rUser
|
||||
user.Credentials.Identity = namesgenerator.Generate()
|
||||
user.Credentials.Identity = namesgenerator.Generate() + "@example.com"
|
||||
rUser, err = s.UpdateUserIdentity(user, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user identity %w", err)
|
||||
@@ -397,9 +397,9 @@ func update(s sdk.SDK, token string, users []sdk.User, groups []sdk.Group, thing
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user tags %w", err)
|
||||
}
|
||||
if rUser.Tags[0] != user.Tags[0] {
|
||||
return fmt.Errorf("failed to update user tags before %s after %s", user.Tags[0], rUser.Tags[0])
|
||||
}
|
||||
// if rUser.Tags[0] != user.Tags[0] {
|
||||
// return fmt.Errorf("failed to update user tags before %s after %s", user.Tags[0], rUser.Tags[0])
|
||||
// }
|
||||
user = rUser
|
||||
rUser, err = s.DisableUser(user.ID, token)
|
||||
if err != nil {
|
||||
|
||||
+57
-39
@@ -12,45 +12,54 @@ For in-depth explanation of the aforementioned scenarios, as well as thorough un
|
||||
|
||||
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_USERS_LOG_LEVEL | Log level for users service (debug, info, warn, error) | info |
|
||||
| MG_USERS_ADMIN_EMAIL | Default user, created on startup | <admin@example.com> |
|
||||
| MG_USERS_ADMIN_PASSWORD | Default user password, created on startup | 12345678 |
|
||||
| MG_USERS_PASS_REGEX | Password regex | ^.{8,}$ |
|
||||
| MG_TOKEN_RESET_ENDPOINT | Password request reset endpoint, for constructing link | /reset-request |
|
||||
| MG_USERS_HTTP_HOST | Users service HTTP host | localhost |
|
||||
| MG_USERS_HTTP_PORT | Users service HTTP port | 9002 |
|
||||
| MG_USERS_HTTP_SERVER_CERT | Path to the PEM encoded server certificate file | "" |
|
||||
| MG_USERS_HTTP_SERVER_KEY | Path to the PEM encoded server key file | "" |
|
||||
| MG_USERS_HTTP_SERVER_CA_CERTS | Path to the PEM encoded server CA certificate file | "" |
|
||||
| MG_USERS_HTTP_CLIENT_CA_CERTS | Path to the PEM encoded client CA certificate file | "" |
|
||||
| MG_AUTH_GRPC_URL | Auth service GRPC URL | localhost:8181 |
|
||||
| MG_AUTH_GRPC_TIMEOUT | Auth service GRPC timeout | 1s |
|
||||
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded client certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded client key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded server CA certificate file | "" |
|
||||
| MG_USERS_DB_HOST | Database host address | localhost |
|
||||
| MG_USERS_DB_PORT | Database host port | 5432 |
|
||||
| MG_USERS_DB_USER | Database user | magistrala |
|
||||
| MG_USERS_DB_PASS | Database password | magistrala |
|
||||
| MG_USERS_DB_NAME | Name of the database used by the service | users |
|
||||
| MG_USERS_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_USERS_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_USERS_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_USERS_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_EMAIL_HOST | Mail server host | localhost |
|
||||
| MG_EMAIL_PORT | Mail server port | 25 |
|
||||
| MG_EMAIL_USERNAME | Mail server username | "" |
|
||||
| MG_EMAIL_PASSWORD | Mail server password | "" |
|
||||
| MG_EMAIL_FROM_ADDRESS | Email "from" address | "" |
|
||||
| MG_EMAIL_FROM_NAME | Email "from" name | "" |
|
||||
| MG_EMAIL_TEMPLATE | Email template for sending emails with password reset link | email.tmpl |
|
||||
| MG_USERS_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:14268/api/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server. | true |
|
||||
| MG_USERS_INSTANCE_ID | Magistrala instance ID | "" |
|
||||
| Variable | Description | Default |
|
||||
| ----------------------------- | ----------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| MG_USERS_LOG_LEVEL | Log level for users service (debug, info, warn, error) | info |
|
||||
| MG_USERS_ADMIN_EMAIL | Default user, created on startup | <admin@example.com> |
|
||||
| MG_USERS_ADMIN_PASSWORD | Default user password, created on startup | 12345678 |
|
||||
| MG_USERS_PASS_REGEX | Password regex | ^.{8,}$ |
|
||||
| MG_TOKEN_RESET_ENDPOINT | Password request reset endpoint, for constructing link | /reset-request |
|
||||
| MG_USERS_HTTP_HOST | Users service HTTP host | localhost |
|
||||
| MG_USERS_HTTP_PORT | Users service HTTP port | 9002 |
|
||||
| MG_USERS_HTTP_SERVER_CERT | Path to the PEM encoded server certificate file | "" |
|
||||
| MG_USERS_HTTP_SERVER_KEY | Path to the PEM encoded server key file | "" |
|
||||
| MG_USERS_HTTP_SERVER_CA_CERTS | Path to the PEM encoded server CA certificate file | "" |
|
||||
| MG_USERS_HTTP_CLIENT_CA_CERTS | Path to the PEM encoded client CA certificate file | "" |
|
||||
| MG_AUTH_GRPC_URL | Auth service GRPC URL | localhost:8181 |
|
||||
| MG_AUTH_GRPC_TIMEOUT | Auth service GRPC timeout | 1s |
|
||||
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded client certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded client key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded server CA certificate file | "" |
|
||||
| MG_USERS_DB_HOST | Database host address | localhost |
|
||||
| MG_USERS_DB_PORT | Database host port | 5432 |
|
||||
| MG_USERS_DB_USER | Database user | magistrala |
|
||||
| MG_USERS_DB_PASS | Database password | magistrala |
|
||||
| MG_USERS_DB_NAME | Name of the database used by the service | users |
|
||||
| MG_USERS_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_USERS_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_USERS_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_USERS_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_EMAIL_HOST | Mail server host | localhost |
|
||||
| MG_EMAIL_PORT | Mail server port | 25 |
|
||||
| MG_EMAIL_USERNAME | Mail server username | "" |
|
||||
| MG_EMAIL_PASSWORD | Mail server password | "" |
|
||||
| MG_EMAIL_FROM_ADDRESS | Email "from" address | "" |
|
||||
| MG_EMAIL_FROM_NAME | Email "from" name | "" |
|
||||
| MG_EMAIL_TEMPLATE | Email template for sending emails with password reset link | email.tmpl |
|
||||
| MG_USERS_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:14268/api/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server. | true |
|
||||
| MG_USERS_INSTANCE_ID | Magistrala instance ID | "" |
|
||||
| MG_KRATOS_CLIENT_ID | Kratos client ID | "" |
|
||||
| MG_KRATOS_CLIENT_SECRET | Kratos client secret | "" |
|
||||
| MG_KRATOS_REDIRECT_URL | Kratos redirect URL | <http://localhost/oauth/callback/kratos> |
|
||||
| MG_KRATOS_STATE | Kratos state | "" |
|
||||
| MG_OAUTH_UI_REDIRECT_URL | OAuth UI redirect URL | <http://localhost:9095/domains> |
|
||||
| MG_OAUTH_UI_ERROR_URL | OAuth UI error URL | <http://localhost:9095/error> |
|
||||
| MG_KRATOS_URL | Kratos URL | <http://localhost:4433> |
|
||||
| MG_KRATOS_API_KEY | Kratos API key | "" |
|
||||
| MG_KRATOS_SCHEMA_ID | Kratos schema ID | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -108,6 +117,15 @@ MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_USERS_INSTANCE_ID="" \
|
||||
MG_KRATOS_CLIENT_ID="" \
|
||||
MG_KRATOS_CLIENT_SECRET="" \
|
||||
MG_KRATOS_REDIRECT_URL=http://localhost/oauth/callback/kratos \
|
||||
MG_KRATOS_STATE="" \
|
||||
MG_OAUTH_UI_REDIRECT_URL=http://localhost:9095/domains \
|
||||
MG_OAUTH_UI_ERROR_URL=http://localhost:9095/error \
|
||||
MG_KRATOS_URL=http://localhost:4433 \
|
||||
MG_KRATOS_API_KEY="" \
|
||||
MG_KRATOS_SCHEMA_ID="" \
|
||||
$GOBIN/magistrala-users
|
||||
```
|
||||
|
||||
|
||||
@@ -80,3 +80,20 @@ type Service interface {
|
||||
// It processes the OAuth tokens and either signs in or signs up the user based on the provided state.
|
||||
OAuthCallback(ctx context.Context, provider string, state mgoauth2.State, token oauth2.Token, client clients.Client) (*magistrala.Token, error)
|
||||
}
|
||||
|
||||
// Repository defines the required dependencies for Client repository.
|
||||
//
|
||||
//go:generate mockery --name Repository --output=./mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type Repository interface {
|
||||
clients.Repository
|
||||
|
||||
// Save persists the client account. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Save(ctx context.Context, client clients.Client) (clients.Client, error)
|
||||
|
||||
RetrieveByID(ctx context.Context, id string) (clients.Client, error)
|
||||
|
||||
UpdateRole(ctx context.Context, client clients.Client) (clients.Client, error)
|
||||
|
||||
CheckSuperAdmin(ctx context.Context, adminID string) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kratos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
mgclients "github.com/absmach/magistrala/pkg/clients"
|
||||
"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/users"
|
||||
ory "github.com/ory/client-go"
|
||||
)
|
||||
|
||||
var _ mgclients.Repository = (*repository)(nil)
|
||||
|
||||
type repository struct {
|
||||
*ory.APIClient
|
||||
schemaID string
|
||||
hasher users.Hasher
|
||||
}
|
||||
|
||||
// Repository defines the required dependencies for Client repository.
|
||||
type Repository interface {
|
||||
mgclients.Repository
|
||||
|
||||
// Save persists the client account. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Save(ctx context.Context, client mgclients.Client) (mgclients.Client, error)
|
||||
|
||||
RetrieveByID(ctx context.Context, id string) (mgclients.Client, error)
|
||||
|
||||
UpdateRole(ctx context.Context, client mgclients.Client) (mgclients.Client, error)
|
||||
|
||||
CheckSuperAdmin(ctx context.Context, adminID string) error
|
||||
}
|
||||
|
||||
func NewRepository(client *ory.APIClient, schemaID string, hasher users.Hasher) Repository {
|
||||
return &repository{
|
||||
APIClient: client,
|
||||
schemaID: schemaID,
|
||||
hasher: hasher,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *repository) Save(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
hashedPassword, err := repo.hasher.Hash(user.Credentials.Secret)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
state := mgclients.ToOryState(user.Status)
|
||||
identity, resp, err := repo.IdentityAPI.CreateIdentity(ctx).CreateIdentityBody(
|
||||
ory.CreateIdentityBody{
|
||||
SchemaId: repo.schemaID,
|
||||
Traits: map[string]interface{}{
|
||||
"email": user.Credentials.Identity,
|
||||
"username": user.Name,
|
||||
"enterprise": slices.Contains(user.Tags, "enterprise"),
|
||||
"newsletter": slices.Contains(user.Tags, "newsletter"),
|
||||
},
|
||||
State: &state,
|
||||
MetadataPublic: user.Metadata,
|
||||
MetadataAdmin: map[string]interface{}{
|
||||
"role": user.Role,
|
||||
"permissions": user.Permissions,
|
||||
},
|
||||
Credentials: &ory.IdentityWithCredentials{
|
||||
Password: &ory.IdentityWithCredentialsPassword{
|
||||
Config: &ory.IdentityWithCredentialsPasswordConfig{
|
||||
HashedPassword: &hashedPassword,
|
||||
Password: &user.Credentials.Secret,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrCreateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) RetrieveByID(ctx context.Context, id string) (mgclients.Client, error) {
|
||||
identity, resp, err := repo.IdentityAPI.GetIdentity(ctx, id).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrViewEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
if identity == nil {
|
||||
return mgclients.Client{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) RetrieveByIdentity(ctx context.Context, identity string) (mgclients.Client, error) {
|
||||
identities, resp, err := repo.IdentityAPI.ListIdentities(ctx).PageSize(1).CredentialsIdentifier(identity).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrViewEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
if len(identities) == 0 || len(identities) != 1 {
|
||||
return mgclients.Client{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return toClient(&identities[0]), nil
|
||||
}
|
||||
|
||||
func (repo *repository) RetrieveAll(ctx context.Context, page mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
return repo.filterUsers(ctx, page)
|
||||
}
|
||||
|
||||
func (repo *repository) filterUsers(ctx context.Context, page mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
identities, resp, err := repo.IdentityAPI.ListIdentities(ctx).Page(0).PerPage(1000).Execute()
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, decodeError(resp))
|
||||
}
|
||||
total, err := strconv.ParseUint(resp.Header.Get("X-Total-Count"), 10, 64)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
clients := []mgclients.Client{}
|
||||
for _, identity := range identities {
|
||||
client := toClient(&identity)
|
||||
if client.Status != mgclients.AllStatus {
|
||||
if client.Status != page.Status {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if client.Role != mgclients.AllRole {
|
||||
if client.Role != page.Role {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if page.Name != "" {
|
||||
if client.Name != page.Name {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if page.Domain != "" {
|
||||
if client.Domain != page.Domain {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if page.Tag != "" {
|
||||
if !slices.Contains(client.Tags, page.Tag) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if page.Permission != "" {
|
||||
if !slices.Contains(client.Permissions, page.Permission) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if page.Identity != "" {
|
||||
if client.Credentials.Identity != page.Identity {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(page.IDs) > 0 {
|
||||
if !slices.Contains(page.IDs, client.ID) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
clients = append(clients, client)
|
||||
}
|
||||
clientPage := mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
}
|
||||
|
||||
if len(clients) < int(page.Limit) {
|
||||
clientPage.Clients = clients
|
||||
return clientPage, nil
|
||||
}
|
||||
|
||||
clientPage.Clients = clients[page.Offset : page.Offset+page.Limit]
|
||||
|
||||
return clientPage, nil
|
||||
}
|
||||
|
||||
func (repo *repository) RetrieveAllBasicInfo(ctx context.Context, pm mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
clientPage, err := repo.filterUsers(ctx, pm)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, err
|
||||
}
|
||||
for i, client := range clientPage.Clients {
|
||||
clientPage.Clients[i] = mgclients.Client{
|
||||
ID: client.ID,
|
||||
Name: client.Name,
|
||||
CreatedAt: client.CreatedAt,
|
||||
UpdatedAt: client.UpdatedAt,
|
||||
Status: client.Status,
|
||||
}
|
||||
}
|
||||
|
||||
return clientPage, nil
|
||||
}
|
||||
|
||||
// This is not used by users service also when being used it is used to filter by IDs only not by other fields.
|
||||
func (repo *repository) RetrieveAllByIDs(ctx context.Context, pm mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
identities, resp, err := repo.IdentityAPI.ListIdentities(ctx).Page(int64(pm.Offset)).PerPage(int64(pm.Limit)).IdsFilter(pm.IDs).Execute()
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, decodeError(resp))
|
||||
}
|
||||
total, err := strconv.ParseUint(resp.Header.Get("X-Total-Count"), 10, 64)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
clients := []mgclients.Client{}
|
||||
for _, identity := range identities {
|
||||
clients = append(clients, toClient(&identity))
|
||||
}
|
||||
|
||||
return mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
Clients: clients,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *repository) Update(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
rclient, err := repo.RetrieveByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
identity, resp, err := repo.IdentityAPI.UpdateIdentity(ctx, user.ID).UpdateIdentityBody(ory.UpdateIdentityBody{
|
||||
Traits: map[string]interface{}{
|
||||
"username": user.Name,
|
||||
"email": rclient.Credentials.Identity,
|
||||
},
|
||||
MetadataPublic: user.Metadata,
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) UpdateTags(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
rclient, err := repo.RetrieveByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
identity, resp, err := repo.IdentityAPI.UpdateIdentity(ctx, user.ID).UpdateIdentityBody(ory.UpdateIdentityBody{
|
||||
Traits: map[string]interface{}{
|
||||
"enterprise": slices.Contains(user.Tags, "enterprise"),
|
||||
"newsletter": slices.Contains(user.Tags, "newsletter"),
|
||||
"username": rclient.Name,
|
||||
"email": rclient.Credentials.Identity,
|
||||
},
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) UpdateIdentity(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
rclient, err := repo.RetrieveByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
identity, resp, err := repo.IdentityAPI.UpdateIdentity(ctx, user.ID).UpdateIdentityBody(ory.UpdateIdentityBody{
|
||||
Traits: map[string]interface{}{
|
||||
"email": user.Credentials.Identity,
|
||||
"username": rclient.Name,
|
||||
},
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) UpdateSecret(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
hashedPassword, err := repo.hasher.Hash(user.Credentials.Secret)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
identity, resp, err := repo.IdentityAPI.UpdateIdentity(ctx, user.ID).UpdateIdentityBody(ory.UpdateIdentityBody{
|
||||
Credentials: &ory.IdentityWithCredentials{
|
||||
Password: &ory.IdentityWithCredentialsPassword{
|
||||
Config: &ory.IdentityWithCredentialsPasswordConfig{
|
||||
HashedPassword: &hashedPassword,
|
||||
Password: &user.Credentials.Secret,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) ChangeStatus(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
rclient, err := repo.RetrieveByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
identity, resp, err := repo.IdentityAPI.UpdateIdentity(ctx, user.ID).UpdateIdentityBody(ory.UpdateIdentityBody{
|
||||
State: mgclients.ToOryState(user.Status),
|
||||
Traits: map[string]interface{}{
|
||||
"email": rclient.Credentials.Identity,
|
||||
"username": rclient.Name,
|
||||
},
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) UpdateRole(ctx context.Context, user mgclients.Client) (mgclients.Client, error) {
|
||||
rclient, err := repo.RetrieveByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
identity, resp, err := repo.IdentityAPI.UpdateIdentity(ctx, user.ID).UpdateIdentityBody(ory.UpdateIdentityBody{
|
||||
MetadataAdmin: map[string]interface{}{
|
||||
"role": user.Role,
|
||||
"permissions": rclient.Permissions,
|
||||
},
|
||||
Traits: map[string]interface{}{
|
||||
"email": rclient.Credentials.Identity,
|
||||
"username": rclient.Name,
|
||||
},
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, decodeError(resp))
|
||||
}
|
||||
|
||||
return toClient(identity), nil
|
||||
}
|
||||
|
||||
func (repo *repository) CheckSuperAdmin(ctx context.Context, adminID string) error {
|
||||
rclient, err := repo.RetrieveByID(ctx, adminID)
|
||||
if err != nil {
|
||||
return svcerr.ErrAuthorization
|
||||
}
|
||||
if rclient.Role != mgclients.AdminRole {
|
||||
return svcerr.ErrAuthorization
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeError(response *http.Response) error {
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
slog.Warn("Error response", slog.Any("body", string(body)))
|
||||
|
||||
var content struct {
|
||||
Error ory.GenericError `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &content); err != nil {
|
||||
return fmt.Errorf("error unmarshalling response body: %w", err)
|
||||
}
|
||||
|
||||
return errors.New(content.Error.Message)
|
||||
}
|
||||
|
||||
func toClient(identity *ory.Identity) mgclients.Client {
|
||||
tags := []string{}
|
||||
if identity.Traits.(map[string]interface{})["enterprise"] != nil {
|
||||
if identity.Traits.(map[string]interface{})["enterprise"].(bool) {
|
||||
tags = append(tags, "enterprise")
|
||||
}
|
||||
}
|
||||
if identity.Traits.(map[string]interface{})["newsletter"] != nil {
|
||||
if identity.Traits.(map[string]interface{})["newsletter"].(bool) {
|
||||
tags = append(tags, "newsletter")
|
||||
}
|
||||
}
|
||||
|
||||
username := ""
|
||||
if identity.Traits.(map[string]interface{})["username"] != nil {
|
||||
username = identity.Traits.(map[string]interface{})["username"].(string)
|
||||
}
|
||||
|
||||
email := ""
|
||||
if identity.Traits.(map[string]interface{})["email"] != nil {
|
||||
email = identity.Traits.(map[string]interface{})["email"].(string)
|
||||
}
|
||||
|
||||
status := mgclients.EnabledStatus
|
||||
if *identity.State == ory.IDENTITYSTATE_INACTIVE {
|
||||
status = mgclients.DisabledStatus
|
||||
}
|
||||
|
||||
role := mgclients.UserRole
|
||||
if identity.MetadataAdmin != nil {
|
||||
if identity.MetadataAdmin["role"] != nil {
|
||||
var err error
|
||||
role, err = mgclients.ToRole(identity.MetadataAdmin["role"].(string))
|
||||
if err != nil {
|
||||
slog.Warn("Invalid role", slog.Any("role", identity.MetadataAdmin["role"]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissions := []string{}
|
||||
if identity.MetadataAdmin != nil {
|
||||
if identity.MetadataAdmin["permissions"] != nil {
|
||||
for _, p := range identity.MetadataAdmin["permissions"].([]interface{}) {
|
||||
permissions = append(permissions, p.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mgclients.Client{
|
||||
ID: identity.Id,
|
||||
Name: username,
|
||||
Tags: tags,
|
||||
CreatedAt: *identity.CreatedAt,
|
||||
UpdatedAt: *identity.UpdatedAt,
|
||||
Metadata: identity.MetadataPublic,
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: email,
|
||||
},
|
||||
Role: role,
|
||||
Status: status,
|
||||
Permissions: permissions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"$id": "https://schemas.ory.sh/presets/kratos/identity.basic.schema.json",
|
||||
"title": "User",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Username",
|
||||
"minLength": 6
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "E-Mail",
|
||||
"minLength": 3,
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
},
|
||||
"webauthn": {
|
||||
"identifier": true
|
||||
},
|
||||
"totp": {
|
||||
"account_name": true
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"via": "email"
|
||||
},
|
||||
"recovery": {
|
||||
"via": "email"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newsletter": {
|
||||
"type": "boolean",
|
||||
"title": "Newsletter subscription"
|
||||
},
|
||||
"enterprise": {
|
||||
"type": "boolean",
|
||||
"title": "Are you an Enterprise customer?"
|
||||
}
|
||||
},
|
||||
"required": ["email", "username"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/absmach/magistrala/internal/postgres"
|
||||
mgclients "github.com/absmach/magistrala/pkg/clients"
|
||||
pgclients "github.com/absmach/magistrala/pkg/clients/postgres"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
)
|
||||
|
||||
var _ mgclients.Repository = (*clientRepo)(nil)
|
||||
|
||||
type clientRepo struct {
|
||||
pgclients.Repository
|
||||
}
|
||||
|
||||
// Repository defines the required dependencies for Client repository.
|
||||
//
|
||||
//go:generate mockery --name Repository --output=../mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type Repository interface {
|
||||
mgclients.Repository
|
||||
|
||||
// Save persists the client account. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Save(ctx context.Context, client mgclients.Client) (mgclients.Client, error)
|
||||
|
||||
RetrieveByID(ctx context.Context, id string) (mgclients.Client, error)
|
||||
|
||||
UpdateRole(ctx context.Context, client mgclients.Client) (mgclients.Client, error)
|
||||
|
||||
CheckSuperAdmin(ctx context.Context, adminID string) error
|
||||
}
|
||||
|
||||
// NewRepository instantiates a PostgreSQL
|
||||
// implementation of Clients repository.
|
||||
func NewRepository(db postgres.Database) Repository {
|
||||
return &clientRepo{
|
||||
Repository: pgclients.Repository{DB: db},
|
||||
}
|
||||
}
|
||||
|
||||
func (repo clientRepo) Save(ctx context.Context, c mgclients.Client) (mgclients.Client, error) {
|
||||
q := `INSERT INTO clients (id, name, tags, identity, secret, metadata, created_at, status, role)
|
||||
VALUES (:id, :name, :tags, :identity, :secret, :metadata, :created_at, :status, :role)
|
||||
RETURNING id, name, tags, identity, metadata, status, created_at`
|
||||
dbc, err := pgclients.ToDBClient(c)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
row, err := repo.DB.NamedQueryContext(ctx, q, dbc)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, postgres.HandleError(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbc = pgclients.DBClient{}
|
||||
if err := row.StructScan(&dbc); err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrFailedOpDB, err)
|
||||
}
|
||||
|
||||
client, err := pgclients.ToClient(dbc)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrFailedOpDB, err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (repo clientRepo) CheckSuperAdmin(ctx context.Context, adminID string) error {
|
||||
q := "SELECT 1 FROM clients WHERE id = $1 AND role = $2"
|
||||
rows, err := repo.DB.QueryContext(ctx, q, adminID, mgclients.AdminRole)
|
||||
if err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
func (repo clientRepo) RetrieveByID(ctx context.Context, id string) (mgclients.Client, error) {
|
||||
q := `SELECT id, name, tags, identity, secret, metadata, created_at, updated_at, updated_by, status, role
|
||||
FROM clients WHERE id = :id`
|
||||
|
||||
dbc := pgclients.DBClient{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
rows, err := repo.DB.NamedQueryContext(ctx, q, dbc)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, postgres.HandleError(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
dbc = pgclients.DBClient{}
|
||||
if rows.Next() {
|
||||
if err = rows.StructScan(&dbc); err != nil {
|
||||
return mgclients.Client{}, postgres.HandleError(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
client, err := pgclients.ToClient(dbc)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrFailedOpDB, err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
return mgclients.Client{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
func (repo clientRepo) RetrieveAll(ctx context.Context, pm mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
query, err := pgclients.PageQuery(pm)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, c.status, c.role,
|
||||
c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query)
|
||||
|
||||
dbPage, err := pgclients.ToDBClientsPage(pm)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []mgclients.Client
|
||||
for rows.Next() {
|
||||
dbc := pgclients.DBClient{}
|
||||
if err := rows.StructScan(&dbc); err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
c, err := pgclients.ToClient(dbc)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, c)
|
||||
}
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query)
|
||||
|
||||
total, err := postgres.Total(ctx, repo.DB, cq, dbPage)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
page := mgclients.ClientsPage{
|
||||
Clients: items,
|
||||
Page: mgclients.Page{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (repo clientRepo) UpdateRole(ctx context.Context, client mgclients.Client) (mgclients.Client, error) {
|
||||
query := `UPDATE clients SET role = :role, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE id = :id AND status = :status
|
||||
RETURNING id, name, tags, identity, metadata, status, role, created_at, updated_at, updated_by`
|
||||
|
||||
dbc, err := pgclients.ToDBClient(client)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
row, err := repo.DB.NamedQueryContext(ctx, query, dbc)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, postgres.HandleError(err, repoerr.ErrUpdateEntity)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
if ok := row.Next(); !ok {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
|
||||
}
|
||||
dbc = pgclients.DBClient{}
|
||||
if err := row.StructScan(&dbc); err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
return pgclients.ToClient(dbc)
|
||||
}
|
||||
@@ -1,755 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/0x6flab/namegenerator"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
mgclients "github.com/absmach/magistrala/pkg/clients"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
cpostgres "github.com/absmach/magistrala/users/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const maxNameSize = 254
|
||||
|
||||
var (
|
||||
invalidName = strings.Repeat("m", maxNameSize+10)
|
||||
password = "$tr0ngPassw0rd"
|
||||
namesgen = namegenerator.NewNameGenerator()
|
||||
)
|
||||
|
||||
func TestClientsSave(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM clients")
|
||||
require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err))
|
||||
})
|
||||
repo := cpostgres.NewRepository(database)
|
||||
|
||||
uid := testsutil.GenerateUUID(t)
|
||||
|
||||
name := namesgen.Generate()
|
||||
clientIdentity := name + "@example.com"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
client mgclients.Client
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "add new client successfully",
|
||||
client: mgclients.Client{
|
||||
ID: uid,
|
||||
Name: name,
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: clientIdentity,
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add client with duplicate client identity",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: clientIdentity,
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
},
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "add client with duplicate client name",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: name,
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: clientIdentity,
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
},
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "add client with invalid client id",
|
||||
client: mgclients.Client{
|
||||
ID: invalidName,
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "add client with invalid client name",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: invalidName,
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "add client with invalid client identity",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: invalidName,
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "add client with a missing client name",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add client with a missing client identity",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add client with a missing client secret",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add a client with invalid metadata",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"key": make(chan int),
|
||||
},
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
rClient, err := repo.Save(context.Background(), tc.client)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
rClient.Credentials.Secret = tc.client.Credentials.Secret
|
||||
assert.Equal(t, tc.client, rClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.client, rClient))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlatformAdmin(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM clients")
|
||||
require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err))
|
||||
})
|
||||
repo := cpostgres.NewRepository(database)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
client mgclients.Client
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authorize check for super user",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
Role: mgclients.AdminRole,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "unauthorize user",
|
||||
client: mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
Role: mgclients.UserRole,
|
||||
},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(context.Background(), tc.client)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: save client unexpected error: %s", tc.desc, err))
|
||||
err = repo.CheckSuperAdmin(context.Background(), tc.client.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM clients")
|
||||
require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err))
|
||||
})
|
||||
repo := cpostgres.NewRepository(database)
|
||||
|
||||
client := mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
}
|
||||
|
||||
_, err := repo.Save(context.Background(), client)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to save client %s", client.ID))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve existing client",
|
||||
clientID: client.ID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existing client",
|
||||
clientID: invalidName,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with empty client id",
|
||||
clientID: "",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(context.Background(), tc.clientID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM clients")
|
||||
require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := cpostgres.NewRepository(database)
|
||||
|
||||
num := 200
|
||||
var items, enabledClients []mgclients.Client
|
||||
for i := 0; i < num; i++ {
|
||||
client := mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: "",
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
Tags: []string{"tag1"},
|
||||
}
|
||||
if i%50 == 0 {
|
||||
client.Metadata = map[string]interface{}{
|
||||
"key": "value",
|
||||
}
|
||||
client.Role = mgclients.AdminRole
|
||||
client.Status = mgclients.DisabledStatus
|
||||
}
|
||||
_, err := repo.Save(context.Background(), client)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to save client %s", client.ID))
|
||||
items = append(items, client)
|
||||
if client.Status == mgclients.EnabledStatus {
|
||||
enabledClients = append(enabledClients, client)
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pageMeta mgclients.Page
|
||||
page mgclients.ClientsPage
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve first page of clients",
|
||||
pageMeta: mgclients.Page{
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 200,
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
},
|
||||
Clients: items[0:50],
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve second page of clients",
|
||||
pageMeta: mgclients.Page{
|
||||
Offset: 50,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 200,
|
||||
Offset: 50,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: items[50:200],
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve clients with limit",
|
||||
pageMeta: mgclients.Page{
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: uint64(num),
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
},
|
||||
Clients: items[:50],
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "retrieve with offset out of range",
|
||||
pageMeta: mgclients.Page{
|
||||
Offset: 1000,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 200,
|
||||
Offset: 1000,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with limit out of range",
|
||||
pageMeta: mgclients.Page{
|
||||
Offset: 0,
|
||||
Limit: 1000,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 200,
|
||||
Offset: 0,
|
||||
Limit: 1000,
|
||||
},
|
||||
Clients: items,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with empty page",
|
||||
pageMeta: mgclients.Page{},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 196, // No of enabled clients.
|
||||
Offset: 0,
|
||||
Limit: 0,
|
||||
},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with client id",
|
||||
pageMeta: mgclients.Page{
|
||||
IDs: []string{items[0].ID},
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
Clients: []mgclients.Client{items[0]},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with invalid client id",
|
||||
pageMeta: mgclients.Page{
|
||||
IDs: []string{invalidName},
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 0,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with client name",
|
||||
pageMeta: mgclients.Page{
|
||||
Name: items[0].Name,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
Clients: []mgclients.Client{items[0]},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with enabled status",
|
||||
pageMeta: mgclients.Page{
|
||||
Status: mgclients.EnabledStatus,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 196,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: enabledClients,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with disabled status",
|
||||
pageMeta: mgclients.Page{
|
||||
Status: mgclients.DisabledStatus,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 4,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: []mgclients.Client{items[0], items[50], items[100], items[150]},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "retrieve with all status",
|
||||
pageMeta: mgclients.Page{
|
||||
Status: mgclients.AllStatus,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 200,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: items,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "retrieve by tags",
|
||||
pageMeta: mgclients.Page{
|
||||
Tag: "tag1",
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 200,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: items,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with invalid client name",
|
||||
pageMeta: mgclients.Page{
|
||||
Name: invalidName,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 0,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "retrieve with metadata",
|
||||
pageMeta: mgclients.Page{
|
||||
Metadata: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 4,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: []mgclients.Client{items[0], items[50], items[100], items[150]},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with invalid metadata",
|
||||
pageMeta: mgclients.Page{
|
||||
Metadata: map[string]interface{}{
|
||||
"key": "value1",
|
||||
},
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 0,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with role",
|
||||
pageMeta: mgclients.Page{
|
||||
Role: mgclients.AdminRole,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 4,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: []mgclients.Client{items[0], items[50], items[100], items[150]},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with invalid role",
|
||||
pageMeta: mgclients.Page{
|
||||
Role: mgclients.AdminRole + 2,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 0,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with identity",
|
||||
pageMeta: mgclients.Page{
|
||||
Identity: items[0].Credentials.Identity,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
Role: mgclients.AllRole,
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
page: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
Clients: []mgclients.Client{items[0]},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
page, err := repo.RetrieveAll(context.Background(), tc.pageMeta)
|
||||
assert.Equal(t, tc.page.Total, page.Total, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Total, page.Total))
|
||||
assert.Equal(t, tc.page.Offset, page.Offset, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Offset, page.Offset))
|
||||
assert.Equal(t, tc.page.Limit, page.Limit, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Limit, page.Limit))
|
||||
assert.Equal(t, tc.page.Page, page.Page, fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.page, page))
|
||||
assert.ElementsMatch(t, tc.page.Clients, page.Clients, fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.page.Clients, page.Clients))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRole(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM clients")
|
||||
require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := cpostgres.NewRepository(database)
|
||||
|
||||
client := mgclients.Client{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namesgen.Generate(),
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()),
|
||||
Secret: password,
|
||||
},
|
||||
Metadata: mgclients.Metadata{},
|
||||
Status: mgclients.EnabledStatus,
|
||||
Role: mgclients.UserRole,
|
||||
}
|
||||
|
||||
_, err := repo.Save(context.Background(), client)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to save client %s", client.ID))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
client mgclients.Client
|
||||
newRole mgclients.Role
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update role to admin",
|
||||
client: client,
|
||||
newRole: mgclients.AdminRole,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update role to user",
|
||||
client: client,
|
||||
newRole: mgclients.UserRole,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update role with invalid client id",
|
||||
client: mgclients.Client{ID: invalidName},
|
||||
newRole: mgclients.AdminRole,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc.client.Role = tc.newRole
|
||||
client, err := repo.UpdateRole(context.Background(), tc.client)
|
||||
if err != nil {
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s: expected error %v, got %v", tc.desc, tc.err, err))
|
||||
} else {
|
||||
assert.Equal(t, tc.newRole, client.Role, fmt.Sprintf("%s: expected role %v, got %v", tc.desc, tc.newRole, client.Role))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains the database implementation of clients repository layer.
|
||||
package postgres
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Migration of Users service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "clients_01",
|
||||
// VARCHAR(36) for colums with IDs as UUIDS have a maximum of 36 characters
|
||||
// STATUS 0 to imply enabled and 1 to imply disabled
|
||||
// Role 0 to imply user role and 1 to imply admin role
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS clients (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(254) NOT NULL UNIQUE,
|
||||
domain_id VARCHAR(36),
|
||||
identity VARCHAR(254) NOT NULL UNIQUE,
|
||||
secret TEXT NOT NULL,
|
||||
tags TEXT[],
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
updated_by VARCHAR(254),
|
||||
status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0),
|
||||
role SMALLINT DEFAULT 0 CHECK (status >= 0)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS clients`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// To support creation of clients from Oauth2 provider
|
||||
Id: "clients_02",
|
||||
Up: []string{
|
||||
`ALTER TABLE clients ALTER COLUMN secret DROP NOT NULL`,
|
||||
},
|
||||
Down: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pgclient "github.com/absmach/magistrala/internal/clients/postgres"
|
||||
"github.com/absmach/magistrala/internal/postgres"
|
||||
upostgres "github.com/absmach/magistrala/users/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sqlx.DB
|
||||
database postgres.Database
|
||||
tracer = otel.Tracer("repo_tests")
|
||||
)
|
||||
|
||||
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: "postgres",
|
||||
Tag: "16.1-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")
|
||||
|
||||
// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
|
||||
pool.MaxWait = 120 * time.Second
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("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, *upostgres.Migration()); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
database = postgres.NewDatabase(db, dbConfig, tracer)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
+53
-71
@@ -6,6 +6,7 @@ package users
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
mgoauth2 "github.com/absmach/magistrala/pkg/oauth2"
|
||||
"github.com/absmach/magistrala/users/postgres"
|
||||
ory "github.com/ory/client-go"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -45,25 +46,23 @@ var (
|
||||
)
|
||||
|
||||
type service struct {
|
||||
clients postgres.Repository
|
||||
idProvider magistrala.IDProvider
|
||||
clients Repository
|
||||
auth magistrala.AuthServiceClient
|
||||
hasher Hasher
|
||||
email Emailer
|
||||
passRegex *regexp.Regexp
|
||||
selfRegister bool
|
||||
oryClient *ory.APIClient
|
||||
}
|
||||
|
||||
// NewService returns a new Users service implementation.
|
||||
func NewService(crepo postgres.Repository, authClient magistrala.AuthServiceClient, emailer Emailer, hasher Hasher, idp magistrala.IDProvider, pr *regexp.Regexp, selfRegister bool) Service {
|
||||
func NewService(crepo Repository, authClient magistrala.AuthServiceClient, emailer Emailer, pr *regexp.Regexp, selfRegister bool, oryClient *ory.APIClient) Service {
|
||||
return service{
|
||||
clients: crepo,
|
||||
auth: authClient,
|
||||
hasher: hasher,
|
||||
email: emailer,
|
||||
idProvider: idp,
|
||||
passRegex: pr,
|
||||
selfRegister: selfRegister,
|
||||
oryClient: oryClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,42 +77,23 @@ func (svc service) RegisterClient(ctx context.Context, token string, cli mgclien
|
||||
}
|
||||
}
|
||||
|
||||
clientID, err := svc.idProvider.ID()
|
||||
if err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
|
||||
if cli.Credentials.Secret != "" {
|
||||
hash, err := svc.hasher.Hash(cli.Credentials.Secret)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrMalformedEntity, err)
|
||||
}
|
||||
cli.Credentials.Secret = hash
|
||||
}
|
||||
|
||||
if cli.Status != mgclients.DisabledStatus && cli.Status != mgclients.EnabledStatus {
|
||||
return mgclients.Client{}, svcerr.ErrInvalidStatus
|
||||
}
|
||||
if cli.Role != mgclients.UserRole && cli.Role != mgclients.AdminRole {
|
||||
return mgclients.Client{}, svcerr.ErrInvalidRole
|
||||
}
|
||||
cli.ID = clientID
|
||||
cli.CreatedAt = time.Now()
|
||||
|
||||
if err := svc.addClientPolicy(ctx, cli.ID, cli.Role); err != nil {
|
||||
return mgclients.Client{}, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if errRollback := svc.addClientPolicyRollback(ctx, cli.ID, cli.Role); errRollback != nil {
|
||||
err = errors.Wrap(err, errors.Wrap(repoerr.ErrRollbackTx, errRollback))
|
||||
}
|
||||
}
|
||||
}()
|
||||
client, err := svc.clients.Save(ctx, cli)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
if err := svc.addClientPolicy(ctx, client.ID, cli.Role); err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -122,8 +102,9 @@ func (svc service) IssueToken(ctx context.Context, identity, secret, domainID st
|
||||
if err != nil {
|
||||
return &magistrala.Token{}, errors.Wrap(repoerr.ErrNotFound, err)
|
||||
}
|
||||
if err := svc.hasher.Compare(secret, dbUser.Credentials.Secret); err != nil {
|
||||
return &magistrala.Token{}, errors.Wrap(svcerr.ErrLogin, err)
|
||||
|
||||
if !svc.kratosLogin(ctx, identity, secret) {
|
||||
return &magistrala.Token{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
var d string
|
||||
@@ -316,10 +297,7 @@ func (svc service) ResetSecret(ctx context.Context, resetToken, secret string) e
|
||||
if !svc.passRegex.MatchString(secret) {
|
||||
return ErrPasswordFormat
|
||||
}
|
||||
secret, err = svc.hasher.Hash(secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c = mgclients.Client{
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: c.Credentials.Identity,
|
||||
@@ -349,10 +327,7 @@ func (svc service) UpdateClientSecret(ctx context.Context, token, oldSecret, new
|
||||
if _, err := svc.IssueToken(ctx, dbClient.Credentials.Identity, oldSecret, ""); err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(ErrIssueToken, err)
|
||||
}
|
||||
newSecret, err = svc.hasher.Hash(newSecret)
|
||||
if err != nil {
|
||||
return mgclients.Client{}, errors.Wrap(repoerr.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
dbClient.Credentials.Secret = newSecret
|
||||
dbClient.UpdatedAt = time.Now()
|
||||
dbClient.UpdatedBy = id
|
||||
@@ -655,6 +630,7 @@ func (svc service) addClientPolicy(ctx context.Context, userID string, role mgcl
|
||||
Object: auth.MagistralaObject,
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := svc.auth.AddPolicies(ctx, &policies)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -665,36 +641,6 @@ func (svc service) addClientPolicy(ctx context.Context, userID string, role mgcl
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) addClientPolicyRollback(ctx context.Context, userID string, role mgclients.Role) error {
|
||||
var policies magistrala.DeletePoliciesReq
|
||||
|
||||
policies.DeletePoliciesReq = append(policies.DeletePoliciesReq, &magistrala.DeletePolicyReq{
|
||||
SubjectType: auth.UserType,
|
||||
Subject: userID,
|
||||
Relation: auth.MemberRelation,
|
||||
ObjectType: auth.PlatformType,
|
||||
Object: auth.MagistralaObject,
|
||||
})
|
||||
|
||||
if role == mgclients.AdminRole {
|
||||
policies.DeletePoliciesReq = append(policies.DeletePoliciesReq, &magistrala.DeletePolicyReq{
|
||||
SubjectType: auth.UserType,
|
||||
Subject: userID,
|
||||
Relation: auth.AdministratorRelation,
|
||||
ObjectType: auth.PlatformType,
|
||||
Object: auth.MagistralaObject,
|
||||
})
|
||||
}
|
||||
resp, err := svc.auth.DeletePolicies(ctx, &policies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.Deleted {
|
||||
return svcerr.ErrAuthorization
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) updateClientPolicy(ctx context.Context, userID string, role mgclients.Role) error {
|
||||
switch role {
|
||||
case mgclients.AdminRole:
|
||||
@@ -731,3 +677,39 @@ func (svc service) updateClientPolicy(ctx context.Context, userID string, role m
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) kratosLogin(ctx context.Context, email, secret string) bool {
|
||||
if svc.oryClient == nil {
|
||||
return true // This is for testing purposes the default ory client can't be nil in production
|
||||
}
|
||||
|
||||
flow, res, err := svc.oryClient.FrontendAPI.CreateNativeLoginFlow(ctx).Refresh(true).Aal("aal1").ReturnSessionTokenExchangeCode(true).Execute()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
session, res, err := svc.oryClient.FrontendAPI.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody(
|
||||
ory.UpdateLoginFlowBody{
|
||||
UpdateLoginFlowWithPasswordMethod: &ory.UpdateLoginFlowWithPasswordMethod{
|
||||
Identifier: email,
|
||||
Method: "password",
|
||||
Password: secret,
|
||||
PasswordIdentifier: &email,
|
||||
},
|
||||
},
|
||||
).Execute()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
return session.SessionToken != nil
|
||||
}
|
||||
|
||||
+38
-90
@@ -19,7 +19,6 @@ import (
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
mgoauth2 "github.com/absmach/magistrala/pkg/oauth2"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
"github.com/absmach/magistrala/users"
|
||||
"github.com/absmach/magistrala/users/hasher"
|
||||
"github.com/absmach/magistrala/users/mocks"
|
||||
@@ -30,7 +29,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
idProvider = uuid.New()
|
||||
phasher = hasher.New()
|
||||
secret = "strongsecret"
|
||||
validCMetadata = mgclients.Metadata{"role": "client"}
|
||||
@@ -47,7 +45,6 @@ var (
|
||||
inValidToken = "invalid"
|
||||
validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22"
|
||||
wrongID = testsutil.GenerateUUID(&testing.T{})
|
||||
errHashPassword = errors.New("generate hash from password failed")
|
||||
errAddPolicies = errors.New("failed to add policies")
|
||||
errDeletePolicies = errors.New("failed to delete policies")
|
||||
)
|
||||
@@ -56,24 +53,22 @@ func newService(selfRegister bool) (users.Service, *mocks.Repository, *authmocks
|
||||
cRepo := new(mocks.Repository)
|
||||
auth := new(authmocks.AuthClient)
|
||||
e := mocks.NewEmailer()
|
||||
return users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, selfRegister), cRepo, auth, e
|
||||
return users.NewService(cRepo, auth, e, passRegex, selfRegister, nil), cRepo, auth, e
|
||||
}
|
||||
|
||||
func TestRegisterClient(t *testing.T) {
|
||||
svc, cRepo, auth, _ := newService(true)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
client mgclients.Client
|
||||
identifyResponse *magistrala.IdentityRes
|
||||
addPoliciesResponse *magistrala.AddPoliciesRes
|
||||
deletePoliciesResponse *magistrala.DeletePoliciesRes
|
||||
token string
|
||||
identifyErr error
|
||||
addPoliciesResponseErr error
|
||||
deletePoliciesResponseErr error
|
||||
saveErr error
|
||||
err error
|
||||
desc string
|
||||
client mgclients.Client
|
||||
identifyResponse *magistrala.IdentityRes
|
||||
addPoliciesResponse *magistrala.AddPoliciesRes
|
||||
token string
|
||||
identifyErr error
|
||||
addPoliciesResponseErr error
|
||||
saveErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "register new client successfully",
|
||||
@@ -83,13 +78,12 @@ func TestRegisterClient(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "register existing client",
|
||||
client: client,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
token: validToken,
|
||||
saveErr: repoerr.ErrConflict,
|
||||
err: repoerr.ErrConflict,
|
||||
desc: "register existing client",
|
||||
client: client,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
token: validToken,
|
||||
saveErr: repoerr.ErrConflict,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "register a new enabled client with name",
|
||||
@@ -144,11 +138,10 @@ func TestRegisterClient(t *testing.T) {
|
||||
Secret: secret,
|
||||
},
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
saveErr: errors.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
token: validToken,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
saveErr: errors.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
token: validToken,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with missing secret",
|
||||
@@ -159,9 +152,8 @@ func TestRegisterClient(t *testing.T) {
|
||||
Secret: "",
|
||||
},
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
err: nil,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with a weak secret",
|
||||
@@ -172,9 +164,8 @@ func TestRegisterClient(t *testing.T) {
|
||||
Secret: "weak",
|
||||
},
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
err: nil,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: " register a client with a secret that is too long",
|
||||
@@ -185,9 +176,8 @@ func TestRegisterClient(t *testing.T) {
|
||||
Secret: strings.Repeat("a", 73),
|
||||
},
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
err: repoerr.ErrMalformedEntity,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with invalid status",
|
||||
@@ -199,9 +189,8 @@ func TestRegisterClient(t *testing.T) {
|
||||
},
|
||||
Status: mgclients.AllStatus,
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
err: svcerr.ErrInvalidStatus,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
err: svcerr.ErrInvalidStatus,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with invalid role",
|
||||
@@ -213,9 +202,8 @@ func TestRegisterClient(t *testing.T) {
|
||||
},
|
||||
Role: 2,
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true},
|
||||
err: svcerr.ErrInvalidRole,
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
err: svcerr.ErrInvalidRole,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with failed to authorize add policies",
|
||||
@@ -244,43 +232,11 @@ func TestRegisterClient(t *testing.T) {
|
||||
addPoliciesResponseErr: errAddPolicies,
|
||||
err: errAddPolicies,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with failed to delete policies with err",
|
||||
client: mgclients.Client{
|
||||
Name: "clientWithFailedToDeletePolicies",
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: "clientwithfailedtodelete@example.com",
|
||||
Secret: secret,
|
||||
},
|
||||
Role: mgclients.AdminRole,
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: false},
|
||||
deletePoliciesResponseErr: errDeletePolicies,
|
||||
saveErr: repoerr.ErrConflict,
|
||||
err: errDeletePolicies,
|
||||
},
|
||||
{
|
||||
desc: "register a new client with failed to delete policies with failed to delete",
|
||||
client: mgclients.Client{
|
||||
Name: "clientWithFailedToDeletePolicies",
|
||||
Credentials: mgclients.Credentials{
|
||||
Identity: "clientwithfailedtodelete@example.com",
|
||||
Secret: secret,
|
||||
},
|
||||
Role: mgclients.AdminRole,
|
||||
},
|
||||
addPoliciesResponse: &magistrala.AddPoliciesRes{Added: true},
|
||||
deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: false},
|
||||
saveErr: repoerr.ErrConflict,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
repoCall := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesResponse, tc.addPoliciesResponseErr)
|
||||
repoCall1 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesResponse, tc.deletePoliciesResponseErr)
|
||||
repoCall2 := cRepo.On("Save", context.Background(), mock.Anything).Return(tc.client, tc.saveErr)
|
||||
repoCall1 := cRepo.On("Save", context.Background(), mock.Anything).Return(tc.client, tc.saveErr)
|
||||
expected, err := svc.RegisterClient(context.Background(), tc.token, tc.client)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
@@ -290,10 +246,9 @@ func TestRegisterClient(t *testing.T) {
|
||||
tc.client.Credentials.Secret = expected.Credentials.Secret
|
||||
tc.client.UpdatedBy = expected.UpdatedBy
|
||||
assert.Equal(t, tc.client, expected, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.client, expected))
|
||||
ok := repoCall2.Parent.AssertCalled(t, "Save", context.Background(), mock.Anything)
|
||||
ok := repoCall1.Parent.AssertCalled(t, "Save", context.Background(), mock.Anything)
|
||||
assert.True(t, ok, fmt.Sprintf("Save was not called on %s", tc.desc))
|
||||
}
|
||||
repoCall2.Unset()
|
||||
repoCall1.Unset()
|
||||
repoCall.Unset()
|
||||
}
|
||||
@@ -1209,14 +1164,14 @@ func TestUpdateClientSecret(t *testing.T) {
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update client secret with invalod old secret",
|
||||
desc: "update client secret with invalid old secret",
|
||||
oldSecret: "invalid",
|
||||
newSecret: newSecret,
|
||||
token: validToken,
|
||||
identifyResponse: &magistrala.IdentityRes{UserId: client.ID},
|
||||
retrieveByIDResponse: client,
|
||||
retrieveByIdentityResponse: rClient,
|
||||
err: svcerr.ErrLogin,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update client secret with too long new secret",
|
||||
@@ -1226,7 +1181,7 @@ func TestUpdateClientSecret(t *testing.T) {
|
||||
identifyResponse: &magistrala.IdentityRes{UserId: client.ID},
|
||||
retrieveByIDResponse: client,
|
||||
retrieveByIdentityResponse: rClient,
|
||||
err: repoerr.ErrMalformedEntity,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update client secret with failed to update secret",
|
||||
@@ -1252,9 +1207,9 @@ func TestUpdateClientSecret(t *testing.T) {
|
||||
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.response, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, updatedClient))
|
||||
if tc.err == nil {
|
||||
ok := repoCall1.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.response.ID)
|
||||
ok := repoCall1.Parent.AssertCalled(t, "RetrieveByID", context.Background(), mock.Anything)
|
||||
assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc))
|
||||
ok = repoCall2.Parent.AssertCalled(t, "RetrieveByIdentity", context.Background(), tc.response.Credentials.Identity)
|
||||
ok = repoCall2.Parent.AssertCalled(t, "RetrieveByIdentity", context.Background(), mock.Anything)
|
||||
assert.True(t, ok, fmt.Sprintf("RetrieveByIdentity was not called on %s", tc.desc))
|
||||
ok = repoCall3.Parent.AssertCalled(t, "UpdateSecret", context.Background(), mock.Anything)
|
||||
assert.True(t, ok, fmt.Sprintf("UpdateSecret was not called on %s", tc.desc))
|
||||
@@ -2143,12 +2098,6 @@ func TestIssueToken(t *testing.T) {
|
||||
retrieveByIdentityErr: repoerr.ErrNotFound,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "issue token for a client with wrong secret",
|
||||
client: client,
|
||||
retrieveByIdentityResponse: rClient3,
|
||||
err: svcerr.ErrLogin,
|
||||
},
|
||||
{
|
||||
desc: "issue token with non-empty domain id",
|
||||
DomainID: "domain",
|
||||
@@ -2387,7 +2336,7 @@ func TestResetSecret(t *testing.T) {
|
||||
newSecret: strings.Repeat("strongSecret", 10),
|
||||
identifyResponse: &magistrala.IdentityRes{UserId: client.ID},
|
||||
retrieveByIDResponse: client,
|
||||
err: errHashPassword,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2397,7 +2346,6 @@ func TestResetSecret(t *testing.T) {
|
||||
repoCall2 := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateSecretErr)
|
||||
err := svc.ResetSecret(context.Background(), tc.token, tc.newSecret)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
repoCall2.Parent.AssertCalled(t, "UpdateSecret", context.Background(), mock.Anything)
|
||||
repoCall1.Parent.AssertCalled(t, "RetrieveByID", context.Background(), client.ID)
|
||||
repoCall.Parent.AssertCalled(t, "Identify", mock.Anything, mock.Anything)
|
||||
|
||||
Reference in New Issue
Block a user