Compare commits

...

8 Commits

Author SHA1 Message Date
dusanb 8ffc710b6a Merge branch 'ci' into 'main'
MG-1 - Update Docker Image names and setup Continous Delivery

Closes #1

See merge request DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala!4
2024-03-01 13:45:04 +00:00
Rodney Osodo f762b91669 MG-1 - Update Docker Image names and setup Continous Delivery 2024-03-01 13:45:04 +00:00
dusanb 6e8ca15f56 Merge branch 'kratos-oauth2' into 'main'
NOISSUE - Add Kratos as an OAuth2.0 Provider

See merge request DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala!6
2024-03-01 13:33:10 +00:00
Rodney Osodo d3fe7d1063 NOISSUE - Add Kratos as an OAuth2.0 Provider 2024-03-01 13:33:09 +00:00
dusanb ad6619f0ac Merge branch 'update-main' into 'main'
NOISSUE - Add Kratos as Users Repository

See merge request DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala!7
2024-03-01 12:29:48 +00:00
Rodney Osodo 4bee424289 NOISSUE - Add Kratos as Users Repository 2024-03-01 12:29:47 +00:00
dusanb f55d7640eb Merge branch 'migrate-to-gitlab' into 'main'
NOISSUE - Migrate to GitLab

See merge request DAVEEmbeddedSystems/innovation-lab/tlm/tlml/abstract-machines/magistrala!8
2024-03-01 12:15:10 +00:00
Rodney Osodo 221c1212bb NOISSUE - Migrate to GitLab 2024-03-01 12:15:10 +00:00
63 changed files with 1251 additions and 2338 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
.git
.github
.gitlab
build
docker
metrics
-52
View File
@@ -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
-11
View File
@@ -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
-58
View File
@@ -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)
-155
View File
@@ -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"
-31
View File
@@ -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
-31
View File
@@ -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
-407
View File
@@ -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
+78
View File
@@ -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:
+36
View File
@@ -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"
+25
View File
@@ -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?
-5
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+16 -4
View File
@@ -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:
+2 -12
View File
@@ -1,8 +1,5 @@
# Magistrala
[![Check License Header](https://github.com/absmach/magistrala/actions/workflows/check-license.yaml/badge.svg?branch=main)](https://github.com/absmach/magistrala/actions/workflows/check-license.yaml)
[![Check the consistency of generated files](https://github.com/absmach/magistrala/actions/workflows/check-generated-files.yml/badge.svg?branch=main)](https://github.com/absmach/magistrala/actions/workflows/check-generated-files.yml)
[![Continuous Delivery](https://github.com/absmach/magistrala/actions/workflows/build.yml/badge.svg?branch=main)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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:
+1
View File
@@ -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
+2
View File
@@ -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=
+2 -2
View File
@@ -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)
}
+12
View File
@@ -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
}
}
+6
View File
@@ -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
+212
View File
@@ -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)
}
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
```
+17
View File
@@ -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
}
+455
View File
@@ -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,
}
}
+52
View File
@@ -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
}
}
}
-203
View File
@@ -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)
}
-755
View File
@@ -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))
}
}
}
-5
View File
@@ -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
-50
View File
@@ -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{},
},
},
}
}
-94
View File
@@ -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
View File
@@ -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
View File
@@ -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)