mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 23:32:14 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 436db8877d | |||
| 4da66aecbf |
@@ -1,6 +1,3 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.git
|
||||
.github
|
||||
build
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
# Docker: Environment variables in Compose
|
||||
|
||||
## NginX
|
||||
MF_NGINX_HTTP_PORT=80
|
||||
MF_NGINX_SSL_PORT=443
|
||||
MF_NGINX_MQTT_PORT=1883
|
||||
MF_NGINX_MQTTS_PORT=8883
|
||||
|
||||
## NATS
|
||||
MF_NATS_URL=nats://nats:4222
|
||||
|
||||
## Redis
|
||||
MF_REDIS_TCP_PORT=6379
|
||||
|
||||
## Grafana
|
||||
MF_GRAFANA_PORT=3000
|
||||
|
||||
## Jaeger
|
||||
MF_JAEGER_PORT=6831
|
||||
MF_JAEGER_FRONTEND=16686
|
||||
MF_JAEGER_COLLECTOR=14268
|
||||
MF_JAEGER_CONFIGS=5778
|
||||
MF_JAEGER_URL=jaeger:6831
|
||||
|
||||
## Core Services
|
||||
|
||||
### AuthN
|
||||
MF_AUTHN_LOG_LEVEL=debug
|
||||
MF_AUTHN_HTTP_PORT=8189
|
||||
MF_AUTHN_GRPC_PORT=8181
|
||||
MF_AUTHN_GRPC_URL=authn:8181
|
||||
MF_AUTHN_GRPC_TIMEOUT=1s
|
||||
MF_AUTHN_DB_PORT=5432
|
||||
MF_AUTHN_DB_USER=mainflux
|
||||
MF_AUTHN_DB_PASS=mainflux
|
||||
MF_AUTHN_DB=authn
|
||||
MF_AUTHN_SECRET=secret
|
||||
|
||||
### Users
|
||||
MF_USERS_LOG_LEVEL=debug
|
||||
MF_USERS_HTTP_PORT=8180
|
||||
MF_USERS_DB_PORT=5432
|
||||
MF_USERS_DB_USER=mainflux
|
||||
MF_USERS_DB_PASS=mainflux
|
||||
MF_USERS_DB=users
|
||||
MF_USERS_ADMIN_EMAIL=admin@example.com
|
||||
MF_USERS_ADMIN_PASSWORD=12345678
|
||||
|
||||
### Email utility
|
||||
MF_EMAIL_DRIVER=smtp
|
||||
MF_EMAIL_HOST=smtp.mailtrap.io
|
||||
MF_EMAIL_PORT=2525
|
||||
MF_EMAIL_USERNAME=18bf7f70705139
|
||||
MF_EMAIL_PASSWORD=2b0d302e775b1e
|
||||
MF_EMAIL_FROM_ADDRESS=from@example.com
|
||||
MF_EMAIL_FROM_NAME=Example
|
||||
MF_EMAIL_TEMPLATE=email.tmpl
|
||||
|
||||
### Token utility
|
||||
MF_TOKEN_RESET_ENDPOINT=/reset-request
|
||||
|
||||
### Things
|
||||
MF_THINGS_LOG_LEVEL=debug
|
||||
MF_THINGS_HTTP_PORT=8182
|
||||
MF_THINGS_AUTH_HTTP_PORT=8989
|
||||
MF_THINGS_AUTH_GRPC_PORT=8183
|
||||
MF_THINGS_AUTH_GRPC_URL=things:8183
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT=1s
|
||||
MF_THINGS_DB_PORT=5432
|
||||
MF_THINGS_DB_USER=mainflux
|
||||
MF_THINGS_DB_PASS=mainflux
|
||||
MF_THINGS_DB=things
|
||||
MF_THINGS_ES_URL=localhost:6379
|
||||
MF_THINGS_ES_PASS=
|
||||
MF_THINGS_ES_DB=0
|
||||
|
||||
### HTTP
|
||||
MF_HTTP_ADAPTER_PORT=8185
|
||||
|
||||
### MQTT
|
||||
MF_MQTT_ADAPTER_LOG_LEVEL=debug
|
||||
MF_MQTT_ADAPTER_MQTT_PORT=1883
|
||||
MF_MQTT_BROKER_PORT=1883
|
||||
MF_MQTT_ADAPTER_WS_PORT=8080
|
||||
MF_MQTT_BROKER_WS_PORT=8080
|
||||
MF_MQTT_ADAPTER_ES_DB=0
|
||||
MF_MQTT_ADAPTER_ES_PASS=
|
||||
|
||||
### VERMEMQ
|
||||
MF_DOCKER_VERNEMQ_ALLOW_ANONYMOUS=on
|
||||
MF_DOCKER_VERNEMQ_LOG__CONSOLE__LEVEL=error
|
||||
|
||||
### CoAP
|
||||
MF_COAP_ADAPTER_LOG_LEVEL=debug
|
||||
MF_COAP_ADAPTER_PORT=5683
|
||||
|
||||
## Addons Services
|
||||
### Bootstrap
|
||||
MF_BOOTSTRAP_LOG_LEVEL=debug
|
||||
MF_BOOTSTRAP_PORT=8202
|
||||
MF_BOOTSTRAP_DB_PORT=5432
|
||||
MF_BOOTSTRAP_DB_USER=mainflux
|
||||
MF_BOOTSTRAP_DB_PASS=mainflux
|
||||
MF_BOOTSTRAP_DB=bootstrap
|
||||
MF_BOOTSTRAP_DB_SSL_MODE=disable
|
||||
|
||||
### Provision
|
||||
MF_PROVISION_CONFIG_FILE=/configs/config.toml
|
||||
MF_PROVISION_LOG_LEVEL=debug
|
||||
MF_PROVISION_HTTP_PORT=8190
|
||||
MF_PROVISION_ENV_CLIENTS_TLS=false
|
||||
MF_PROVISION_SERVER_CERT=
|
||||
MF_PROVISION_SERVER_KEY=
|
||||
MF_PROVISION_MQTT_URL=tcp://localhost
|
||||
MF_PROVISION_USERS_LOCATION=http://localhost:8180
|
||||
MF_PROVISION_THINGS_LOCATION=http://things:8182
|
||||
MF_PROVISION_USER=
|
||||
MF_PROVISION_PASS=
|
||||
MF_PROVISION_API_KEY=
|
||||
MF_PROVISION_CERTS_SVC_URL=http://certs:8204
|
||||
MF_PROVISION_X509_PROVISIONING=true
|
||||
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things
|
||||
MF_PROVISION_BS_SVC_WHITELIST_URL=http://bootstrap:8202/things/state
|
||||
MF_PROVISION_BS_CONFIG_PROVISIONING=true
|
||||
MF_PROVISION_BS_AUTO_WHITELIST=true
|
||||
MF_PROVISION_BS_CONTENT=
|
||||
MF_PROVISION_CERTS_RSA_BITS=4096
|
||||
MF_PROVISION_CERTS_HOURS_VALID=2400h
|
||||
|
||||
# Certs
|
||||
MF_CERTS_LOG_LEVEL=debug
|
||||
MF_CERTS_HTTP_PORT=8204
|
||||
MF_CERTS_DB_HOST=certs-db
|
||||
MF_CERTS_DB_PORT=5432
|
||||
MF_CERTS_DB_USER=mainflux
|
||||
MF_CERTS_DB_PASS=mainflux
|
||||
MF_CERTS_DB=certs
|
||||
MF_CERTS_DB_SSL_MODE=
|
||||
MF_CERTS_DB_SSL_CERT=
|
||||
MF_CERTS_DB_SSL_KEY=
|
||||
MF_CERTS_DB_SSL_ROOT_CERT=
|
||||
MF_CERTS_ENCRYPT_KEY=
|
||||
MF_CERTS_CLIENT_TLS=
|
||||
MF_CERTS_CA_CERTS=
|
||||
MF_CERTS_SERVER_CERT=
|
||||
MF_CERTS_SERVER_KEY=
|
||||
MF_SDK_BASE_URL=http://172.17.0.1
|
||||
MF_SDK_THINGS_PREFIX=
|
||||
MF_CERTS_SIGN_CA_PATH=/etc/ssl/certs/ca.crt
|
||||
MF_CERTS_SIGN_CA_KEY_PATH=/etc/ssl/certs/ca.key
|
||||
MF_CERTS_SIGN_HOURS_VALID=2048h
|
||||
MF_CERTS_SIGN_RSA_BITS=2048
|
||||
MF_CERTS_VAULT_HOST=
|
||||
MF_CERTS_VAULT_PKI_PATH=pki_int
|
||||
MF_CERTS_VAULT_ROLE=agent
|
||||
MF_CERTS_VAULT_TOKEN=s.nArgw6xn3uIOfA7nfKk8LFaW
|
||||
|
||||
|
||||
### LoRa
|
||||
MF_LORA_ADAPTER_LOG_LEVEL=debug
|
||||
MF_LORA_ADAPTER_MESSAGES_URL=tcp://lora.mqtt.mainflux.io:1883
|
||||
MF_LORA_ADAPTER_HTTP_PORT=8187
|
||||
MF_LORA_ADAPTER_ROUTE_MAP_URL=localhost:6379
|
||||
MF_LORA_ADAPTER_ROUTE_MAP_PASS=
|
||||
MF_LORA_ADAPTER_ROUTE_MAP_DB=0
|
||||
|
||||
### OPC-UA
|
||||
MF_OPCUA_ADAPTER_HTTP_PORT=8188
|
||||
MF_OPCUA_ADAPTER_LOG_LEVEL=debug
|
||||
MF_OPCUA_ADAPTER_POLICY=
|
||||
MF_OPCUA_ADAPTER_MODE=
|
||||
MF_OPCUA_ADAPTER_CERT_FILE=
|
||||
MF_OPCUA_ADAPTER_KEY_FILE=
|
||||
MF_OPCUA_ADAPTER_ROUTE_MAP_URL=localhost:6379
|
||||
MF_OPCUA_ADAPTER_ROUTE_MAP_PASS=
|
||||
MF_OPCUA_ADAPTER_ROUTE_MAP_DB=0
|
||||
MF_OPCUA_ADAPTER_EVENT_CONSUMER=opcua
|
||||
|
||||
### Cassandra Writer
|
||||
MF_CASSANDRA_WRITER_LOG_LEVEL=debug
|
||||
MF_CASSANDRA_WRITER_PORT=8902
|
||||
MF_CASSANDRA_WRITER_DB_PORT=9042
|
||||
MF_CASSANDRA_WRITER_DB_CLUSTER=mainflux-cassandra
|
||||
MF_CASSANDRA_WRITER_DB_KEYSPACE=mainflux
|
||||
MF_CASSANDRA_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### Cassandra Reader
|
||||
MF_CASSANDRA_READER_LOG_LEVEL=debug
|
||||
MF_CASSANDRA_READER_PORT=8903
|
||||
MF_CASSANDRA_READER_DB_PORT=9042
|
||||
MF_CASSANDRA_READER_DB_CLUSTER=mainflux-cassandra
|
||||
MF_CASSANDRA_READER_DB_KEYSPACE=mainflux
|
||||
MF_CASSANDRA_READER_SERVER_CERT=
|
||||
MF_CASSANDRA_READER_SERVER_KEY=
|
||||
|
||||
### InfluxDB Writer
|
||||
MF_INFLUX_WRITER_LOG_LEVEL=debug
|
||||
MF_INFLUX_WRITER_PORT=8900
|
||||
MF_INFLUX_WRITER_BATCH_SIZE=5000
|
||||
MF_INFLUX_WRITER_BATCH_TIMEOUT=5
|
||||
MF_INFLUX_WRITER_DB_PORT=8086
|
||||
MF_INFLUX_WRITER_DB_USER=mainflux
|
||||
MF_INFLUX_WRITER_DB_PASS=mainflux
|
||||
MF_INFLUX_WRITER_DB=mainflux
|
||||
MF_INFLUX_WRITER_GRAFANA_PORT=3001
|
||||
MF_INFLUX_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### InfluxDB Reader
|
||||
MF_INFLUX_READER_LOG_LEVEL=debug
|
||||
MF_INFLUX_READER_PORT=8905
|
||||
MF_INFLUX_READER_DB_PORT=8086
|
||||
MF_INFLUX_READER_DB_USER=mainflux
|
||||
MF_INFLUX_READER_DB_PASS=mainflux
|
||||
MF_INFLUX_READER_DB=mainflux
|
||||
MF_INFLUX_READER_SERVER_CERT=
|
||||
MF_INFLUX_READER_SERVER_KEY=
|
||||
|
||||
### MongoDB Writer
|
||||
MF_MONGO_WRITER_LOG_LEVEL=debug
|
||||
MF_MONGO_WRITER_PORT=8901
|
||||
MF_MONGO_WRITER_DB=mainflux
|
||||
MF_MONGO_WRITER_DB_PORT=27017
|
||||
MF_MONGO_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### MongoDB Reader
|
||||
MF_MONGO_READER_LOG_LEVEL=debug
|
||||
MF_MONGO_READER_PORT=8904
|
||||
MF_MONGO_READER_DB=mainflux
|
||||
MF_MONGO_READER_DB_PORT=27017
|
||||
MF_MONGO_READER_SERVER_CERT=
|
||||
MF_MONGO_READER_SERVER_KEY=
|
||||
|
||||
### Postgres Writer
|
||||
MF_POSTGRES_WRITER_LOG_LEVEL=debug
|
||||
MF_POSTGRES_WRITER_PORT=9104
|
||||
MF_POSTGRES_WRITER_DB_PORT=5432
|
||||
MF_POSTGRES_WRITER_DB_USER=mainflux
|
||||
MF_POSTGRES_WRITER_DB_PASS=mainflux
|
||||
MF_POSTGRES_WRITER_DB=mainflux
|
||||
MF_POSTGRES_WRITER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_WRITER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_KEY=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_ROOT_CERT=""
|
||||
MF_POSTGRES_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### Postgres Reader
|
||||
MF_POSTGRES_READER_LOG_LEVEL=debug
|
||||
MF_POSTGRES_READER_PORT=9204
|
||||
MF_POSTGRES_READER_CLIENT_TLS=false
|
||||
MF_POSTGRES_READER_CA_CERTS=""
|
||||
MF_POSTGRES_READER_DB_PORT=5432
|
||||
MF_POSTGRES_READER_DB_USER=mainflux
|
||||
MF_POSTGRES_READER_DB_PASS=mainflux
|
||||
MF_POSTGRES_READER_DB=mainflux
|
||||
MF_POSTGRES_READER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_READER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_READER_DB_SSL_KEY=""
|
||||
MF_POSTGRES_READER_DB_SSL_ROOT_CERT=""
|
||||
|
||||
# Twins
|
||||
MF_TWINS_LOG_LEVEL=debug
|
||||
MF_TWINS_HTTP_PORT=9021
|
||||
MF_TWINS_SERVER_CERT=""
|
||||
MF_TWINS_SERVER_KEY=""
|
||||
MF_TWINS_DB=mainflux-twins
|
||||
MF_TWINS_DB_HOST=twins-db
|
||||
MF_TWINS_DB_PORT=27018
|
||||
MF_TWINS_SINGLE_USER_EMAIL=""
|
||||
MF_TWINS_SINGLE_USER_TOKEN=""
|
||||
MF_TWINS_CLIENT_TLS=""
|
||||
MF_TWINS_CA_CERTS=""
|
||||
MF_TWINS_CHANNEL_ID=
|
||||
MF_TWINS_CACHE_URL=es-redis:6379
|
||||
MF_TWINS_CACHE_PASS=
|
||||
MF_TWINS_CACHE_DB=0
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @absmach/supermq
|
||||
* @mainflux/maintainers
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
The GitHub issue tracker is for bug reports and feature requests. General support can be found at
|
||||
the following locations:
|
||||
|
||||
- Google group - https://groups.google.com/forum/#!forum/mainflux
|
||||
- Gitter - https://gitter.im/mainflux/mainflux
|
||||
-->
|
||||
|
||||
**FEATURE REQUEST**
|
||||
|
||||
1. Is there an open issue addressing this request? If it does, please add a "+1" reaction to the
|
||||
existing issue, otherwise proceed to step 2.
|
||||
|
||||
2. Describe the feature you are requesting, as well as the possible use case(s) for it.
|
||||
|
||||
3. Indicate the importance of this feature to you (must-have, should-have, nice-to-have).
|
||||
|
||||
**BUG REPORT**
|
||||
|
||||
1. What were you trying to achieve?
|
||||
|
||||
2. What are the expected results?
|
||||
|
||||
3. What are the received results?
|
||||
|
||||
4. What are the steps to reproduce the issue?
|
||||
|
||||
5. In what environment did you encounter the issue?
|
||||
|
||||
6. Additional information you deem important:
|
||||
@@ -1,52 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Bug Report
|
||||
description: File a bug/issue report. Make sure to search to see if an issue already exists for the bug you encountered.
|
||||
title: "Bug: <title>"
|
||||
labels: ["bug", "needs-review", "help wanted"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What were you trying to achieve?
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What are the expected results?
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What are the received results?
|
||||
description: A concise description of what you received.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: What are the steps to reproduce the issue?
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: In what environment did you encounter the issue?
|
||||
description: A concise description of the environment you encountered the issue in.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information you deem important
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,11 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Google group
|
||||
url: https://groups.google.com/forum/#!forum/mainflux
|
||||
about: Join the SuperMQ community on Google group.
|
||||
- name: Gitter
|
||||
url: https://gitter.im/mainflux/mainflux
|
||||
about: Join the SuperMQ community on Gitter.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Feature Request
|
||||
description: File a feature request. Make sure to search to see if a request already exists for the feature you are requesting.
|
||||
title: "Feature: <title>"
|
||||
labels: ["enchancement", "needs-review"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you are requesting, as well as the possible use case(s) for it.
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Indicate the importance of this feature to you.
|
||||
description: This will help us prioritize the feature request.
|
||||
options:
|
||||
- Must-have
|
||||
- Should-have
|
||||
- Nice-to-have
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the feature that you are requesting.
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,69 +1,15 @@
|
||||
<!-- Copyright (c) Abstract Machines
|
||||
SPDX-License-Identifier: Apache-2.0 -->
|
||||
Pull request title should be `MF-XXX - description` or `NOISSUE - description` where XXX is ID of issue that this PR relate to.
|
||||
Please review the [CONTRIBUTING.md](./CONTRIBUTING.md) file for detailed contributing guidelines.
|
||||
|
||||
<!--
|
||||
### What does this do?
|
||||
|
||||
Pull request title should be `SMQ-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/supermq/blob/main/CONTRIBUTING.md) file for detailed contributing guidelines.
|
||||
### Which issue(s) does this PR fix/relate to?
|
||||
Put here `Resolves #XXX` to auto-close the issue that your PR fixes (if such)
|
||||
|
||||
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.
|
||||
### List any changes that modify/break current functionality
|
||||
|
||||
For a timely review/response, please avoid force-pushing additional commits if your PR already received reviews or comments.
|
||||
### Have you included tests for your changes?
|
||||
|
||||
- Provide tests for your changes.
|
||||
- Use descriptive commit messages.
|
||||
- Comment your code where appropriate.
|
||||
- Squash your commits
|
||||
- Update any related documentation.
|
||||
-->
|
||||
|
||||
# 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: ...
|
||||
-->
|
||||
|
||||
## What does this do?
|
||||
|
||||
<!--
|
||||
Please provide a brief description of what this PR is intended to do.
|
||||
Include List any changes that modify/break current functionality.
|
||||
-->
|
||||
|
||||
## Which issue(s) does this PR fix/relate to?
|
||||
|
||||
<!--
|
||||
For pull requests that relate or close an issue, please include them below. We like to follow [Github's guidance on linking issues to pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||
|
||||
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?
|
||||
|
||||
<!--If you have not included tests, please explain why.
|
||||
For example:
|
||||
Yes, I have included tests for my changes.
|
||||
No, I have not included tests because I do not know how to.
|
||||
-->
|
||||
|
||||
## Did you document any new/modified feature?
|
||||
|
||||
<!--If you have not included documentation, please explain why.
|
||||
For example:
|
||||
Yes, I have updated the documentation for the new feature.
|
||||
No, I have not updated the documentation because I do not know how to.
|
||||
-->
|
||||
### Did you document any new/modified functionality?
|
||||
|
||||
### Notes
|
||||
|
||||
<!--Please provide any additional information you feel is important.-->
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "./.github/workflows"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
day: "monday"
|
||||
timezone: "Europe/Paris"
|
||||
groups:
|
||||
gh-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
timezone: "Europe/Paris"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "./docker"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
day: "monday"
|
||||
timezone: "Europe/Paris"
|
||||
groups:
|
||||
docker-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -1,294 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Property Based Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/**"
|
||||
- "auth/api/http/**"
|
||||
- "bootstrap/api**"
|
||||
- "certs/api/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
- "consumers/notifiers/api/**"
|
||||
- "domains/api/http/**"
|
||||
- "groups/api/http/**"
|
||||
- "http/api/**"
|
||||
- "invitations/api/**"
|
||||
- "journal/api/**"
|
||||
- "provision/api/**"
|
||||
- "readers/api/**"
|
||||
- "users/api/**"
|
||||
|
||||
env:
|
||||
TOKENS_URL: http://localhost:9002/users/tokens/issue
|
||||
CREATE_DOMAINS_URL: http://localhost:9003/domains
|
||||
USER_IDENTITY: admin@example.com
|
||||
USER_SECRET: 12345678
|
||||
DOMAIN_NAME: demo-test
|
||||
USERS_URL: http://localhost:9002
|
||||
DOMAIN_URL: http://localhost:9003
|
||||
CLIENTS_URL: http://localhost:9006
|
||||
CHANNELS_URL: http://localhost:9005
|
||||
GROUPS_URL: http://localhost:9004
|
||||
HTTP_ADAPTER_URL: http://localhost:8008
|
||||
INVITATIONS_URL: http://localhost:9020
|
||||
AUTH_URL: http://localhost:9001
|
||||
BOOTSTRAP_URL: http://localhost:9013
|
||||
CERTS_URL: http://localhost:9019
|
||||
PROVISION_URL: http://localhost:9016
|
||||
POSTGRES_READER_URL: http://localhost:9009
|
||||
TIMESCALE_READER_URL: http://localhost:9011
|
||||
JOURNAL_URL: http://localhost:9021
|
||||
|
||||
jobs:
|
||||
api-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Build images
|
||||
run: make all -j $(nproc) && make dockers_dev -j $(nproc)
|
||||
|
||||
- name: Start containers
|
||||
run: make run up args="-d" && make run_addons up args="-d"
|
||||
|
||||
- name: Set access token
|
||||
run: |
|
||||
export USER_TOKEN=$(curl -sSX POST $TOKENS_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token)
|
||||
export DOMAIN_ID=$(curl -sSX POST $CREATE_DOMAINS_URL -H "Content-Type: application/json" -H "Authorization: Bearer $USER_TOKEN" -d "{\"name\":\"$DOMAIN_NAME\",\"alias\":\"$DOMAIN_NAME\"}" | jq -r .id)
|
||||
echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV
|
||||
export CLIENT_SECRET=$(supermq-cli provision test | /usr/bin/grep -Eo '"secret": "[^"]+"' | awk 'NR % 2 == 0' | sed 's/"secret": "\(.*\)"/\1/')
|
||||
echo "CLIENT_SECRET=$CLIENT_SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
journal:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/journal.yml"
|
||||
- "journal/api/**"
|
||||
|
||||
auth:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/auth.yml"
|
||||
- "auth/api/http/**"
|
||||
|
||||
bootstrap:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/bootstrap.yml"
|
||||
- "bootstrap/api/**"
|
||||
|
||||
certs:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/certs.yml"
|
||||
- "certs/api/**"
|
||||
|
||||
domains:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/domains.yml"
|
||||
- "domains/api/http/**"
|
||||
|
||||
http:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/http.yml"
|
||||
- "http/api/**"
|
||||
|
||||
invitations:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/invitations.yml"
|
||||
- "invitations/api/**"
|
||||
|
||||
provision:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/provision.yml"
|
||||
- "provision/api/**"
|
||||
|
||||
readers:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/readers.yml"
|
||||
- "readers/api/**"
|
||||
|
||||
clients:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/clients.yml"
|
||||
- "clients/api/http/**"
|
||||
|
||||
channels:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/channels.yml"
|
||||
- "channels/api/http/**"
|
||||
|
||||
groups:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/groups.yml"
|
||||
- "groups/api/http/**"
|
||||
|
||||
users:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/openapi/users.yml"
|
||||
- "users/api/**"
|
||||
|
||||
- name: Run Users API tests
|
||||
if: steps.changes.outputs.users == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/users.yml
|
||||
base-url: ${{ env.USERS_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Groups API tests
|
||||
if: steps.changes.outputs.groups == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/groups.yml
|
||||
base-url: ${{ env.GROUPS_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Clients API tests
|
||||
if: steps.changes.outputs.clients == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/clients.yml
|
||||
base-url: ${{ env.CLIENTS_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Channels API tests
|
||||
if: steps.changes.outputs.channels == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/channels.yml
|
||||
base-url: ${{ env.CHANNELS_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run HTTP Adapter API tests
|
||||
if: steps.changes.outputs.http == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/http.yml
|
||||
base-url: ${{ env.HTTP_ADAPTER_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Client ${{ env.CLIENT_SECRET }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Invitations API tests
|
||||
if: steps.changes.outputs.invitations == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/invitations.yml
|
||||
base-url: ${{ env.INVITATIONS_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Auth API tests
|
||||
if: steps.changes.outputs.auth == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/auth.yml
|
||||
base-url: ${{ env.AUTH_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Domains API tests
|
||||
if: steps.changes.outputs.domains == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/domains.yml
|
||||
base-url: ${{ env.DOMAIN_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Journal API tests
|
||||
if: steps.changes.outputs.journal == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/journal.yml
|
||||
base-url: ${{ env.JOURNAL_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Bootstrap API tests
|
||||
if: steps.changes.outputs.bootstrap == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/bootstrap.yml
|
||||
base-url: ${{ env.BOOTSTRAP_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Certs API tests
|
||||
if: steps.changes.outputs.certs == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/certs.yml
|
||||
base-url: ${{ env.CERTS_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Provision API tests
|
||||
if: steps.changes.outputs.provision == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/provision.yml
|
||||
base-url: ${{ env.PROVISION_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Seed Messages
|
||||
if: steps.changes.outputs.readers == 'true'
|
||||
run: |
|
||||
make cli
|
||||
./build/cli provision test
|
||||
|
||||
- name: Run Postgres Reader API tests
|
||||
if: steps.changes.outputs.readers == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/readers.yml
|
||||
base-url: ${{ env.POSTGRES_READER_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Timescale Reader API tests
|
||||
if: steps.changes.outputs.readers == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/readers.yml
|
||||
base-url: ${{ env.TIMESCALE_READER_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: make run down args="-v" && make run_addons down args="-v"
|
||||
@@ -1,62 +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: Fetch tags for the build
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
files: ./coverage/*.out
|
||||
codecov_yml_path: tools/config/codecov.yml
|
||||
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: |
|
||||
SMQ_MESSAGE_BROKER_TYPE=rabbitmq make mqtt
|
||||
|
||||
- name: Compile check for redis
|
||||
run: |
|
||||
SMQ_ES_TYPE=redis make mqtt
|
||||
|
||||
- name: Build and push Dockers
|
||||
run: |
|
||||
make latest -j $(nproc)
|
||||
@@ -1,243 +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@v5
|
||||
with:
|
||||
go-version: 1.22.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@v3
|
||||
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/sdk.go"
|
||||
- "users/postgres/clients.go"
|
||||
- "users/clients.go"
|
||||
- "pkg/clients/clients.go"
|
||||
- "pkg/messaging/pubsub.go"
|
||||
- "clients/postgres/clients.go"
|
||||
- "clients/clients.go"
|
||||
- "pkg/authz.go"
|
||||
- "pkg/authn.go"
|
||||
- "auth/domains.go"
|
||||
- "auth/keys.go"
|
||||
- "auth/service.go"
|
||||
- "pkg/events/events.go"
|
||||
- "provision/service.go"
|
||||
- "pkg/groups/groups.go"
|
||||
- "bootstrap/service.go"
|
||||
- "bootstrap/configs.go"
|
||||
- "invitations/invitations.go"
|
||||
- "users/emailer.go"
|
||||
- "users/hasher.go"
|
||||
- "mqtt/events/streams.go"
|
||||
- "readers/messages.go"
|
||||
- "consumers/notifiers/notifier.go"
|
||||
- "consumers/notifiers/service.go"
|
||||
- "consumers/notifiers/subscriptions.go"
|
||||
- "certs/certs.go"
|
||||
- "certs/pki/vault.go"
|
||||
- "certs/service.go"
|
||||
- "journal/journal.go"
|
||||
|
||||
- name: Set up protoc
|
||||
if: steps.changes.outputs.proto == 'true'
|
||||
run: |
|
||||
PROTOC_VERSION=29.0
|
||||
PROTOC_GEN_VERSION=v1.35.2
|
||||
PROTOC_GRPC_VERSION=v1.5.1
|
||||
|
||||
# Export the variables so they are available in future steps
|
||||
echo "PROTOC_VERSION=$PROTOC_VERSION" >> $GITHUB_ENV
|
||||
echo "PROTOC_GEN_VERSION=$PROTOC_GEN_VERSION" >> $GITHUB_ENV
|
||||
echo "PROTOC_GRPC_VERSION=$PROTOC_GRPC_VERSION" >> $GITHUB_ENV
|
||||
|
||||
# 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 "Here is the difference:"
|
||||
diff $p $p.tmp || true
|
||||
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.43.2
|
||||
go install github.com/vektra/mockery/v2@$MOCKERY_VERSION
|
||||
|
||||
mv ./invitations/mocks/repository.go ./invitations/mocks/repository.go.tmp
|
||||
mv ./invitations/mocks/service.go ./invitations/mocks/service.go.tmp
|
||||
mv ./auth/mocks/token_client.go ./auth/mocks/token_client.go.tmp
|
||||
mv ./auth/mocks/authz.go ./auth/mocks/authz.go.tmp
|
||||
mv ./auth/mocks/keys.go ./auth/mocks/keys.go.tmp
|
||||
mv ./auth/mocks/service.go ./auth/mocks/service.go.tmp
|
||||
mv ./bootstrap/mocks/configs.go ./bootstrap/mocks/configs.go.tmp
|
||||
mv ./bootstrap/mocks/config_reader.go ./bootstrap/mocks/config_reader.go.tmp
|
||||
mv ./bootstrap/mocks/service.go ./bootstrap/mocks/service.go.tmp
|
||||
mv ./domains/mocks/domains_client.go ./domains/mocks/domains_client.go.tmp
|
||||
mv ./domains/mocks/repository.go ./domains/mocks/repository.go.tmp
|
||||
mv ./domains/mocks/service.go ./domains/mocks/service.go.tmp
|
||||
mv ./channels/mocks/repository.go ./channels/mocks/repository.go.tmp
|
||||
mv ./channels/mocks/channels_client.go ./channels/mocks/channels_client.go.tmp
|
||||
mv ./channels/mocks/service.go ./channels/mocks/service.go.tmp
|
||||
mv ./groups/private/mocks/service.go ./groups/private/mocks/service.go.tmp
|
||||
mv ./groups/mocks/repository.go ./groups/mocks/repository.go.tmp
|
||||
mv ./groups/mocks/groups_client.go ./groups/mocks/groups_client.go.tmp
|
||||
mv ./groups/mocks/service.go ./groups/mocks/service.go.tmp
|
||||
mv ./users/mocks/hasher.go ./users/mocks/hasher.go.tmp
|
||||
mv ./users/mocks/emailer.go ./users/mocks/emailer.go.tmp
|
||||
mv ./users/mocks/repository.go ./users/mocks/repository.go.tmp
|
||||
mv ./users/mocks/service.go ./users/mocks/service.go.tmp
|
||||
mv ./journal/mocks/repository.go ./journal/mocks/repository.go.tmp
|
||||
mv ./journal/mocks/service.go ./journal/mocks/service.go.tmp
|
||||
mv ./consumers/notifiers/mocks/notifier.go ./consumers/notifiers/mocks/notifier.go.tmp
|
||||
mv ./consumers/notifiers/mocks/repository.go ./consumers/notifiers/mocks/repository.go.tmp
|
||||
mv ./consumers/notifiers/mocks/service.go ./consumers/notifiers/mocks/service.go.tmp
|
||||
mv ./certs/mocks/pki.go ./certs/mocks/pki.go.tmp
|
||||
mv ./certs/mocks/service.go ./certs/mocks/service.go.tmp
|
||||
mv ./provision/mocks/service.go ./provision/mocks/service.go.tmp
|
||||
mv ./clients/private/mocks/service.go ./clients/private/mocks/service.go.tmp
|
||||
mv ./clients/mocks/repository.go ./clients/mocks/repository.go.tmp
|
||||
mv ./clients/mocks/clients_client.go ./clients/mocks/clients_client.go.tmp
|
||||
mv ./clients/mocks/cache.go ./clients/mocks/cache.go.tmp
|
||||
mv ./clients/mocks/service.go ./clients/mocks/service.go.tmp
|
||||
mv ./mqtt/mocks/events.go ./mqtt/mocks/events.go.tmp
|
||||
mv ./readers/mocks/messages.go ./readers/mocks/messages.go.tmp
|
||||
mv ./pkg/sdk/mocks/sdk.go ./pkg/sdk/mocks/sdk.go.tmp
|
||||
mv ./pkg/messaging/mocks/pubsub.go ./pkg/messaging/mocks/pubsub.go.tmp
|
||||
mv ./pkg/authn/mocks/authn.go ./pkg/authn/mocks/authn.go.tmp
|
||||
mv ./pkg/roles/mocks/rolesRepo.go ./pkg/roles/mocks/rolesRepo.go.tmp
|
||||
mv ./pkg/roles/mocks/provisioner.go ./pkg/roles/mocks/provisioner.go.tmp
|
||||
mv ./pkg/roles/mocks/rolemanager.go ./pkg/roles/mocks/rolemanager.go.tmp
|
||||
mv ./pkg/oauth2/mocks/provider.go ./pkg/oauth2/mocks/provider.go.tmp
|
||||
mv ./pkg/authz/mocks/authz.go ./pkg/authz/mocks/authz.go.tmp
|
||||
mv ./pkg/events/mocks/subscriber.go ./pkg/events/mocks/subscriber.go.tmp
|
||||
mv ./pkg/events/mocks/publisher.go ./pkg/events/mocks/publisher.go.tmp
|
||||
mv ./pkg/policies/mocks/evaluator.go ./pkg/policies/mocks/evaluator.go.tmp
|
||||
mv ./pkg/policies/mocks/service.go ./pkg/policies/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 ./invitations/mocks/repository.go " ./invitations/mocks/repository.go"
|
||||
check_mock_changes ./invitations/mocks/service.go " ./invitations/mocks/service.go"
|
||||
check_mock_changes ./auth/mocks/token_client.go " ./auth/mocks/token_client.go"
|
||||
check_mock_changes ./auth/mocks/authz.go " ./auth/mocks/authz.go"
|
||||
check_mock_changes ./auth/mocks/keys.go " ./auth/mocks/keys.go"
|
||||
check_mock_changes ./auth/mocks/service.go " ./auth/mocks/service.go"
|
||||
check_mock_changes ./bootstrap/mocks/configs.go " ./bootstrap/mocks/configs.go"
|
||||
check_mock_changes ./bootstrap/mocks/config_reader.go " ./bootstrap/mocks/config_reader.go"
|
||||
check_mock_changes ./bootstrap/mocks/service.go " ./bootstrap/mocks/service.go"
|
||||
check_mock_changes ./domains/mocks/domains_client.go " ./domains/mocks/domains_client.go"
|
||||
check_mock_changes ./domains/mocks/repository.go " ./domains/mocks/repository.go"
|
||||
check_mock_changes ./domains/mocks/service.go " ./domains/mocks/service.go"
|
||||
check_mock_changes ./channels/mocks/repository.go " ./channels/mocks/repository.go"
|
||||
check_mock_changes ./channels/mocks/channels_client.go " ./channels/mocks/channels_client.go"
|
||||
check_mock_changes ./channels/mocks/service.go " ./channels/mocks/service.go"
|
||||
check_mock_changes ./groups/private/mocks/service.go " ./groups/private/mocks/service.go"
|
||||
check_mock_changes ./groups/mocks/repository.go " ./groups/mocks/repository.go"
|
||||
check_mock_changes ./groups/mocks/groups_client.go " ./groups/mocks/groups_client.go"
|
||||
check_mock_changes ./groups/mocks/service.go " ./groups/mocks/service.go"
|
||||
check_mock_changes ./users/mocks/hasher.go " ./users/mocks/hasher.go"
|
||||
check_mock_changes ./users/mocks/emailer.go " ./users/mocks/emailer.go"
|
||||
check_mock_changes ./users/mocks/repository.go " ./users/mocks/repository.go"
|
||||
check_mock_changes ./users/mocks/service.go " ./users/mocks/service.go"
|
||||
check_mock_changes ./journal/mocks/repository.go " ./journal/mocks/repository.go"
|
||||
check_mock_changes ./journal/mocks/service.go " ./journal/mocks/service.go"
|
||||
check_mock_changes ./consumers/notifiers/mocks/notifier.go " ./consumers/notifiers/mocks/notifier.go"
|
||||
check_mock_changes ./consumers/notifiers/mocks/repository.go " ./consumers/notifiers/mocks/repository.go"
|
||||
check_mock_changes ./consumers/notifiers/mocks/service.go " ./consumers/notifiers/mocks/service.go"
|
||||
check_mock_changes ./certs/mocks/pki.go " ./certs/mocks/pki.go"
|
||||
check_mock_changes ./certs/mocks/service.go " ./certs/mocks/service.go"
|
||||
check_mock_changes ./provision/mocks/service.go " ./provision/mocks/service.go"
|
||||
check_mock_changes ./clients/private/mocks/service.go " ./clients/private/mocks/service.go"
|
||||
check_mock_changes ./clients/mocks/repository.go " ./clients/mocks/repository.go"
|
||||
check_mock_changes ./clients/mocks/clients_client.go " ./clients/mocks/clients_client.go"
|
||||
check_mock_changes ./clients/mocks/cache.go " ./clients/mocks/cache.go"
|
||||
check_mock_changes ./clients/mocks/service.go " ./clients/mocks/service.go"
|
||||
check_mock_changes ./mqtt/mocks/events.go " ./mqtt/mocks/events.go"
|
||||
check_mock_changes ./readers/mocks/messages.go " ./readers/mocks/messages.go"
|
||||
check_mock_changes ./pkg/sdk/mocks/sdk.go " ./pkg/sdk/mocks/sdk.go"
|
||||
check_mock_changes ./pkg/messaging/mocks/pubsub.go " ./pkg/messaging/mocks/pubsub.go"
|
||||
check_mock_changes ./pkg/authn/mocks/authn.go " ./pkg/authn/mocks/authn.go"
|
||||
check_mock_changes ./pkg/roles/mocks/rolesRepo.go " ./pkg/roles/mocks/rolesRepo.go"
|
||||
check_mock_changes ./pkg/roles/mocks/provisioner.go " ./pkg/roles/mocks/provisioner.go"
|
||||
check_mock_changes ./pkg/roles/mocks/rolemanager.go " ./pkg/roles/mocks/rolemanager.go"
|
||||
check_mock_changes ./pkg/oauth2/mocks/provider.go " ./pkg/oauth2/mocks/provider.go"
|
||||
check_mock_changes ./pkg/authz/mocks/authz.go " ./pkg/authz/mocks/authz.go"
|
||||
check_mock_changes ./pkg/events/mocks/subscriber.go " ./pkg/events/mocks/subscriber.go"
|
||||
check_mock_changes ./pkg/events/mocks/publisher.go " ./pkg/events/mocks/publisher.go"
|
||||
check_mock_changes ./pkg/policies/mocks/evaluator.go " ./pkg/policies/mocks/evaluator.go"
|
||||
check_mock_changes ./pkg/policies/mocks/service.go " ./pkg/policies/mocks/service.go"
|
||||
@@ -1,40 +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=""
|
||||
for file in $(grep -rl --exclude-dir={.git,build,**vernemq**} \
|
||||
--exclude=\*.{crt,key,pem,zed,hcl,md,json,csv,mod,sum,tmpl,args} \
|
||||
--exclude={CODEOWNERS,LICENSE,MAINTAINERS} \
|
||||
.); do
|
||||
|
||||
if ! head -n 5 "$file" | grep -q "Copyright (c) Abstract Machines"; then
|
||||
CHECK="$CHECK $file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$CHECK" ]; then
|
||||
echo "License header check failed. Fix the following files:"
|
||||
echo "$CHECK"
|
||||
exit 1
|
||||
else
|
||||
echo "All files have the correct license header!"
|
||||
fi
|
||||
@@ -1,31 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
swagger-ui:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Swagger UI action
|
||||
id: swagger-ui-action
|
||||
uses: blokovi/swagger-ui-action@main
|
||||
with:
|
||||
dir: "./api/openapi"
|
||||
pattern: "*.yml"
|
||||
debug: "true"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: docs.api.magistrala.abstractmachines.fr
|
||||
@@ -1,390 +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@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Install protolint
|
||||
run: |
|
||||
go install github.com/yoheimuta/protolint/cmd/protolint@latest
|
||||
|
||||
- name: Lint Protobuf Files
|
||||
run: |
|
||||
protolint .
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.60.3
|
||||
args: --config ./tools/config/golangci.yml
|
||||
|
||||
- name: Build all Binaries
|
||||
run: |
|
||||
make all -j $(nproc)
|
||||
|
||||
- name: Compile check for rabbitmq
|
||||
run: |
|
||||
SMQ_MESSAGE_BROKER_TYPE=rabbitmq make mqtt
|
||||
|
||||
- name: Compile check for redis
|
||||
run: |
|
||||
SMQ_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@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v3
|
||||
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/**"
|
||||
|
||||
cli:
|
||||
- "cli/**"
|
||||
- "cmd/cli/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
coap:
|
||||
- "coap/**"
|
||||
- "cmd/coap/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
consumers:
|
||||
- "consumers/**"
|
||||
- "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/**"
|
||||
|
||||
journal:
|
||||
- "journal/**"
|
||||
- "cmd/journal/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
http:
|
||||
- "http/**"
|
||||
- "cmd/http/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
- "logger/**"
|
||||
|
||||
internal:
|
||||
- "internal/**"
|
||||
|
||||
invitations:
|
||||
- "invitations/**"
|
||||
- "cmd/invitations/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
logger:
|
||||
- "logger/**"
|
||||
|
||||
mqtt:
|
||||
- "mqtt/**"
|
||||
- "cmd/mqtt/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
- "logger/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
pkg-errors:
|
||||
- "pkg/errors/**"
|
||||
|
||||
pkg-events:
|
||||
- "pkg/events/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
pkg-grpcclient:
|
||||
- "pkg/grpcclient/**"
|
||||
|
||||
pkg-messaging:
|
||||
- "pkg/messaging/**"
|
||||
|
||||
pkg-sdk:
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/errors/**"
|
||||
- "pkg/groups/**"
|
||||
- "auth/**"
|
||||
- "bootstrap/**"
|
||||
- "certs/**"
|
||||
- "consumers/**"
|
||||
- "http/**"
|
||||
- "internal/*"
|
||||
- "internal/api/**"
|
||||
- "internal/apiutil/**"
|
||||
- "internal/groups/**"
|
||||
- "invitations/**"
|
||||
- "provision/**"
|
||||
- "readers/**"
|
||||
- "clients/**"
|
||||
- "users/**"
|
||||
|
||||
pkg-transformers:
|
||||
- "pkg/transformers/**"
|
||||
|
||||
pkg-ulid:
|
||||
- "pkg/ulid/**"
|
||||
|
||||
pkg-uuid:
|
||||
- "pkg/uuid/**"
|
||||
|
||||
provision:
|
||||
- "provision/**"
|
||||
- "cmd/provision/**"
|
||||
- "logger/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
readers:
|
||||
- "readers/**"
|
||||
- "cmd/postgres-reader/**"
|
||||
- "cmd/timescale-reader/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "auth/**"
|
||||
|
||||
clients:
|
||||
- "clients/**"
|
||||
- "cmd/clients/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
users:
|
||||
- "users/**"
|
||||
- "cmd/users/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
ws:
|
||||
- "ws/**"
|
||||
- "cmd/ws/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
- name: Create coverage directory
|
||||
run: |
|
||||
mkdir coverage
|
||||
|
||||
- name: Run Journal tests
|
||||
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/journal.out ./journal/...
|
||||
|
||||
- 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 cli tests
|
||||
if: steps.changes.outputs.cli == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/cli.out ./cli/...
|
||||
|
||||
- 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 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 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 grpcclient tests
|
||||
if: steps.changes.outputs.pkg-grpcclient == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-grpcclient.out ./pkg/grpcclient/...
|
||||
|
||||
- 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 clients tests
|
||||
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/clients.out ./clients/...
|
||||
|
||||
- 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@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
files: ./coverage/*.out
|
||||
codecov_yml_path: tools/config/codecov.yml
|
||||
verbose: true
|
||||
+4
-16
@@ -1,20 +1,8 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# build dirs
|
||||
# Set your private global .gitignore:
|
||||
# https://digitalfortress.tech/tricks/creating-a-global-gitignore/
|
||||
|
||||
build
|
||||
|
||||
# tools
|
||||
tools/e2e/e2e
|
||||
tools/mqtt-bench/mqtt-bench
|
||||
tools/provision/provision
|
||||
tools/provision/mgconn.toml
|
||||
|
||||
# coverage files
|
||||
coverage
|
||||
|
||||
# Schemathesis
|
||||
.hypothesis
|
||||
|
||||
# Ignore Vault data directory as it contains runtime-generated data
|
||||
docker/addons/vault/data/
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
# Adopters
|
||||
|
||||
As SuperMQ Community grows, we'd like to keep track of SuperMQ 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 SuperMQ 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 SuperMQ, 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 SuperMQ 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 SuperMQ:**
|
||||
|
||||
**Note**: Several other organizations/users couldn't publicly share their usage details but are active project contributors and SuperMQ 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/supermq/blob/main/MAINTAINERS) and you will be permanently removed from the list.
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
# Mainflux Changelog
|
||||
|
||||
## Generation
|
||||
Mainflux release notes for the latest release can be obtained via:
|
||||
```
|
||||
make changelog
|
||||
```
|
||||
|
||||
Otherwise, whole log in a similar format can be observed via:
|
||||
```
|
||||
git log --pretty=oneline --abbrev-commit
|
||||
```
|
||||
|
||||
## 0.11.0 - 29. MAY 2020.
|
||||
### Features and Bugfixes
|
||||
- Add VerneMQ docker image build from source (#1178)
|
||||
- MF-994 - Add tracing middleware for twins and states repos (#1181)
|
||||
- MF-995 - Add Twins tests for endpoint list twins and list states (#1174)
|
||||
- NOISSUE - Update dependencies (#1176)
|
||||
- MF-1163 - Fix influxdb-reader to use nanoseconds precision (#1171)
|
||||
- Rename environment variable MF_MQTT_ADAPTER_PORT to MF_MQTT_ADAPTER_MQTT_PORT in docker environment (#1170)
|
||||
- Remove thing related code from twins service (#1169)
|
||||
- MF-997 - Add twins service swagger file (#1167)
|
||||
- MF-1079 - Add MQTT forwarder (#1164)
|
||||
- MF-1159 - add gateway metadata update in provision method (#1160)
|
||||
- MF-1055 - rollback/release transaction on error (#1166)
|
||||
- NOISSUE - Use log level error for VermeMQ docker (#1162)
|
||||
- NOISSUE - Fix default nats pubsub subject (#1153)
|
||||
- MF-1125 - Document Provision service (#1143)
|
||||
- NOISSUE - Fix bootstrap SDK args naming (#1151)
|
||||
- Use VerneMQ default log level (#1150)
|
||||
- NOISSUE - Update provision service (#1133)
|
||||
- NOISSUE - Refactor messaging (#1141)
|
||||
- Add JSON tags to SDK entities (#1146)
|
||||
- NOISSUE - Update CLI README.md (#1139)
|
||||
- NOISSUE - Update mProxy version (#1137)
|
||||
- fix nginx, channel connect (#1136)
|
||||
- Remove concurrency flag for golangci-lint (#1134)
|
||||
- MF-1088 - Remove message payload content type (#1121)
|
||||
- MF-1129 - Use snake_case for Lora and OPC-UA metadata fields (#1130)
|
||||
- MF-1128 - Add golangci-linter to a CI script (#1131)
|
||||
- MF-1123 - Move Provision service to monorepo (#1132)
|
||||
- MF-845 - Add FOSSA badge for licensing (#1127)
|
||||
- MF-1087 - Remove WebSocket adapter (#1120)
|
||||
- NOISSUE - Use HTTP Status in SDK error messages (#1119)
|
||||
- NOISSUE - Fix bootstrap token naming and interfaces named args (#1117)
|
||||
- MF-1115 - Improve the SDK error encoding (#1118)
|
||||
- MF-862 - Add boostrap CRUD to SDK and CLI (#1114)
|
||||
- NOISSUE - Update coding style in Things service (#1116)
|
||||
- NOISSUE - Remove defers from TestMain (#1111)
|
||||
- NOISSUE - Create func to encode SDK errors (#1110)
|
||||
- MF-1078 - Add timestamp to published messages and use it in Transformer (#1106)
|
||||
- Fix prometheus namespace in postgres reader & writer (#1109)
|
||||
- NOISSUE - Implement errors package in senml transformer, readers and writers (#1108)
|
||||
- NOISSUE - Implement errors package in Authentication service (#1105)
|
||||
- MF-1103 - API key should ignore empty expiration time (#1104)
|
||||
- MF-1096 - Fix AuthN and Things Auth ENVARS (#1066)
|
||||
- fix Contains function for nil arguments (#1102)
|
||||
- MF-1099 - Add email subdomain validator (#1101)
|
||||
- MF-1091 - Use channels. as broker prefix (#1098)
|
||||
- MF-1090 - Use named Interfaces args (#1097)
|
||||
- NOISSUE - Create broker package for NATS (#1080)
|
||||
- NOISSUE - Implement errors package in bootstrap service (#1093)
|
||||
- NOISSUE - Fix writers loadSubjectsConfig if file is missing (#1094)
|
||||
- NOISSUE - Adding subtopics filtering in writer services (#1072)
|
||||
- NOISSUE - Improve errors package (#1086)
|
||||
- NOISSUE - Enable MQTT over WS in docker composition (#1085)
|
||||
- NOISSUE - Rm unused opc-ua envars (#1083)
|
||||
- MF-798 - Add utf8 support for email validation (#1082)
|
||||
- Remove unused Tokenizer interface (#1084)
|
||||
- Update mqtt adapter imports (#1081)
|
||||
- NOISSUE - Update state based on SenML time value (#1075)
|
||||
- NOISSUE - Fix StatusBadDecodingError for opc-ua browse (#1074)
|
||||
- Save senml array msg to multiple states (#1073)
|
||||
- NOISSUE - Fix opc-ua message type handling (#1071)
|
||||
- NOISSUE - Add Publisher field to MQTT adapter (#1067)
|
||||
- NOISSUE - Fix users CLI (#1062)
|
||||
- NOISSUE - Fix SDK Messages response (#1064)
|
||||
- Merged MQTT docker compose in core composition file (#1060)
|
||||
- MF-1016 - Add UserUpdate and UpdatePassword to sdk and CLI (#1057)
|
||||
- Update mProxy (#1058)
|
||||
- MF-1053 - Add disconnect event to MQTT adapter (#1056)
|
||||
- Fix data type for data_value in databases (#1054)
|
||||
- NOISSUE - Fix opc-ua subscriptions store (#1052)
|
||||
- NOISSUE - Fix connect CLI command and remove ConnectThing func from SDK (#1051)
|
||||
- NOISSUE - Update Vernemq image repository (#1050)
|
||||
- Removed VerneMQ auth plugin, Aedes impl. Added mproxy support in docker (#1049)
|
||||
- NOISSUE - Add default subscription nodeID and Interval ENVAR (#1046)
|
||||
- MF-415 - Merge mProxy support (#1045)
|
||||
- NOISSUE - Remove twins-service mqtt dependency and publish notifs to nats (#1042)
|
||||
- Add arbitrary SenML value type saving to twin state (#1039)
|
||||
- Fixed Aedes dependencies (#1036)
|
||||
- MF-998 - Add Twins service to Makefile and docker-compose.yml (#1035)
|
||||
- MF-1032 - Fix redis docker volume of opcua-adapter (#1033)
|
||||
- NOISSUE - add nats conf (#1031)
|
||||
- MF-442 - Add SSL encryption to the MongoDB, InfluxDB and Cassanda readers (#1024)
|
||||
- NOISSUE - Add opc-ua type handling and unsubscription (#1029)
|
||||
- NOISSUE - Add aggregate attribute-based search for twin retrieval (#1027)
|
||||
- NOISSUE - Fix metadata in add Things endpoint (#1028)
|
||||
- NOISSUE - Fix minimal password length (#1023)
|
||||
- MF-1020 - Change default password for CLI provision test (#1021)
|
||||
- NOISSUE - Add subtopic to opcua messages (#1022)
|
||||
- NOISSUE - Add details to browsed OPC-UA nodes (#1019)
|
||||
- NOISSUE Fix obsolete attribute persistance (#1018)
|
||||
- Fix twins update revision counter (#1011)
|
||||
- Fixed docs instructions in README (#1010)
|
||||
- Fix copyright year (#1009)
|
||||
- Fix issuing recovery key (#1007)
|
||||
- Removed gatling load-test (#1005)
|
||||
- Removed old k8s manifests (#1004)
|
||||
- NOISSUE - Remove UI from docker-compose (#1001)
|
||||
- NOISSUE - Store successfull OPC-UA subscriptions (#999)
|
||||
- MF-730 - Add digital twin service for things (#855)
|
||||
- Fix Redis event naming (#996)
|
||||
- NOISSUE - Add a Browse endpoint in opcua-adapter (#988)
|
||||
- NOISSUE - Add Redis ES Username/Pass for VerneMQ (#991)
|
||||
- MF-982 - Add error when connecting empty channels or things (#985)
|
||||
|
||||
## 0.10.0 - 17. DEC 2019.
|
||||
### Features
|
||||
- MF-932 - User API keys (#941)
|
||||
- NOISSUE - Use opcua server timestamp in opcua-adapter messages (#980)
|
||||
- Simplify CI script (#979)
|
||||
- NOISSUE - Add opcua-adapter conn route-map, use ServerURI and NodeID (#975)
|
||||
- Move docs to a separate repo (#976)
|
||||
- NOISSUE - Support multiple types values in opcua-adapter (#973)
|
||||
- Migrate from dep to go modules (#971)
|
||||
- NOISSUE - Add Node IdentifierType config in opcua-adapter (#967)
|
||||
- NOISSUE - Remove messages limit in influxdb-reader (#968)
|
||||
- MF-898 - Add bulk connect to CLI and SDK (#956)
|
||||
- MF-538 - Improve logging and API errors (#866)
|
||||
- NOISSUE - Remove Elm UI (#953)
|
||||
- MF-898 - Add bulk connections endpoint (#948)
|
||||
- MF-898 - Change thing's service to use bulk connect (#946)
|
||||
- MF-898 - Add transactions to postgres connect (#940)
|
||||
- Add missing user service tests (#945)
|
||||
- Remove Normalizer service from compose (#937)
|
||||
- MF-919 - Mainflux message updates (#924)
|
||||
- NOISSUE - Remove ARM multi-arch images (#929)
|
||||
- MF-906 - Change single creation endpoints to use bulk service calls (#927)
|
||||
- MF-922 - Add UpdateUser endpoint (#923)
|
||||
- MF-780 - Use Normalizer as a lib (#915)
|
||||
- NOISSUE - Switch to grpcbox for VerneMQ (#914)
|
||||
- Change channels to chs (#918)
|
||||
- MF-484 - Add bulk provisioning for things and channels (#889)
|
||||
- MF-899 - Update README and official docs (#910)
|
||||
- NOISSUE - Fix Redis envars (#903)
|
||||
- Add disconnect on gen_server terminate() (#913)
|
||||
- MF-890 - Add OPC-UA docs (#904)
|
||||
- NOISSUE - Update Protobuf version (#902)
|
||||
- MF-886 - Add OPC-UA adapter (#878)
|
||||
- MF-532 - Password reset (#873)
|
||||
- MF-785 - Change CanAccess to CanAccessByKey (#894)
|
||||
- NOISSUE - Add MQTT UserName check on register and InstanceId in Redis (#884)
|
||||
- Add MQTT troubleshooting section (#882)
|
||||
- MF-875 - Add tracing to official documentation (#877)
|
||||
- MF-788 - Remove date and minimize copyright comments (#876)
|
||||
- MF-787 - Add tags to user, thing, and channel spans (#869)
|
||||
- Update docker-compose version for addons (#874)
|
||||
- MF-859 - Channels metadata search (#867)
|
||||
- MF-858 Users metadata (#861)
|
||||
- NOISSUE - Simplify MQTT benchmarking tool (#852)
|
||||
- NOISSUE - Upgrade Go version to 1.13 in container images (#868)
|
||||
- MF-820 - Fetch messages for a particular device (#843)
|
||||
- Update gorilla websocket version (#865)
|
||||
- NOISSUE - Update aedes version and fix Dockerfile (#863)
|
||||
- NOISSUE - Search by metadata (#849)
|
||||
- MF-846 - Install python in docker build for aedes mqtt image (#860)
|
||||
- NOISSUE - Clean NginX files, move .gitignores to dirs (#853)
|
||||
- NOISSUE - Add docker-compose for MQTT cluster (#841)
|
||||
- Add debug logs to the WS adapter (#848)
|
||||
- NOISSUE - Add measuring time from pub to sub (#839)
|
||||
- NOISSUE - update mqtt prov tool and some refactor (#831)
|
||||
- NOISSUE - Use Thing ID to update certs data (#827)
|
||||
- NOISSUE - Improve VerneMQ plugin code, add configurable gRPC pool size (#836)
|
||||
- NOISSUE - Use gRPC for VerneMQ (#835)
|
||||
- Switch secure of WS connection according to secure of http connection of UI (#829)
|
||||
- NOISSUE - Use current hostname instead of localhost for a WebSocket connection in the UI (#826)
|
||||
- NOISSUE - Improve MQTT benchmarking tools (#828)
|
||||
- NOISSUE - update mqtt benchmark (#824)
|
||||
- Add encryption key to env vars table (#823)
|
||||
- NOISSUE - Add version endpoint to MQTT adapter (#816)
|
||||
- MF-295 add mqtt benchmark tool (#817)
|
||||
- update mqtts commands (#815)
|
||||
- NOISSUE - Support encrypted bootstrap (#796)
|
||||
- Add config to writers docs (#812)
|
||||
- NOISSUE - Add VerneMQ support (#809)
|
||||
- NOISSUE - Add content type as part of MQTT subscription topic (#810)
|
||||
|
||||
### Bugfixes
|
||||
- Fix MQTT protobuf filename(#981)
|
||||
- MF-950 - Runtime error in normalizer - CBOR SenML (#974)
|
||||
- NOISSUE - Fix opcua-adapter events warnings (#965)
|
||||
- NOISSUE - Fix opcua-adapter events decode (#951)
|
||||
- Fix subtopic handling in VerneMQ (#962)
|
||||
- NOISSUE - Fix Update User (#959)
|
||||
- NOISSUE - Fix make dockers (#957)
|
||||
- Add dev_ back to make dockers_dev (#955)
|
||||
- NOISSUE - Fix docs (#952)
|
||||
- MF-916 - Fix Things and Channels counters (#947)
|
||||
- MF-942 - Fix email template logic (#944)
|
||||
- NOISSUE - Fix HTTP header for Things and Channels creation (#939)
|
||||
- NOISSUE - Fix docker ui image name (#938)
|
||||
- NOISSUE - Fix lora-adapter (#936)
|
||||
- NOISSUE - Fix lora creation events (#933)
|
||||
- Fix doc for ENV vars in README (#920)
|
||||
- Fix compilation (#911)
|
||||
- Revert "NOISSUE - Make event sourcing optional (#907)" (#909)
|
||||
- NOISSUE - Make event sourcing optional (#907)
|
||||
- NOISSUE - Fix InfluxDB env vars (#908)
|
||||
- Fix Elm version for ARM Docker images (#905)
|
||||
- Fix Elm version in Dockerfile (#901)
|
||||
- NOISSUE - fix security doc (#897)
|
||||
- NOISSUE - Fix typo in docs and README (#891)
|
||||
- Fix Nginx mTLS configuration (#885)
|
||||
- Fix provision tool connect error handling (#879)
|
||||
- Fix: Correct 404 and Content-Type Issues in MQTT Version Endpoint (#837)
|
||||
- NOISSUE - Fix proto files in VerneMQ (#834)
|
||||
- NOISSUE - Fix hackney HTTP request (#833)
|
||||
- Add socket pool and fix pattern matching (#830)
|
||||
- Fix typo (#814)
|
||||
|
||||
## 0.9.0 - 19. JUL 2019.
|
||||
### Features
|
||||
- Create and push docker manifest for new release from Makefile (#794)
|
||||
- MF-399 - Add open tracing support (#782)
|
||||
- MF-783 - Allow access checking by a thing ID (#784)
|
||||
- NOISSUE - Add authorization HTTP API to things service (#772)
|
||||
- Remove cli executable from repo (#776)
|
||||
- NOISSUE - Use .env vars in docker-compose (#770)
|
||||
- MF-663 - enable nginx port conf from docker env (#769)
|
||||
- Update docs (#766)
|
||||
- NOISSUE - Remove installing non-existent package in ci (#758)
|
||||
- NOISSUE - Add searchable Channels name (#754)
|
||||
- MF-466 - ARM docker deployment (#756)
|
||||
- Add missing Websocket.js into docker ui image (#755)
|
||||
- NOISSUE - Add searchable Things name (#750)
|
||||
- NOISSUE - Add certificate fields to the Bootstrap service (#752)
|
||||
- Update grpc and protobuf deps in mqtt adapter (#751)
|
||||
- MF-742 - Things to support single user scenario (#749)
|
||||
- MF-732 - Add Postgres reader (#740)
|
||||
- MF-722 - Change UUID lib (#746)
|
||||
- Add performance improvement to writer filtering (#744)
|
||||
- NOISSUE - Update nginx version (#748)
|
||||
- MF-574 - Add missing environment variables to Cassandra writer (#745)
|
||||
- NOISSUE - Add compile test to CI (#743)
|
||||
- MF-708 - Assign Writer(s) to a channel (#737)
|
||||
- MF-732 - Add PostgreSQL writer (#733)
|
||||
- NOISSUE - Add readers pagination in SDK (#736)
|
||||
- Add UI websocket open/close and send/receive (#728)
|
||||
- MF-707 - Allow custom Thing key (#726)
|
||||
- MF-525 - Add pagination response to the readers (#729)
|
||||
- NOISSUE - Rm Things type from lora-adapter (#727)
|
||||
- skip deleting of persistent volumes by default (#723)
|
||||
- MF-488 - Remove Thing type (app or device) (#718)
|
||||
- Remove empty channels check (#720)
|
||||
- MF-655 Proper usage of docker volumes (#657)
|
||||
- NOISSUE - Improve UI styling (#719)
|
||||
- MF-715 - Conflict on updating connection with a valid list of channels (#716)
|
||||
- MF-711 - Create separate Redis instance for ES (#717)
|
||||
- NOISSUE - Update event fields naming (#713)
|
||||
- MF-698 - Add missing info and docs about sys event sourcing (#712)
|
||||
- MF-549 - Change metadata format from JSON string to JSON object (#706)
|
||||
- NOISSUE - Replace repeating code by card gen func (#697)
|
||||
- Update Bootstrap service docker-compose.yml (#700)
|
||||
- Remove Debug function (#699)
|
||||
- MF-687 - Add event sourcing to Bootstrap service (#695)
|
||||
- NOISSUE - Remove debugging message from response of handle error function (#696)
|
||||
- Add event stream to MQTT adapter for conn status (#692)
|
||||
- NOISSUE - Improve UI style (#691)
|
||||
- Update docs structure (#686)
|
||||
- Use images instead of carousel (#685)
|
||||
- NOISSUE - Update docs (#683)
|
||||
- MF-662 - Change menu style (#678)
|
||||
- MF-651 - X509 Mutual TLS authentication (#676)
|
||||
- Update Aedes version for MQTT adapter (#677)
|
||||
- MF-661 - Bootstrap pagination in UI (#672)
|
||||
- Update subtopics section in documentation (#670)
|
||||
- Remove default base URL value (#671)
|
||||
|
||||
### Bugfixes
|
||||
- NOISSUE - Fix Readers logs (#735)
|
||||
- NOISSUE - Fix Docker for ARM (#760)
|
||||
- NOISSUE - Fix count when search by name is performed (#767)
|
||||
- NOISSUE - Typo fix (#777)
|
||||
- NOISSUE - Fix Postgres logs in Things service (#734)
|
||||
- Fix CI with fixed plugin versions (#747)
|
||||
- fix building problems (#741)
|
||||
- fix docker-compose env (#775)
|
||||
- Fix MF_THINGS_AUTH_GRPC_PORT in addons' docker-compose files (#781)
|
||||
- Fix MQTT raw message deserialization (#753)
|
||||
- fix variant option for manifest annotate (#765)
|
||||
- fix to makefile for OSX/Darwin (#724)
|
||||
- Fix .dockerignore file by removing index.html (#725)
|
||||
- Fix things and channels metadata create and edit & remove thing type (#721)
|
||||
- Fix Bootstrap service event map keys (#705)
|
||||
- Fix logging in publish event callback (#694)
|
||||
- Fix InfluxDB time bug (#689)
|
||||
- Fix users service to work in offline mode (#795)
|
||||
- fix mainflux_id parameter in bootstrap swagger (#789)
|
||||
- Fix offset calculation after deleting thing/channel, not to go to negative offset after deleting last thing/channel (#679)
|
||||
- Use errors and null packets in authorized pub/sub (#773)
|
||||
- NOISSUE - Fix CoAP adapter (#779)
|
||||
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/10?closed=1
|
||||
|
||||
## 0.8.0 - 20. MAR 2019.
|
||||
### Features
|
||||
- MF-571 - Add Env.elm to set custom base URL (#654)
|
||||
- NOISSUE Added docs about docker-compose config overriding (#653)
|
||||
- MF-539 - Improve Bootstrap Service documentation (#646)
|
||||
- MF-596 - Add subtopic to RawMessage (#642)
|
||||
- NOISSUE - Prevent infinite loop in lora-adapter if Redis init fail (#647)
|
||||
- Corrected grammar and rephrased a few sentences to read nicely (#641)
|
||||
- MF-571 - Elm UI (#632)
|
||||
- MF-552 - Use event sourcing to keep Bootstrap service in sync with Things service (#603)
|
||||
- MF-540 - Add pagination in API responses for Bootstrap service (#575)
|
||||
- MF-600 - Handle custom LoRa Server application decoder (#608)
|
||||
- update docker-compose (#590)
|
||||
- Update generated code (#602)
|
||||
- Add generated files check (#601)
|
||||
- MF-597 - Removed legacy code as not needed anymore (#598)
|
||||
- NOISSUE - Added normalizer service to run script (#594)
|
||||
- Changed RawMessage (#587)
|
||||
- NOISSUE - fix CLI log (#581)
|
||||
- MF-519 - Refine Message (#567)
|
||||
- NOISSUE - Add name field for Bootstrap Config (#564)
|
||||
- Fix non-SenML message routing in normalizer (#573)
|
||||
- NOISSUE - Update authors list (#569)
|
||||
- Update lora.md (#568)
|
||||
- NOISSUE- Improve LoRa doc (#562)
|
||||
- MF-551 - Add metadata fields to Bootstrap Channels (#563)
|
||||
- Fix MQTT adapter by setting subscription queue (#561)
|
||||
- MF-558 - Add MQTT subtopics documentation (#559)
|
||||
- Fix regexp for SUB (#557)
|
||||
- Simplify MQTT topipc regexp (#555)
|
||||
- MF-429 -Enabled MQTT subtopic's (#554)
|
||||
- Add env var for number of concurrent messages (#545)
|
||||
- NOISSUE - Update doc and fix empty key bug (#544)
|
||||
- MF-370 - Simplify and refine CI (#541)
|
||||
- NOISSUE - Add connection commands to CLI (#542)
|
||||
- NOISSUE - Refine docs (#537)
|
||||
- Update licnese year (#533)
|
||||
- MF-513 - Add Bootstrapping service (#524)
|
||||
- Add dedicated env vars for event sourcing (#536)
|
||||
- NOISSUE - Fix docs (#535)
|
||||
- Add lora doc to getting-started.md (#529)
|
||||
- MF-483 - Enable channels and devices corresponding lists in backend (#520)
|
||||
- Add missing components doc to architecture.md (#531)
|
||||
|
||||
### Bugfixes
|
||||
- MF-639 Split Content-Type header field on semicolon and evaluate all substrings (#644)
|
||||
- MF-656 - Change bootstrap service port to 8200 (#658)
|
||||
- Replace crossOrigin with relative path and fix messaging bug (#645)
|
||||
- MF-579 Things & Channels returns 404 when not found or ID is malformed, not 500 (#633)
|
||||
- Fix run command in dev guide (#605)
|
||||
- MF-583 - Correct cmd/mongodb-reader HTTPServer log Info (#584)
|
||||
- Fix Dusan Maldenovic GitHub (#570)
|
||||
- Fix CLI docs (#566)
|
||||
- Fix pagination response for empty page (#547)
|
||||
- Fix swagger and provisioning docs (#546)
|
||||
- NOISSUE - Fix event sourcing client on LoRa adapter (#527)
|
||||
- Fix MQTT adapter scaling issue (#526)
|
||||
- NOISSUE - Fix subtopic regex and restrict empty subtopic parts (#659)
|
||||
- Fix missing css in container ui (#638)
|
||||
- NOISSUE - Fix lora-adapter Object decode (#610)
|
||||
- NOISSUE - Fix users logs in main.go (#577)
|
||||
- NOISSUE - Fix normalizer exposed port in docker-compose (#548)
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/9?closed=1
|
||||
|
||||
|
||||
## 0.7.0 - 08. DEC 2018.
|
||||
### Features
|
||||
|
||||
- MF-486 - Add provisioning command to CLI (#487)
|
||||
- Fix lora-adapter event store handlers (#492)
|
||||
- NOISSUE - Add LoRa route map validation and fix LoRa messages URL (#491)
|
||||
- MF-475 - Replace increment ID with UUID (#490)
|
||||
- MF-166 - Add lora-adapter service (#481)
|
||||
- NOISSUE - Add Makefile target to clean old imgs (#485)
|
||||
- MF-473 - Add metadata field to channel (#476)
|
||||
- Make CoAP ping period configurable (#469)
|
||||
- Add nginx ingress config to k8s services (#472)
|
||||
- Add CoAP section in getting-started (#468)
|
||||
- NOISSUE - Move CLI documentation from getting started guide to separate page (#470)
|
||||
- NOISSUE - Update Getting Started doc with CLI usage (#465)
|
||||
- Update CoAP docs with URL example (#463)
|
||||
- MF-447 - Add event sourcing to things service (#460)
|
||||
- Add TLS support to CoAP adapter and all readers (#459)
|
||||
- MF-417 - Implement SDK tests (#438)
|
||||
- MF-454 - Use message Time field as a time for InfluxDB points (#455)
|
||||
- NOISSUE - Add .dockerignore to project root (#457)
|
||||
- Update docker-compose so that every service has debug log level (#453)
|
||||
- NOISSUE - Add TLS flag for Mainflux services (#452)
|
||||
- MF-448 - Option for Postgres SSL Mode (#449)
|
||||
- MF-443 Update project dependencies (#444)
|
||||
- MF-426 - Add optional MF_CA_CERTS env variable to allow GRPC client to use TLS certs (#430)
|
||||
- Expose the InfluxDB and Cassandra ports to host (#441)
|
||||
- MF-374 - Bring back CoAP adapter (#413)
|
||||
|
||||
### Bugfixes
|
||||
- gRPC Load Balancing between http-adapter and things (#387)
|
||||
- MF-407 - Values of zero are being omitted (#434)
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/8?closed=1
|
||||
|
||||
|
||||
## 0.6.0 - 26. OCT 2018.
|
||||
### Features
|
||||
|
||||
- Added Go SDK (#357)
|
||||
- Updated NATS version (#412)
|
||||
- Added debbug level to MFX logger (#379)
|
||||
- Added Documentation for readers (#389)
|
||||
- Added Redis cache to improve performance (#382)
|
||||
|
||||
|
||||
## 0.5.1 - 05. SEP 2018.
|
||||
### Features
|
||||
- Improve performance by adding Redis cache (#382)
|
||||
|
||||
### Bugfixes
|
||||
- Mixed up name and type of the things (#375)
|
||||
- Fix MQTT topic (#380)
|
||||
|
||||
|
||||
## 0.5.0 - 28. AUG 2018
|
||||
### Features
|
||||
- InfluxDB Reader (#311)
|
||||
- Cassandra Reader (#313)
|
||||
- MongoDB Reader (#344)
|
||||
- MQTT Persistance via Redis (#328)
|
||||
- CLI integrated into monorepo (#216)
|
||||
- Normalizer logging (#333)
|
||||
- WS swagger doc (#337)
|
||||
- Payload renamed to Metadata (#343)
|
||||
- Protobuf files added (#363)
|
||||
- SPDX headers added (#325)
|
||||
|
||||
### Bugfixes
|
||||
- Docker network for InfluxDB (#346)
|
||||
- Vendor correct gRPC version (#340)
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/6?closed=1
|
||||
|
||||
|
||||
## 0.4.0 - 01. JUN 2018.
|
||||
* Integrated MQTT adapter (#165 )
|
||||
* Support for storing messages in MongoDB (#237)
|
||||
* Support for storing messages in InfluxDB (#236)
|
||||
* Use UUID PKs with auto-incremented values (#269 )
|
||||
* Replaced JWT with plain string tokens in things service (#268 )
|
||||
* Emit non-SenML messages (#239 )
|
||||
* Support for Grafana (#296)
|
||||
* Added WS Load test (#299 )
|
||||
|
||||
|
||||
## 0.3.0 - 14. MAY 2018.
|
||||
- CoAP API for message exchange (#186)
|
||||
- Split `manager` service into `clients` and `users` (#266)
|
||||
- Replaced ORM with raw SQL (#265)
|
||||
- Setup Kubernetes (#226, #273)
|
||||
- Fix docker compose (#274)
|
||||
- Integrated `dashflux` into monorepo (#258)
|
||||
- Integrated (*non-compatible*) `mqtt` into monorepo (#260)
|
||||
|
||||
|
||||
## 0.2.3 - 24. APR 2018.
|
||||
- Fix examples in the documentation (#243)
|
||||
- Add service name in info response (#241)
|
||||
- Improve code coverage in WS adapter (#242)
|
||||
|
||||
|
||||
## 0.2.2 - 23. APR 2018.
|
||||
- Setup load testing scenarios (#225)
|
||||
|
||||
|
||||
## 0.2.1 - 22. APR 2018.
|
||||
- Fixed `Content-Type` header checking (#238)
|
||||
|
||||
## 0.2.0 - 18. APR 2018
|
||||
- Protobuf message serialization (#192)
|
||||
- Websocket API for exchanging messages (#188)
|
||||
- Channel & client retrieval paging (#227)
|
||||
- Service instrumentation (#213)
|
||||
- `go-kit` based JSON logger (#212)
|
||||
- Project documentation (#218, #220)
|
||||
- API tests (#211, #224)
|
||||
|
||||
|
||||
## 0.1.2 - 18. MAR 2018.
|
||||
### Bug fixes
|
||||
|
||||
- Fixed go lint warnings (#189)
|
||||
- Compose failing startup (#185)
|
||||
- Added missing service startup messages (#190)
|
||||
+14
-14
@@ -1,11 +1,11 @@
|
||||
# Contributing to SuperMQ
|
||||
# Contributing to Mainflux
|
||||
|
||||
The following is a set of guidelines to contribute to SuperMQ 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 Mainflux and its libraries, which are
|
||||
hosted on the [Mainflux Organization](https://github.com/mainflux) on GitHub.
|
||||
|
||||
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@abstractmachines.fr](mailto:abuse@abstractmachines.fr).
|
||||
[abuse@mainflux.com](mailto:abuse@mainflux.com).
|
||||
|
||||
## Reporting issues
|
||||
|
||||
@@ -13,7 +13,7 @@ Reporting issues are a great way to contribute to the project. We are perpetuall
|
||||
thorough bug report.
|
||||
|
||||
Before raising a new issue, check [our issue
|
||||
list](https://github.com/absmach/supermq/issues) to determine if it already contains the
|
||||
list](https://github.com/mainflux/mainflux/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
|
||||
@@ -41,23 +41,23 @@ To contribute to the project, [fork](https://help.github.com/articles/fork-a-rep
|
||||
clone your fork repository, and configure the remotes:
|
||||
|
||||
```
|
||||
git clone https://github.com/<your-username>/supermq.git
|
||||
cd supermq
|
||||
git remote add upstream https://github.com/absmach/supermq.git
|
||||
git clone https://github.com/<your-username>/mainflux.git
|
||||
cd mainflux
|
||||
git remote add upstream https://github.com/mainflux/mainflux.git
|
||||
```
|
||||
|
||||
If your cloned repository is behind the upstream commits, then get the latest changes from upstream:
|
||||
|
||||
```
|
||||
git checkout main
|
||||
git pull --rebase upstream main
|
||||
git checkout master
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
Create a new topic branch from `main` using the naming convention `SMQ-[issue-number]`
|
||||
Create a new topic branch from `master` using the naming convention `MF-[issue-number]`
|
||||
to help us keep track of your contribution scope:
|
||||
|
||||
```
|
||||
git checkout -b SMQ-[issue-number]
|
||||
git checkout -b MF-[issue-number]
|
||||
```
|
||||
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
@@ -74,13 +74,13 @@ and `user.email` git configs, you can sign your commit automatically with `git c
|
||||
Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```
|
||||
git pull --rebase upstream main
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
Push your topic branch up to your fork:
|
||||
|
||||
```
|
||||
git push origin SMQ-[issue-number]
|
||||
git push origin MF-[issue-number]
|
||||
```
|
||||
|
||||
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2020 SuperMQ
|
||||
Copyright 2015-2020 Mainflux
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+23
-7
@@ -1,4 +1,4 @@
|
||||
# SuperMQ follows the timeless, highly efficient and totally unfair system
|
||||
# Mainflux follows the timeless, highly efficient and totally unfair system
|
||||
# known as [Benevolent dictator for
|
||||
# life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
|
||||
# Drasko DRASKOVIC in the role of BDFL.
|
||||
@@ -6,8 +6,8 @@
|
||||
[bdfl]
|
||||
|
||||
[[drasko]]
|
||||
Name = "Drasko Draskovic"
|
||||
Email = "draasko.draskovic@abstractmachines.fr"
|
||||
Name = "Drasko DRASKOVIC"
|
||||
Email = "drasko@mainflux.com"
|
||||
GitHub = "drasko"
|
||||
|
||||
# However, this role serves only in dead-lock events, or in a special and very rare cases
|
||||
@@ -18,13 +18,29 @@
|
||||
# Maintainers have the special role in the project in managing and accepting PRs,
|
||||
# overall leading the project and making design decisions on the maintained subsystems.
|
||||
#
|
||||
# A reference list of all maintainers of the SuperMQ project.
|
||||
# A reference list of all maintainers of the Mainflux project.
|
||||
|
||||
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||
|
||||
[maintainers]
|
||||
|
||||
[[aleksandar]]
|
||||
Name = "Aleksandar NOVAKOVIC"
|
||||
Email = "aleksandar@mainflux.com"
|
||||
GitHub = "anovakovic01"
|
||||
|
||||
[[dusan]]
|
||||
Name = "Dusan Borovcanin"
|
||||
Email = "dusan.borovcanin@abstractmachines.fr"
|
||||
GitHub = "dborovcanin"
|
||||
Name = "Dusan BOROVCANIN"
|
||||
Email = "dusan@mainflux.com"
|
||||
GitHub = "dusanb94"
|
||||
|
||||
[[manuel]]
|
||||
Name = "Manuel IMPERIALE"
|
||||
Email = "manuel@mainflux.com"
|
||||
GitHub = "manuIO"
|
||||
|
||||
[[nikola]]
|
||||
Name = "Nikola MARCETIC"
|
||||
Email = "nikola@mainflux.com"
|
||||
GitHub = "nmarcetic"
|
||||
|
||||
|
||||
@@ -1,51 +1,18 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
SMQ_DOCKER_IMAGE_NAME_PREFIX ?= supermq
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = auth users clients groups channels domains http coap ws postgres-writer postgres-reader timescale-writer \
|
||||
timescale-reader cli bootstrap mqtt provision certs invitations journal
|
||||
TEST_API_SERVICES = journal auth bootstrap certs http invitations notifiers provision readers clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
|
||||
bootstrap opcua authn twins mqtt provision certs
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
VERSION ?= $(shell git describe --abbrev=0 --tags 2>/dev/null || echo 'unknown')
|
||||
COMMIT ?= $(shell git rev-parse HEAD)
|
||||
TIME ?= $(shell date +%F_%T)
|
||||
USER_REPO ?= $(shell git remote get-url origin | sed -e 's/.*\/\([^/]*\)\/\([^/]*\).*/\1_\2/' )
|
||||
empty:=
|
||||
space:= $(empty) $(empty)
|
||||
# Docker compose project name should follow this guidelines: https://docs.docker.com/compose/reference/#use--p-to-specify-a-project-name
|
||||
DOCKER_PROJECT ?= $(shell echo $(subst $(space),,$(USER_REPO)) | tr -c -s '[:alnum:][=-=]' '_' | tr '[:upper:]' '[:lower:]')
|
||||
DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart
|
||||
DEFAULT_DOCKER_COMPOSE_COMMAND := up
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
MOCKERY_VERSION=v2.43.2
|
||||
INTERNAL_PROTO_GEN_OUT_DIR=internal/grpc
|
||||
INTERNAL_PROTO_DIR=internal/proto
|
||||
INTERNAL_PROTO_FILES := $(shell find $(INTERNAL_PROTO_DIR) -name "*.proto" | sed 's|$(INTERNAL_PROTO_DIR)/||')
|
||||
|
||||
ifneq ($(SMQ_MESSAGE_BROKER_TYPE),)
|
||||
SMQ_MESSAGE_BROKER_TYPE := $(SMQ_MESSAGE_BROKER_TYPE)
|
||||
else
|
||||
SMQ_MESSAGE_BROKER_TYPE=nats
|
||||
endif
|
||||
|
||||
ifneq ($(SMQ_ES_TYPE),)
|
||||
SMQ_ES_TYPE := $(SMQ_ES_TYPE)
|
||||
else
|
||||
SMQ_ES_TYPE=nats
|
||||
endif
|
||||
|
||||
define compile_service
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||
go build -tags $(SMQ_MESSAGE_BROKER_TYPE) --tags $(SMQ_ES_TYPE) -ldflags "-s -w \
|
||||
-X 'github.com/absmach/supermq.BuildTime=$(TIME)' \
|
||||
-X 'github.com/absmach/supermq.Version=$(VERSION)' \
|
||||
-X 'github.com/absmach/supermq.Commit=$(COMMIT)'" \
|
||||
-o ${BUILD_DIR}/$(1) cmd/$(1)/main.go
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -mod=vendor -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
@@ -56,10 +23,7 @@ define make_docker
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg COMMIT=$(COMMIT) \
|
||||
--build-arg TIME=$(TIME) \
|
||||
--tag=$(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
@@ -69,127 +33,37 @@ define make_docker_dev
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(svc) \
|
||||
--tag=$(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
ADDON_SERVICES = bootstrap journal provision certs timescale-reader timescale-writer postgres-reader postgres-writer
|
||||
|
||||
EXTERNAL_SERVICES = vault prometheus
|
||||
|
||||
ifneq ($(filter run%,$(firstword $(MAKECMDGOALS))),)
|
||||
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
DOCKER_COMPOSE_COMMAND := $(if $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
$(eval $(DOCKER_COMPOSE_COMMAND):;@)
|
||||
endif
|
||||
|
||||
ifneq ($(filter run_addons%,$(firstword $(MAKECMDGOALS))),)
|
||||
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
RUN_ADDON_ARGS := $(if $(filter-out $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(filter-out $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)),$(ADDON_SERVICES) $(EXTERNAL_SERVICES))
|
||||
$(eval $(RUN_ADDON_ARGS):;@)
|
||||
endif
|
||||
|
||||
ifneq ("$(wildcard docker/ssl/certs/*-grpc-*)","")
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 1
|
||||
else
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
endif
|
||||
|
||||
FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
|
||||
cleandocker:
|
||||
# Stops containers and removes containers, networks, volumes, and images created by up
|
||||
docker compose -f docker/docker-compose.yml -p $(DOCKER_PROJECT) down --rmi all -v --remove-orphans
|
||||
docker-compose -f docker/docker-compose.yml down --rmi all -v --remove-orphans
|
||||
|
||||
ifdef pv
|
||||
# Remove unused volumes
|
||||
docker volume ls -f name=$(SMQ_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
docker volume ls -f name=$(MF_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
endif
|
||||
|
||||
install:
|
||||
for file in $(BUILD_DIR)/*; do \
|
||||
cp $$file $(GOBIN)/supermq-`basename $$file`; \
|
||||
done
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
mocks:
|
||||
@which mockery > /dev/null || go install github.com/vektra/mockery/v2@$(MOCKERY_VERSION)
|
||||
@unset MOCKERY_VERSION && go generate ./...
|
||||
mockery --config ./tools/config/mockery.yaml
|
||||
|
||||
|
||||
DIRS = consumers readers postgres internal
|
||||
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\|cmd\|middleware')
|
||||
|
||||
define test_api_service
|
||||
$(eval svc=$(subst test_api_,,$(1)))
|
||||
@which st > /dev/null || (echo "schemathesis not found, please install it from https://github.com/schemathesis/schemathesis#getting-started" && exit 1)
|
||||
|
||||
@if [ -z "$(USER_TOKEN)" ]; then \
|
||||
echo "USER_TOKEN is not set"; \
|
||||
echo "Please set it to a valid token"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@if [ "$(svc)" = "http" ] && [ -z "$(CLIENT_SECRET)" ]; then \
|
||||
echo "CLIENT_SECRET is not set"; \
|
||||
echo "Please set it to a valid secret"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@if [ "$(svc)" = "http" ]; then \
|
||||
st run api/openapi/$(svc).yml \
|
||||
--checks all \
|
||||
--base-url $(2) \
|
||||
--header "Authorization: Client $(CLIENT_SECRET)" \
|
||||
--contrib-openapi-formats-uuid \
|
||||
--hypothesis-suppress-health-check=filter_too_much \
|
||||
--stateful=links; \
|
||||
else \
|
||||
st run api/openapi/$(svc).yml \
|
||||
--checks all \
|
||||
--base-url $(2) \
|
||||
--header "Authorization: Bearer $(USER_TOKEN)" \
|
||||
--contrib-openapi-formats-uuid \
|
||||
--hypothesis-suppress-health-check=filter_too_much \
|
||||
--stateful=links; \
|
||||
fi
|
||||
endef
|
||||
|
||||
test_api_users: TEST_API_URL := http://localhost:9002
|
||||
test_api_clients: TEST_API_URL := http://localhost:9006
|
||||
test_api_domains: TEST_API_URL := http://localhost:9003
|
||||
test_api_channels: TEST_API_URL := http://localhost:9005
|
||||
test_api_groups: TEST_API_URL := http://localhost:9004
|
||||
test_api_http: TEST_API_URL := http://localhost:8008
|
||||
test_api_invitations: TEST_API_URL := http://localhost:9020
|
||||
test_api_auth: TEST_API_URL := http://localhost:9001
|
||||
test_api_bootstrap: TEST_API_URL := http://localhost:9013
|
||||
test_api_certs: TEST_API_URL := http://localhost:9019
|
||||
test_api_provision: TEST_API_URL := http://localhost:9016
|
||||
test_api_readers: TEST_API_URL := http://localhost:9009 # This can be the URL of any reader service.
|
||||
test_api_journal: TEST_API_URL := http://localhost:9021
|
||||
|
||||
$(TEST_API):
|
||||
$(call test_api_service,$(@),$(TEST_API_URL))
|
||||
test:
|
||||
go test -mod=vendor -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
|
||||
|
||||
proto:
|
||||
protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto
|
||||
mkdir -p $(INTERNAL_PROTO_GEN_OUT_DIR)
|
||||
protoc -I $(INTERNAL_PROTO_DIR) --go_out=$(INTERNAL_PROTO_GEN_OUT_DIR) --go_opt=paths=source_relative --go-grpc_out=$(INTERNAL_PROTO_GEN_OUT_DIR) --go-grpc_opt=paths=source_relative $(INTERNAL_PROTO_FILES)
|
||||
protoc --gofast_out=plugins=grpc:. *.proto
|
||||
protoc --gofast_out=plugins=grpc:. pkg/messaging/*.proto
|
||||
|
||||
$(FILTERED_SERVICES):
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
|
||||
$(DOCKERS):
|
||||
@@ -203,7 +77,7 @@ dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
docker push $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
done
|
||||
endef
|
||||
|
||||
@@ -218,53 +92,22 @@ release:
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
docker tag $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
done
|
||||
$(call docker_push,$(version))
|
||||
|
||||
rundev:
|
||||
cd scripts && ./run.sh
|
||||
|
||||
grpc_mtls_certs:
|
||||
$(MAKE) -C docker/ssl auth_grpc_certs clients_grpc_certs
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
check_tls:
|
||||
ifeq ($(GRPC_TLS),true)
|
||||
@unset GRPC_MTLS
|
||||
@echo "gRPC TLS is enabled"
|
||||
GRPC_MTLS=
|
||||
else
|
||||
@unset GRPC_TLS
|
||||
GRPC_TLS=
|
||||
endif
|
||||
runlora:
|
||||
docker-compose \
|
||||
-f docker/docker-compose.yml \
|
||||
-f docker/addons/influxdb-writer/docker-compose.yml \
|
||||
-f docker/addons/lora-adapter/docker-compose.yml up \
|
||||
|
||||
check_mtls:
|
||||
ifeq ($(GRPC_MTLS),true)
|
||||
@unset GRPC_TLS
|
||||
@echo "gRPC MTLS is enabled"
|
||||
GRPC_TLS=
|
||||
else
|
||||
@unset GRPC_MTLS
|
||||
GRPC_MTLS=
|
||||
endif
|
||||
|
||||
check_certs: check_mtls check_tls
|
||||
ifeq ($(GRPC_MTLS_CERT_FILES_EXISTS),0)
|
||||
ifeq ($(filter true,$(GRPC_MTLS) $(GRPC_TLS)),true)
|
||||
ifeq ($(filter $(DEFAULT_DOCKER_COMPOSE_COMMAND),$(DOCKER_COMPOSE_COMMAND)),$(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
$(MAKE) -C docker/ssl auth_grpc_certs clients_grpc_certs
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
run: check_certs
|
||||
docker compose -f docker/docker-compose.yml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_addons: check_certs
|
||||
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
done
|
||||
|
||||
run_live: check_certs
|
||||
GOPATH=$(go env GOPATH) docker compose -f docker/docker-compose.yml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
# Run all Mainflux core services except distributed tracing system - Jaeger. Recommended on gateways:
|
||||
rungw:
|
||||
MF_JAEGER_URL= docker-compose -f docker/docker-compose.yml up --scale jaeger=0
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# SuperMQ
|
||||
# Mainflux
|
||||
|
||||
[](https://github.com/absmach/supermq/actions/workflows/check-license.yaml)
|
||||
[](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yml)
|
||||
[](https://github.com/absmach/supermq/actions/workflows/build.yml)
|
||||
[![build][ci-badge]][ci-url]
|
||||
[![go report card][grc-badge]][grc-url]
|
||||
[![coverage][cov-badge]][cov-url]
|
||||
[![license][license]](LICENSE)
|
||||
@@ -10,24 +8,29 @@
|
||||
|
||||
![banner][banner]
|
||||
|
||||
SuperMQ is modern, scalable, secure, open-source, and patent-free cloud platform for messaging and event-driven architecture (EDA) written in Go.
|
||||
Mainflux is modern, scalable, secure, open-source, and patent-free IoT cloud platform written in Go.
|
||||
|
||||
It accepts user and client (device, user, application) connections over various network protocols (i.e. HTTP, MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the architecture backbone for building complex distributed solutions.
|
||||
It accepts user and thing connections over various network protocols (i.e. HTTP,
|
||||
MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware
|
||||
for building complex IoT solutions.
|
||||
|
||||
For more details, check out the [official documentation][docs].
|
||||
For extra bits and services see [our contrib repository][contrib].
|
||||
|
||||
Mainflux is member of the [Linux Foundation][lf] and an active contributor
|
||||
to the [EdgeX Foundry][edgex] project. It has been made with :heart: by [Mainflux Labs][company],
|
||||
which maintains the project and offers professional services around it.
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-protocol connectivity and bridging (HTTP, MQTT, WebSocket and CoAP; see [contrib repository][contrib] for LoRa and OPC UA)
|
||||
- Multi-protocol connectivity and bridging (HTTP, MQTT, WebSocket and CoAP)
|
||||
- Device management and provisioning (Zero Touch provisioning)
|
||||
- Mutual TLS Authentication (mTLS) using X.509 Certificates
|
||||
- Fine-grained access control (policies, ABAC/RBAC)
|
||||
- Message persistence (Timescale and PostgresSQL - see [contrib repository][contrib] for Cassandra, InfluxDB, and MongoDB support)
|
||||
- Platform logging and instrumentation support (Prometheus and OpenTelemetry)
|
||||
- Fine-grained access control
|
||||
- Message persistence (Cassandra, InfluxDB, MongoDB and PostgresSQL)
|
||||
- Platform logging and instrumentation support (Grafana, Prometheus and OpenTracing)
|
||||
- Event sourcing
|
||||
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
|
||||
- Edge [Agent][agent] and [Export][export] services for remote IoT gateway management and edge computing
|
||||
- [LoRaWAN][lora] network integration
|
||||
- SDK
|
||||
- CLI
|
||||
- Small memory footprint and fast execution
|
||||
@@ -35,97 +38,83 @@ For extra bits and services see [our contrib repository][contrib].
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following are needed to run SuperMQ:
|
||||
The following are needed to run Mainflux:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/) (version 26.0.0)
|
||||
- [Docker](https://docs.docker.com/install/) (version 18.09)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.24.1)
|
||||
|
||||
Developing SuperMQ will also require:
|
||||
Developing Mainflux will also require:
|
||||
|
||||
- [Go](https://golang.org/doc/install) (version 1.21)
|
||||
- [Protobuf](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation) (version 25.1)
|
||||
- [Go](https://golang.org/doc/install) (version 1.13.3)
|
||||
- [Protobuf](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation) (version 3.6.1)
|
||||
|
||||
## Install
|
||||
|
||||
Once the prerequisites are installed, execute the following commands from the project's root:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yml --env-file docker/.env -p git_github_com_absmach_supermq_git_ up
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
```
|
||||
|
||||
This will bring up the SuperMQ docker services and interconnect them. This command can also be executed using the project's included Makefile:
|
||||
This will bring up the Mainflux docker services and interconnect them. This command can also be executed using the project's included Makefile:
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
If you want to run services from specific release checkout code from github and make sure that
|
||||
`SMQ_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`
|
||||
```
|
||||
|
||||
Check that `.env` file contains:
|
||||
|
||||
```bash
|
||||
SMQ_RELEASE_TAG=<release_number>
|
||||
```
|
||||
|
||||
> `docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://docs.supermq.abstractmachines.fr/kubernetes).
|
||||
|
||||
## Usage
|
||||
|
||||
The quickest way to start using SuperMQ is via the CLI. The latest version can be downloaded from the [official releases page][releases].
|
||||
The quickest way to start using Mainflux is via the CLI. The latest version can be downloaded from the [official releases page][rel].
|
||||
|
||||
It can also be built and used from the project's root directory:
|
||||
|
||||
```bash
|
||||
make cli
|
||||
./build/cli version
|
||||
./build/mainflux-cli version
|
||||
```
|
||||
|
||||
Additional details on using the CLI can be found in the [CLI documentation](https://docs.supermq.abstractmachines.fr/cli).
|
||||
Additional details on using the CLI can be found in the [CLI documentation](https://mainflux.readthedocs.io/en/latest/cli/).
|
||||
|
||||
## Documentation
|
||||
|
||||
Official documentation is hosted at [SuperMQ official docs page][docs]. Documentation is auto-generated, checkout the instructions on [official docs repository](https://github.com/absmach/supermq-docs):
|
||||
Official documentation is hosted at [Mainflux Read The Docs page][docs]. Documentation is auto-generated, checkout the instructions on [official docs repository](https://github.com/mainflux/docs):
|
||||
|
||||
If you spot an error or a need for corrections, please let us know - or even better: send us a PR.
|
||||
|
||||
Additional practical information, news and tutorials can be found on the [Mainflux blog][blog].
|
||||
|
||||
## Authors
|
||||
|
||||
Main architect and BDFL of SuperMQ project is [@drasko][drasko].
|
||||
Main architect and BDFL of Mainflux project is [@drasko][drasko].
|
||||
|
||||
Additionally, [@nmarcetic][nikola] and [@janko-isidorovic][janko] assured overall architecture and design, while [@manuio][manu] and [@darkodraskovic][darko] helped with crafting initial implementation and continuously worked on the project evolutions.
|
||||
Additionally, [@nmarcetic][nikola] and [@janko-isidorovic][janko] assured
|
||||
overall architecture and design, while [@manuio][manu] and [@darkodraskovic][darko]
|
||||
helped with crafting initial implementation and continuously worked on the project evolutions.
|
||||
|
||||
Besides them, SuperMQ is constantly improved and actively developed by [@anovakovic01][alex], [@dusanb94][dusan], [@srados][sava], [@gsaleh][george], [@blokovi][iva], [@chombium][kole], [@mteodor][mirko], [@rodneyosodo][rodneyosodo] and a large set of contributors.
|
||||
Besides them, Mainflux is constantly improved and actively
|
||||
developed by [@anovakovic01][alex], [@dusanb94][dusan], [@srados][sava],
|
||||
[@gsaleh][george], [@blokovi][iva], [@chombium][kole], [@mteodor][mirko] and a large set of contributors.
|
||||
|
||||
Maintainers are listed in [MAINTAINERS](MAINTAINERS) file.
|
||||
|
||||
The SuperMQ team would like to give special thanks to [@mijicd][dejan] for his monumental work on designing and implementing a highly improved and optimized version of the platform, and [@malidukica][dusanm] for his effort on implementing the initial user interface.
|
||||
|
||||
## Professional Support
|
||||
|
||||
There are many companies offering professional support for the SuperMQ system.
|
||||
|
||||
If you need this kind of support, best is to reach out to [@drasko][drasko] directly, and he will point you out to the best-matching support team.
|
||||
The Mainflux team would like to give special thanks to [@mijicd][dejan] for his monumental work
|
||||
on designing and implementing a highly improved and optimized version of the platform,
|
||||
and [@malidukica][dusanm] for his effort on implementing the initial user interface.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for your interest in SuperMQ and the desire to contribute!
|
||||
Thank you for your interest in Mainflux and the desire to contribute!
|
||||
|
||||
1. Take a look at our [open issues](https://github.com/absmach/supermq/issues). The [good-first-issue](https://github.com/absmach/supermq/labels/good-first-issue) label is specifically for issues that are great for getting started.
|
||||
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues). The [good-first-issue](https://github.com/mainflux/mainflux/labels/good-first-issue) label is specifically for issues that are great for getting started.
|
||||
2. Checkout the [contribution guide](CONTRIBUTING.md) to learn more about our style and conventions.
|
||||
3. Make your changes compatible to our workflow.
|
||||
|
||||
Also, explore our [contrib][contrib] repository for extra services such as Cassandra, InfluxDB, MongoDB readers and writers, LoRa, OPC UA support, Digital Twins, and more. If you have a contribution that is not a good fit for the core monorepo (it's specific to your use case, it's an additional feature or a new service, it's optional or an add-on), this is a great place to submit the pull request.
|
||||
|
||||
### We're Hiring
|
||||
|
||||
You like SuperMQ 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.
|
||||
If you are interested in working professionally on Mainflux,
|
||||
please head to company's [careers page][careers] or shoot us an e-mail at <careers@mainflux.com>.
|
||||
|
||||
> The best way to grab our attention is, of course, by sending PRs :sunglasses:.
|
||||
>The best way to grab our attention is by sending PRs :sunglasses:.
|
||||
|
||||
## Community
|
||||
|
||||
@@ -137,42 +126,30 @@ You like SuperMQ and you would like to make it your day job? We're always lookin
|
||||
|
||||
[Apache-2.0](LICENSE)
|
||||
|
||||
[![FOSSA Status][FOSSA]][FOSSA]
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fmainflux%2Fmainflux?ref=badge_large)
|
||||
|
||||
## Data Collection for SuperMQ
|
||||
|
||||
SuperMQ is committed to continuously improving its services and ensuring a seamless experience for its users. To achieve this, we collect certain data from your deployments. Rest assured, this data is collected solely for the purpose of enhancing SuperMQ and is not used with any malicious intent. The deployment summary can be found on our [website][callhome].
|
||||
|
||||
The collected data includes:
|
||||
|
||||
- **IP Address** - Used for approximate location information on deployments.
|
||||
- **Services Used** - To understand which features are popular and prioritize future developments.
|
||||
- **Last Seen Time** - To ensure the stability and availability of SuperMQ.
|
||||
- **SuperMQ Version** - To track the software version and deliver relevant updates.
|
||||
|
||||
We take your privacy and data security seriously. All data collected is handled in accordance with our stringent privacy policies and industry best practices.
|
||||
|
||||
Data collection is on by default and can be disabled by setting the env variable:
|
||||
`SMQ_SEND_TELEMETRY=false`
|
||||
|
||||
By utilizing SuperMQ, you actively contribute to its improvement. Together, we can build a more robust and efficient platform. Thank you for your trust in SuperMQ!
|
||||
|
||||
[banner]: https://github.com/absmach/supermq-docs/blob/main/docs/img/gopherBanner.jpg
|
||||
[docs]: https://docs.supermq.abstractmachines.fr
|
||||
[banner]: https://github.com/mainflux/docs/blob/master/docs/img/gopherBanner.jpg
|
||||
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
|
||||
[ci-url]: https://semaphoreci.com/mainflux/mainflux
|
||||
[docs]: http://mainflux.readthedocs.io
|
||||
[docker]: https://www.docker.com
|
||||
[forum]: https://groups.google.com/forum/#!forum/mainflux
|
||||
[gitter]: https://gitter.im/absmach/supermq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
[gitter]: https://gitter.im/mainflux/mainflux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
|
||||
[grc-badge]: https://goreportcard.com/badge/github.com/absmach/supermq
|
||||
[grc-url]: https://goreportcard.com/report/github.com/absmach/supermq
|
||||
[cov-badge]: https://codecov.io/gh/absmach/supermq/graph/badge.svg?token=nPCEr5nW8S
|
||||
[cov-url]: https://codecov.io/gh/absmach/supermq
|
||||
[grc-badge]: https://goreportcard.com/badge/github.com/mainflux/mainflux
|
||||
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
|
||||
[cov-badge]: https://codecov.io/gh/mainflux/mainflux/branch/master/graph/badge.svg
|
||||
[cov-url]: https://codecov.io/gh/mainflux/mainflux
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/absmach
|
||||
[agent]: https://github.com/absmach/agent
|
||||
[export]: https://github.com/absmach/export
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
[lora]: https://lora-alliance.org/
|
||||
[kubernetes]: https://kubernetes.io/
|
||||
[releases]: https://github.com/absmach/supermq/releases
|
||||
[rel]: https://github.com/mainflux/mainflux/releases
|
||||
[careers]: https://www.mainflux.com/careers.html
|
||||
[lf]: https://www.linuxfoundation.org/
|
||||
[edgex]: https://www.edgexfoundry.org/
|
||||
[company]: https://www.mainflux.com/
|
||||
[blog]: https://medium.com/mainflux-iot-platform
|
||||
[drasko]: https://github.com/drasko
|
||||
[nikola]: https://github.com/nmarcetic
|
||||
[dejan]: https://github.com/mijicd
|
||||
@@ -180,14 +157,10 @@ By utilizing SuperMQ, you actively contribute to its improvement. Together, we c
|
||||
[darko]: https://github.com/darkodraskovic
|
||||
[janko]: https://github.com/janko-isidorovic
|
||||
[alex]: https://github.com/anovakovic01
|
||||
[dusan]: https://github.com/dborovcanin
|
||||
[dusan]: https://github.com/dusanb94
|
||||
[sava]: https://github.com/srados
|
||||
[george]: https://github.com/gesaleh
|
||||
[iva]: https://github.com/blokovi
|
||||
[kole]: https://github.com/chombium
|
||||
[dusanm]: https://github.com/malidukica
|
||||
[mirko]: https://github.com/mteodor
|
||||
[rodneyosodo]: https://github.com/rodneyosodo
|
||||
[callhome]: https://deployments.magistrala.abstractmachines.fr/
|
||||
[contrib]: https://www.github.com/absmach/mg-contrib
|
||||
[FOSSA]: https://app.fossa.com/api/projects/git%2Bgithub.com%2Fabsmach%2Fsupermq.svg?type=small
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package supermq
|
||||
package mainflux
|
||||
|
||||
// Response contains HTTP response specific methods.
|
||||
type Response interface {
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
asyncapi: '2.6.0'
|
||||
id: 'https://github.com/absmach/supermq/blob/main/api/asyncapi/mqtt.yml'
|
||||
info:
|
||||
title: SuperMQ MQTT Adapter
|
||||
version: '1.0.0'
|
||||
contact:
|
||||
name: SuperMQ Team
|
||||
url: 'https://github.com/absmach/supermq'
|
||||
email: info@abstractmachines.fr
|
||||
description: |
|
||||
MQTT adapter provides an MQTT API for sending messages through the platform. MQTT adapter uses [mProxy](https://github.com/absmach/mproxy) for proxying traffic between client and MQTT broker.
|
||||
Additionally, the MQTT adapter and the message broker are replicating the traffic between brokers.
|
||||
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'https://github.com/absmach/supermq/blob/main/LICENSE'
|
||||
|
||||
|
||||
defaultContentType: application/json
|
||||
|
||||
servers:
|
||||
dev:
|
||||
url: localhost:{port}
|
||||
protocol: mqtt
|
||||
description: Test broker
|
||||
variables:
|
||||
port:
|
||||
description: Secure connection (TLS) is available through port 8883.
|
||||
default: '1883'
|
||||
enum:
|
||||
- '1883'
|
||||
- '8883'
|
||||
security:
|
||||
- user-password: []
|
||||
|
||||
channels:
|
||||
channels/{channelID}/messages/{subtopic}:
|
||||
parameters:
|
||||
channelID:
|
||||
$ref: '#/components/parameters/channelID'
|
||||
in: path
|
||||
required: true
|
||||
subtopic:
|
||||
$ref: '#/components/parameters/subtopic'
|
||||
in: path
|
||||
required: false
|
||||
|
||||
publish:
|
||||
traits:
|
||||
- $ref: '#/components/operationTraits/mqtt'
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
subscribe:
|
||||
traits:
|
||||
- $ref: '#/components/operationTraits/mqtt'
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
|
||||
components:
|
||||
messages:
|
||||
jsonMsg:
|
||||
title: JSON Message
|
||||
summary: Arbitrary JSON array or object.
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: "#/components/schemas/jsonMsg"
|
||||
|
||||
schemas:
|
||||
jsonMsg:
|
||||
type: object
|
||||
description: Arbitrary JSON object or array. SenML format is recommended.
|
||||
example: |
|
||||
### SenML
|
||||
```json
|
||||
[{"bn":"some-base-name:","bt":1641646520, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]
|
||||
```
|
||||
### JSON
|
||||
```json
|
||||
{"field_1":"val_1", "t": 1641646525}
|
||||
```
|
||||
### JSON Array
|
||||
```json
|
||||
[{"field_1":"val_1", "t": 1641646520},{"field_2":"val_2", "t": 1641646522}]
|
||||
```
|
||||
|
||||
parameters:
|
||||
channelID:
|
||||
description: Channel ID connected to the Client ID defined in the username.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
subtopic:
|
||||
description: Arbitrary message subtopic.
|
||||
schema:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
securitySchemes:
|
||||
user-password:
|
||||
type: userPassword
|
||||
description: |
|
||||
username is client ID connected to the channel defined in the mqtt topic and
|
||||
password is client secret corresponding to the client ID
|
||||
|
||||
operationTraits:
|
||||
mqtt:
|
||||
bindings:
|
||||
mqtt:
|
||||
qos: 2
|
||||
@@ -1,144 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
asyncapi: 2.6.0
|
||||
id: 'https://github.com/absmach/supermq/blob/main/api/asyncapi/websocket.yml'
|
||||
info:
|
||||
title: SuperMQ WebSocket adapter
|
||||
description: WebSocket adapter provides a WebSocket API for sending messages through communication channels. WebSocket adapter uses [mProxy](https://github.com/absmach/mproxy) for proxying traffic between client and MQTT broker.
|
||||
version: '1.0.0'
|
||||
contact:
|
||||
name: SuperMQ Team
|
||||
url: 'https://github.com/absmach/supermq'
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'https://github.com/absmach/supermq/blob/main/LICENSE'
|
||||
tags:
|
||||
- name: WebSocket
|
||||
defaultContentType: application/json
|
||||
|
||||
servers:
|
||||
dev:
|
||||
url: 'ws://{host}:{port}'
|
||||
protocol: ws
|
||||
description: Default WebSocket Adapter URL
|
||||
variables:
|
||||
host:
|
||||
description: Hostname of the WebSocket adapter
|
||||
default: localhost
|
||||
port:
|
||||
description: SuperMQ WebSocket Adapter port
|
||||
default: '8186'
|
||||
|
||||
channels:
|
||||
'channels/{channelID}/messages/{subtopic}':
|
||||
parameters:
|
||||
channelID:
|
||||
$ref: '#/components/parameters/channelID'
|
||||
in: path
|
||||
required: true
|
||||
subtopic:
|
||||
$ref: '#/components/parameters/subtopic'
|
||||
in: path
|
||||
required: false
|
||||
publish:
|
||||
summary: Publish messages to a channel
|
||||
operationId: publishToChannel
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
messageId: publishMessage
|
||||
bindings:
|
||||
ws:
|
||||
method: POST
|
||||
query:
|
||||
subtopic: '{$request.query.subtopic}'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
subscribe:
|
||||
summary: Subscribe to receive messages from a channel
|
||||
operationId: subscribeToChannel
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
messageId: subscribeMessage
|
||||
bindings:
|
||||
ws:
|
||||
method: GET
|
||||
query:
|
||||
subtopic: '{$request.query.subtopic}'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
/version:
|
||||
subscribe:
|
||||
summary: Get the version of the SuperMQ adapter
|
||||
operationId: getVersion
|
||||
bindings:
|
||||
http:
|
||||
method: GET
|
||||
metrics:
|
||||
description: Endpoint for getting service metrics.
|
||||
subscribe:
|
||||
operationId: metrics
|
||||
summary: Service metrics
|
||||
bindings:
|
||||
http:
|
||||
type: request
|
||||
method: GET
|
||||
|
||||
components:
|
||||
messages:
|
||||
jsonMsg:
|
||||
title: JSON Message
|
||||
summary: Arbitrary JSON array or object.
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/jsonMsg'
|
||||
schemas:
|
||||
jsonMsg:
|
||||
type: object
|
||||
description: Arbitrary JSON object or array. SenML format is recommended.
|
||||
example: >
|
||||
### SenML
|
||||
|
||||
```json
|
||||
|
||||
[{"bn":"some-base-name:","bt":1641646520, "bu":"A","bver":5,
|
||||
"n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2},
|
||||
{"n":"current","t":-4,"v":1.3}]
|
||||
|
||||
```
|
||||
|
||||
### JSON
|
||||
|
||||
```json
|
||||
|
||||
{"field_1":"val_1", "t": 1641646525}
|
||||
|
||||
```
|
||||
|
||||
### JSON Array
|
||||
|
||||
```json
|
||||
|
||||
[{"field_1":"val_1", "t": 1641646520},{"field_2":"val_2", "t":
|
||||
1641646522}]
|
||||
|
||||
```
|
||||
parameters:
|
||||
channelID:
|
||||
description: Channel ID connected to the Client ID defined in the username.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
subtopic:
|
||||
description: Arbitrary message subtopic.
|
||||
schema:
|
||||
type: string
|
||||
default: ''
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Client access: "Authorization: Client <client_key>"
|
||||
@@ -1,5 +0,0 @@
|
||||
# SuperMQ OpenAPI Specification
|
||||
|
||||
This folder contains an OpenAPI specifications for SuperMQ API.
|
||||
|
||||
View specification in Swagger UI at [docs.api.magistrala.abstractmachines.fr](https://docs.api.supermq.abstractmachines.fr)
|
||||
@@ -1,285 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: SuperMQ Auth Service
|
||||
description: |
|
||||
This is the Auth Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform users. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9001
|
||||
- url: https://localhost:9001
|
||||
|
||||
tags:
|
||||
- name: Keys
|
||||
description: Everything about your Keys.
|
||||
externalDocs:
|
||||
description: Find out more about keys
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
- name: Health
|
||||
description: Service health check endpoint.
|
||||
externalDocs:
|
||||
description: Find out more about health check
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
operationId: issueKey
|
||||
tags:
|
||||
- Keys
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Issued new key.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"409":
|
||||
description: Failed due to using already existing ID.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/keys/{keyID}:
|
||||
get:
|
||||
operationId: getKey
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- Keys
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
delete:
|
||||
operationId: revokeKey
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- Keys
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
"204":
|
||||
description: Key revoked.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- Health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
issuer_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "9118de62-c680-46b7-ad0a-21748a52833a"
|
||||
description: In ID of the entity that issued the token.
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
subject:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key subject.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated.
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires. If this field is missing,
|
||||
that means that Key is valid indefinitely.
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
Status:
|
||||
name: status
|
||||
description: Domain status.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: enabled
|
||||
required: false
|
||||
example: enabled
|
||||
DomainName:
|
||||
name: name
|
||||
description: Domain's name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "domainName"
|
||||
Permission:
|
||||
name: permission
|
||||
description: permission.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "edit"
|
||||
ApiKeyId:
|
||||
name: keyID
|
||||
description: API Key ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
Type:
|
||||
name: type
|
||||
description: The type of the API Key.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Subject:
|
||||
name: subject
|
||||
description: The subject of an API Key
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
links:
|
||||
revoke:
|
||||
operationId: revokeKey
|
||||
parameters:
|
||||
keyID: $response.body#/id
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,690 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ Bootstrap service
|
||||
description: |
|
||||
HTTP API for managing platform clients configuration.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9013
|
||||
- url: https://localhost:9013
|
||||
|
||||
tags:
|
||||
- name: configs
|
||||
description: Everything about your Configs
|
||||
externalDocs:
|
||||
description: Find out more about Configs
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/{domainID}/clients/configs:
|
||||
post:
|
||||
operationId: createConfig
|
||||
summary: Adds new config
|
||||
description: |
|
||||
Adds new config to the list of config owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"409":
|
||||
description: Failed due to using an existing identity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
"503":
|
||||
description: Failed to receive response from the clients service.
|
||||
get:
|
||||
operationId: getConfigs
|
||||
summary: Retrieves managed configs
|
||||
description: |
|
||||
Retrieves a list of managed configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/{configId}:
|
||||
get:
|
||||
operationId: getConfig
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
"400":
|
||||
description: Missing or invalid config.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateConfig
|
||||
summary: Updates config info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the owner, ID, external ID,
|
||||
external key, SuperMQ Client ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeConfig
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the SuperMQ channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
"204":
|
||||
description: Config removed.
|
||||
"400":
|
||||
description: Failed due to malformed config ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/certs/{configId}:
|
||||
patch:
|
||||
operationId: updateConfigCerts
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
$ref: "#/components/responses/ConfigUpdateCertsRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/connections/{configId}:
|
||||
put:
|
||||
operationId: updateConfigConnections
|
||||
summary: Updates channels the client is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Client is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/clients/bootstrap/{externalId}:
|
||||
get:
|
||||
operationId: getBootstrapConfig
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid external key provided.
|
||||
"404":
|
||||
description: Failed to retrieve corresponding config.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/clients/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
operationId: getSecureBootstrapConfig
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapEncAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/state/{configId}:
|
||||
put:
|
||||
operationId: updateConfigState
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding SuperMQ Client to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigStateUpdateReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Config removed.
|
||||
"400":
|
||||
description: Failed due to malformed config's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
magistrala_secret:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client key.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Channel unique identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to the Channel.
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Config"
|
||||
required:
|
||||
- configs
|
||||
BootstrapConfig:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client key.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
required:
|
||||
- client_id
|
||||
- client_key
|
||||
- channels
|
||||
- content
|
||||
ConfigUpdateCerts:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- client_id
|
||||
- client_key
|
||||
- channels
|
||||
- content
|
||||
|
||||
parameters:
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Client.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/State"
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ConfigCreateReq:
|
||||
description: JSON-formatted document describing the new config.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of the corresponding SuperMQ Client.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Client Private Key.
|
||||
ca_cert:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
description: JSON-formatted document describing the updated client.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigCertUpdateReq:
|
||||
description: JSON-formatted document describing the updated client.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the client is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
ConfigStateUpdateReq:
|
||||
description: Update the state of the Config.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
|
||||
responses:
|
||||
ConfigCreateRes:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /clients/configs/{configId}).
|
||||
ConfigListRes:
|
||||
description: Data retrieved. Configs from this list don't contain channels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigList"
|
||||
ConfigRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
links:
|
||||
update:
|
||||
operationId: updateConfig
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
updateCerts:
|
||||
operationId: updateConfigCerts
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
updateConnections:
|
||||
operationId: updateConfigConnections
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
updateState:
|
||||
operationId: updateConfigState
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
delete:
|
||||
operationId: removeConfig
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
BootstrapConfigRes:
|
||||
description: |
|
||||
Data retrieved. If secure, a response is encrypted using
|
||||
the secret key, so the response is in the binary form.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
ConfigUpdateCertsRes:
|
||||
description: Data retrieved. Config certs updated.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigUpdateCerts"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
bootstrapAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: string
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <external_key>"
|
||||
|
||||
bootstrapEncAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: aes-sha256-uuid
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <external_enc_key>"
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,316 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ Certs service
|
||||
description: |
|
||||
HTTP API for Certs service
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9019
|
||||
- url: https://localhost:9019
|
||||
|
||||
tags:
|
||||
- name: certs
|
||||
description: Everything about your Certs
|
||||
externalDocs:
|
||||
description: Find out more about certs
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/{domainID}/certs:
|
||||
post:
|
||||
operationId: createCert
|
||||
summary: Creates a certificate for client
|
||||
description: Creates a certificate for client
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/certs/{certID}:
|
||||
get:
|
||||
operationId: getCert
|
||||
summary: Retrieves a certificate
|
||||
description: |
|
||||
Retrieves a certificate for a given cert ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/CertRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: revokeCert
|
||||
summary: Revokes a certificate
|
||||
description: |
|
||||
Revokes a certificate for a given cert ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/serials/{clientID}:
|
||||
get:
|
||||
operationId: getSerials
|
||||
summary: Retrieves certificates' serial IDs
|
||||
description: |
|
||||
Retrieves a list of certificates' serial IDs for a given client ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ClientID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/SerialsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding certificates.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
ClientID:
|
||||
name: clientID
|
||||
description: Client ID
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
CertID:
|
||||
name: certID
|
||||
description: Serial of certificate
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Cert:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
issuing_ca:
|
||||
type: string
|
||||
description: CA Certificate that is used to issue client certs, usually intermediate.
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Serial:
|
||||
type: object
|
||||
properties:
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
CertsPage:
|
||||
type: object
|
||||
properties:
|
||||
certs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
SerialsPage:
|
||||
type: object
|
||||
properties:
|
||||
serials:
|
||||
type: array
|
||||
description: Certificate serials IDs.
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
Revoke:
|
||||
type: object
|
||||
properties:
|
||||
revocation_time:
|
||||
type: string
|
||||
description: Certificate revocation time
|
||||
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a client
|
||||
provide a client id, data identifying particular client will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- client_id
|
||||
- ttl
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
ttl:
|
||||
type: string
|
||||
example: "10h"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
CertRes:
|
||||
description: Certificate data.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
links:
|
||||
serial:
|
||||
operationId: getSerials
|
||||
parameters:
|
||||
clientID: $response.body#/client_id
|
||||
delete:
|
||||
operationId: revokeCert
|
||||
parameters:
|
||||
certID: $response.body#/serial
|
||||
CertsPageRes:
|
||||
description: Certificates page.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CertsPage"
|
||||
SerialsPageRes:
|
||||
description: Serials page.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SerialsPage"
|
||||
RevokeRes:
|
||||
description: Certificate revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revoke"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,959 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identified: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Magistrala Domains Service
|
||||
description: |
|
||||
This is the Domains Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform domains. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9003
|
||||
- url: https://localhost:9003
|
||||
|
||||
tags:
|
||||
- name: Domains
|
||||
description: CRUD operations for your domains
|
||||
externalDocs:
|
||||
description: Find out more about domains
|
||||
url: https://docs.magistrala.abstractmachines.fr/
|
||||
- name: Roles
|
||||
description: All operations involving roles for clients
|
||||
externalDocs:
|
||||
description: Find out more about roles
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
- name: Health
|
||||
description: Service health check endpoint.
|
||||
externalDocs:
|
||||
description: Find out more about health check
|
||||
url: https://docs.magistrala.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/domains:
|
||||
post:
|
||||
tags:
|
||||
- Domains
|
||||
summary: Adds new domain
|
||||
description: |
|
||||
Adds new domain.
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/DomainCreateReq"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/DomainCreateRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"409":
|
||||
description: Failed due to using an existing alias.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves list of domains.
|
||||
description: |
|
||||
Retrieves list of domains that the user have access.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Status"
|
||||
- $ref: "#/components/parameters/DomainName"
|
||||
- $ref: "#/components/parameters/Permission"
|
||||
tags:
|
||||
- Domains
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/DomainsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}:
|
||||
get:
|
||||
summary: Retrieves domain information
|
||||
description: |
|
||||
Retrieves a specific domain that is identified by the domain ID.
|
||||
tags:
|
||||
- Domains
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/DomainRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Unauthorized access to domain id.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
patch:
|
||||
summary: Updates name, metadata, tags and alias of the domain.
|
||||
description: |
|
||||
Updates name, metadata, tags and alias of the domain.
|
||||
tags:
|
||||
- Domains
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/DomainUpdateReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/DomainRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Unauthorized access to domain id.
|
||||
"404":
|
||||
description: Failed due to non existing domain.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/enable:
|
||||
post:
|
||||
summary: Enables a domain
|
||||
description: |
|
||||
Enables a specific domain that is identified by the domain ID.
|
||||
tags:
|
||||
- Domains
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully enabled domain.
|
||||
"400":
|
||||
description: Failed due to malformed domain's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Unauthorized access the domain ID.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/disable:
|
||||
post:
|
||||
summary: Disable a domain
|
||||
description: |
|
||||
Disable a specific domain that is identified by the domain ID.
|
||||
tags:
|
||||
- Domains
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully disabled domain.
|
||||
"400":
|
||||
description: Failed due to malformed domain's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Unauthorized access the domain ID.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/freeze:
|
||||
post:
|
||||
summary: Freeze a domain
|
||||
description: |
|
||||
Freeze a specific domain that is identified by the domain ID.
|
||||
tags:
|
||||
- Domains
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully freezed domain.
|
||||
"400":
|
||||
description: Failed due to malformed domain's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Unauthorized access the domain ID.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles:
|
||||
post:
|
||||
operationId: createDomainRole
|
||||
summary: Creates a role for a domain
|
||||
description: |
|
||||
Creates a role for a specific domain that is identified by the domain ID.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "./schemas/roles.yml#/components/requestBodies/CreateRoleReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"201":
|
||||
$ref: "./schemas/roles.yml#/components/responses/CreateRoleRes"
|
||||
"400":
|
||||
description: Failed due to malformed domain's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: listDomainRoles
|
||||
tags:
|
||||
- Roles
|
||||
summary: Retrieves domains roles.
|
||||
description: |
|
||||
Retrieves a list of domain roles. Due to performance concerns, data
|
||||
is retrieved in subsets. The API domains must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/ListRolesRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}:
|
||||
get:
|
||||
operationId: getDomainRole
|
||||
tags:
|
||||
- Roles
|
||||
summary: Retrieves domain role.
|
||||
description: |
|
||||
Retrieves a specific domain role that is identified by the role name.
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/GetRoleRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
put:
|
||||
operationId: updateDomainRole
|
||||
summary: Updates domain role.
|
||||
description: |
|
||||
Updates a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
requestBody:
|
||||
$ref: "./schemas/roles.yml#/components/requestBodies/UpdateRoleReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/GetRoleRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
delete:
|
||||
operationId: deleteDomainRole
|
||||
summary: Deletes domain role.
|
||||
description: |
|
||||
Deletes a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Role deleted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}/actions:
|
||||
post:
|
||||
operationId: addDomainRoleAction
|
||||
summary: Adds a role action for a domain role.
|
||||
description: |
|
||||
Adds a role action for a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
requestBody:
|
||||
$ref: "./schemas/roles.yml#/components/requestBodies/AddRoleActionsReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/AddRoleActionsRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: listDomainRoleActions
|
||||
tags:
|
||||
- Roles
|
||||
summary: Lists domain role actions.
|
||||
description: |
|
||||
Retrieves a list of domain role actions.
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/ListRoleActionsRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}/actions/delete:
|
||||
post:
|
||||
operationId: deleteDomainRoleAction
|
||||
summary: Deletes role actions for a domain role.
|
||||
description: |
|
||||
Deletes a role action for a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
requestBody:
|
||||
$ref: "./schemas/roles.yml#/components/requestBodies/AddRoleActionsReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Role actions deleted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}/actions/delete-all:
|
||||
post:
|
||||
operationId: deleteAllDomainRoleActions
|
||||
summary: Deletes all role actions for a domain role.
|
||||
description: |
|
||||
Deletes all role actions for a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: All role actions deleted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}/members:
|
||||
post:
|
||||
operationId: addDomainRoleMember
|
||||
summary: Adds a member to a domain role.
|
||||
description: |
|
||||
Adds a member to a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
requestBody:
|
||||
$ref: "./schemas/roles.yml#/components/requestBodies/AddRoleMembersReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/AddRoleMembersRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: listDomainRoleMembers
|
||||
tags:
|
||||
- Roles
|
||||
summary: Lists domain role members.
|
||||
description: |
|
||||
Retrieves a list of domain role members.
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/ListRoleMembersRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}/members/delete:
|
||||
post:
|
||||
operationId: deleteDomainRoleMembers
|
||||
summary: Deletes members from a domain role.
|
||||
description: |
|
||||
Deletes a member from a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
requestBody:
|
||||
$ref: "./schemas/roles.yml#/components/requestBodies/AddRoleMembersReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Role members deleted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/{domainID}/roles/{roleName}/members/delete-all:
|
||||
post:
|
||||
operationId: deleteAllDomainRoleMembers
|
||||
summary: Deletes all members from a domain role.
|
||||
description: |
|
||||
Deletes all members from a specific domain role that is identified by the role name.
|
||||
tags:
|
||||
- Roles
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "./schemas/roles.yml#/components/parameters/RoleName"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: All role members deleted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/domains/roles/available-actions:
|
||||
get:
|
||||
operationId: listAvailableActions
|
||||
tags:
|
||||
- Roles
|
||||
summary: Retrieves available actions.
|
||||
description: |
|
||||
Retrieves a list of available actions.
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "./schemas/roles.yml#/components/responses/ListAvailableActionsRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- Health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
DomainReqObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: domainName
|
||||
description: Domain name.
|
||||
tags:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
example: ["tag1", "tag2"]
|
||||
description: domain tags.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "domain": "example.com" }
|
||||
description: Arbitrary, object-encoded domain's data.
|
||||
alias:
|
||||
type: string
|
||||
example: domain alias
|
||||
description: Domain alias.
|
||||
required:
|
||||
- name
|
||||
- alias
|
||||
Domain:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Domain unique identified.
|
||||
name:
|
||||
type: string
|
||||
example: domainName
|
||||
description: Domain name.
|
||||
tags:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
example: ["tag1", "tag2"]
|
||||
description: domain tags.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "domain": "example.com" }
|
||||
description: Arbitrary, object-encoded domain's data.
|
||||
alias:
|
||||
type: string
|
||||
example: domain alias
|
||||
description: Domain alias.
|
||||
status:
|
||||
type: string
|
||||
description: Domain Status
|
||||
format: string
|
||||
example: enabled
|
||||
created_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "0d837f56-3f8a-4e2a-9359-6347d0fc9f06 "
|
||||
description: User ID of the user who created the domain.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the domain was created.
|
||||
updated_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "80f66b77-ed74-4e74-9f88-6cce9a0a3049"
|
||||
description: User ID of the user who last updated the domain.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the domain was last updated.
|
||||
xml:
|
||||
name: domain
|
||||
|
||||
DomainsPage:
|
||||
type: object
|
||||
properties:
|
||||
domains:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Domain"
|
||||
total:
|
||||
type: integer
|
||||
example: 1
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- domains
|
||||
- total
|
||||
- offset
|
||||
DomainUpdate:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: domainName
|
||||
description: Domain name.
|
||||
tags:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
example: ["tag1", "tag2"]
|
||||
description: domain tags.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "domain": "example.com" }
|
||||
description: Arbitrary, object-encoded domain's data.
|
||||
alias:
|
||||
type: string
|
||||
example: domain alias
|
||||
description: Domain alias.
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identified.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
Status:
|
||||
name: status
|
||||
description: Domain status.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: enabled
|
||||
required: false
|
||||
example: enabled
|
||||
DomainName:
|
||||
name: name
|
||||
description: Domain's name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "domainName"
|
||||
Permission:
|
||||
name: permission
|
||||
description: permission.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "edit"
|
||||
ApiKeyId:
|
||||
name: keyID
|
||||
description: API Key ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
Type:
|
||||
name: type
|
||||
description: The type of the API Key.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Subject:
|
||||
name: subject
|
||||
description: The subject of an API Key
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
DomainCreateReq:
|
||||
description: JSON-formatted document describing the new domain to be registered
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DomainReqObj"
|
||||
DomainUpdateReq:
|
||||
description: JSON-formated document describing the name, alias, tags, and metadata of the domain to be updated
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DomainUpdate"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
|
||||
DomainCreateRes:
|
||||
description: Create new domain.
|
||||
headers:
|
||||
Location:
|
||||
schema:
|
||||
type: string
|
||||
format: url
|
||||
description: Registered domain relative URL in the format `/domains/<domainID_id>`
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Domain"
|
||||
|
||||
DomainRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Domain"
|
||||
DomainsPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DomainsPage"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,182 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ http adapter
|
||||
description: |
|
||||
HTTP API for sending messages through communication channels.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8008
|
||||
- url: https://localhost:8008
|
||||
|
||||
tags:
|
||||
- name: messages
|
||||
description: Everything about your Messages
|
||||
externalDocs:
|
||||
description: Find out more about messages
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/channels/{id}/messages:
|
||||
post:
|
||||
summary: Sends message to the communication channel
|
||||
description: |
|
||||
Sends message to the communication channel. Messages can be sent as
|
||||
JSON formatted SenML or as blob.
|
||||
tags:
|
||||
- messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MessageReq"
|
||||
responses:
|
||||
"202":
|
||||
description: Message is accepted for processing.
|
||||
"400":
|
||||
description: Message discarded due to its malformed content.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Message discarded due to invalid channel id.
|
||||
"415":
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
SenMLRecord:
|
||||
type: object
|
||||
properties:
|
||||
bn:
|
||||
type: string
|
||||
description: Base Name
|
||||
bt:
|
||||
type: number
|
||||
format: double
|
||||
description: Base Time
|
||||
bu:
|
||||
type: number
|
||||
format: double
|
||||
description: Base Unit
|
||||
bv:
|
||||
type: number
|
||||
format: double
|
||||
description: Base Value
|
||||
bs:
|
||||
type: number
|
||||
format: double
|
||||
description: Base Sum
|
||||
bver:
|
||||
type: number
|
||||
format: double
|
||||
description: Version
|
||||
n:
|
||||
type: string
|
||||
description: Name
|
||||
u:
|
||||
type: string
|
||||
description: Unit
|
||||
v:
|
||||
type: number
|
||||
format: double
|
||||
description: Value
|
||||
vs:
|
||||
type: string
|
||||
description: String Value
|
||||
vb:
|
||||
type: boolean
|
||||
description: Boolean Value
|
||||
vd:
|
||||
type: string
|
||||
description: Data Value
|
||||
s:
|
||||
type: number
|
||||
format: double
|
||||
description: Value Sum
|
||||
t:
|
||||
type: number
|
||||
format: double
|
||||
description: Time
|
||||
ut:
|
||||
type: number
|
||||
format: double
|
||||
description: Update Time
|
||||
SenMLArray:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/SenMLRecord"
|
||||
|
||||
parameters:
|
||||
ID:
|
||||
name: id
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
|
||||
requestBodies:
|
||||
MessageReq:
|
||||
description: |
|
||||
Message to be distributed. Since the platform expects messages to be
|
||||
properly formatted SenML in order to be post-processed, clients are
|
||||
obliged to specify Content-Type header for each published message.
|
||||
Note that all messages that aren't SenML will be accepted and published,
|
||||
but no post-processing will be applied.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SenMLArray"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Client access: "Authorization: Client <client_key>"
|
||||
|
||||
basicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
* Clients access: "Authorization: Basic <base64-encoded_credentials>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- basicAuth: []
|
||||
@@ -1,537 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: SuperMQ Invitations Service
|
||||
description: |
|
||||
This is the Invitations Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform invitations. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9020
|
||||
- url: https://localhost:9020
|
||||
|
||||
tags:
|
||||
- name: Invitations
|
||||
description: Everything about your Invitations
|
||||
externalDocs:
|
||||
description: Find out more about Invitations
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/invitations:
|
||||
post:
|
||||
operationId: sendInvitation
|
||||
tags:
|
||||
- Invitations
|
||||
summary: Send invitation
|
||||
description: |
|
||||
Send invitation to user to join domain.
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/SendInvitationReq"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"201":
|
||||
description: Invitation sent.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"409":
|
||||
description: Failed due to using an existing identity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: listInvitations
|
||||
tags:
|
||||
- Invitations
|
||||
summary: List invitations
|
||||
description: |
|
||||
Retrieves a list of invitations. Due to performance concerns, data
|
||||
is retrieved in subsets. The API must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/UserID"
|
||||
- $ref: "#/components/parameters/InvitedBy"
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/Relation"
|
||||
- $ref: "#/components/parameters/State"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/InvitationPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
This endpoint is available only for administrators.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/invitations/accept:
|
||||
post:
|
||||
operationId: acceptInvitation
|
||||
summary: Accept invitation
|
||||
description: |
|
||||
Current logged in user accepts invitation to join domain.
|
||||
tags:
|
||||
- Invitations
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/AcceptInvitationReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Invitation accepted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/invitations/reject:
|
||||
post:
|
||||
operationId: rejectInvitation
|
||||
summary: Reject invitation
|
||||
description: |
|
||||
Current logged in user rejects invitation to join domain.
|
||||
tags:
|
||||
- Invitations
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/AcceptInvitationReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Invitation rejected.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/invitations/{user_id}/{domain_id}:
|
||||
get:
|
||||
operationId: getInvitation
|
||||
summary: Retrieves a specific invitation
|
||||
description: |
|
||||
Retrieves a specific invitation that is identifier by the user ID and domain ID.
|
||||
tags:
|
||||
- Invitations
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/user_id"
|
||||
- $ref: "#/components/parameters/domain_id"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/InvitationRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
delete:
|
||||
operationId: deleteInvitation
|
||||
summary: Deletes a specific invitation
|
||||
description: |
|
||||
Deletes a specific invitation that is identifier by the user ID and domain ID.
|
||||
tags:
|
||||
- Invitations
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/user_id"
|
||||
- $ref: "#/components/parameters/domain_id"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Invitation deleted.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Failed due to non existing user.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
SendInvitationReqObj:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: User unique identifier.
|
||||
domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Domain unique identifier.
|
||||
relation:
|
||||
type: string
|
||||
enum:
|
||||
- administrator
|
||||
- editor
|
||||
- contributor
|
||||
- member
|
||||
- guest
|
||||
- domain
|
||||
- parent_group
|
||||
- role_group
|
||||
- group
|
||||
- platform
|
||||
example: editor
|
||||
description: Relation between user and domain.
|
||||
resend:
|
||||
type: boolean
|
||||
example: true
|
||||
description: Resend invitation.
|
||||
required:
|
||||
- user_id
|
||||
- domain_id
|
||||
- relation
|
||||
|
||||
Invitation:
|
||||
type: object
|
||||
properties:
|
||||
invited_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: User unique identifier.
|
||||
user_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: User unique identifier.
|
||||
domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Domain unique identifier.
|
||||
relation:
|
||||
type: string
|
||||
enum:
|
||||
- administrator
|
||||
- editor
|
||||
- contributor
|
||||
- member
|
||||
- guest
|
||||
- domain
|
||||
- parent_group
|
||||
- role_group
|
||||
- group
|
||||
- platform
|
||||
example: editor
|
||||
description: Relation between user and domain.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the group was created.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the group was created.
|
||||
confirmed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the group was created.
|
||||
xml:
|
||||
name: invitation
|
||||
|
||||
InvitationPage:
|
||||
type: object
|
||||
properties:
|
||||
invitations:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Invitation"
|
||||
total:
|
||||
type: integer
|
||||
example: 1
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- invitations
|
||||
- total
|
||||
- offset
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
example: { "error": "malformed entity specification" }
|
||||
|
||||
HealthRes:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Service status.
|
||||
enum:
|
||||
- pass
|
||||
version:
|
||||
type: string
|
||||
description: Service version.
|
||||
example: 0.14.0
|
||||
commit:
|
||||
type: string
|
||||
description: Service commit hash.
|
||||
example: 7d6f4dc4f7f0c1fa3dc24eddfb18bb5073ff4f62
|
||||
description:
|
||||
type: string
|
||||
description: Service description.
|
||||
example: <service_name> service
|
||||
build_time:
|
||||
type: string
|
||||
description: Service build time.
|
||||
example: 1970-01-01_00:00:00
|
||||
|
||||
parameters:
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 10
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "10"
|
||||
|
||||
UserID:
|
||||
name: user_id
|
||||
description: Unique user identifier.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
user_id:
|
||||
name: user_id
|
||||
description: Unique user identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
DomainID:
|
||||
name: domain_id
|
||||
description: Unique identifier for a domain.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
domain_id:
|
||||
name: domain_id
|
||||
description: Unique identifier for a domain.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
InvitedBy:
|
||||
name: invited_by
|
||||
description: Unique identifier for a user that invited the user.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
Relation:
|
||||
name: relation
|
||||
description: Relation between user and domain.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- administrator
|
||||
- editor
|
||||
- contributor
|
||||
- member
|
||||
- guest
|
||||
- domain
|
||||
- parent_group
|
||||
- role_group
|
||||
- group
|
||||
- platform
|
||||
required: false
|
||||
example: editor
|
||||
|
||||
State:
|
||||
name: state
|
||||
description: Invitation state.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- pending
|
||||
- accepted
|
||||
- all
|
||||
required: false
|
||||
example: accepted
|
||||
|
||||
requestBodies:
|
||||
SendInvitationReq:
|
||||
description: JSON-formatted document describing request for sending invitation
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SendInvitationReqObj"
|
||||
|
||||
AcceptInvitationReq:
|
||||
description: JSON-formatted document describing request for accepting invitation
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Domain unique identifier.
|
||||
required:
|
||||
- domain_id
|
||||
|
||||
responses:
|
||||
InvitationRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Invitation"
|
||||
links:
|
||||
delete:
|
||||
operationId: deleteInvitation
|
||||
parameters:
|
||||
user_id: $response.body#/user_id
|
||||
domain_id: $response.body#/domain_id
|
||||
|
||||
InvitationPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InvitationPage"
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthRes"
|
||||
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* User access: "Authorization: Bearer <user_access_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,344 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: SuperMQ Journal Log Service
|
||||
description: |
|
||||
This is the Journal Log Server based on the OpenAPI 3.0 specification. It is the HTTP API for viewing journal log history. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@mainflux.com
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9021
|
||||
- url: https://localhost:9021
|
||||
|
||||
tags:
|
||||
- name: journal-log
|
||||
description: Everything about your Journal Log
|
||||
externalDocs:
|
||||
description: Find out more about Journal Log
|
||||
url: http://docs.mainflux.io/
|
||||
|
||||
paths:
|
||||
/journal/user/{userID}:
|
||||
get:
|
||||
tags:
|
||||
- journal-log
|
||||
summary: List user journal log
|
||||
description: |
|
||||
Retrieves a list of journal. Due to performance concerns, data
|
||||
is retrieved in subsets. The API must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/user_id"
|
||||
- $ref: "#/components/parameters/offset"
|
||||
- $ref: "#/components/parameters/limit"
|
||||
- $ref: "#/components/parameters/operation"
|
||||
- $ref: "#/components/parameters/with_attributes"
|
||||
- $ref: "#/components/parameters/with_metadata"
|
||||
- $ref: "#/components/parameters/from"
|
||||
- $ref: "#/components/parameters/to"
|
||||
- $ref: "#/components/parameters/dir"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/JournalsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/journal/{entityType}/{id}:
|
||||
get:
|
||||
tags:
|
||||
- journal-log
|
||||
summary: List entity journal log
|
||||
description: |
|
||||
Retrieves a list of journal. Due to performance concerns, data
|
||||
is retrieved in subsets. The API must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/domain_id"
|
||||
- $ref: "#/components/parameters/entity_type"
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/offset"
|
||||
- $ref: "#/components/parameters/limit"
|
||||
- $ref: "#/components/parameters/operation"
|
||||
- $ref: "#/components/parameters/with_attributes"
|
||||
- $ref: "#/components/parameters/with_metadata"
|
||||
- $ref: "#/components/parameters/from"
|
||||
- $ref: "#/components/parameters/to"
|
||||
- $ref: "#/components/parameters/dir"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/JournalsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Journal:
|
||||
type: object
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
example: user.create
|
||||
description: Journal operation.
|
||||
occurred_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2024-01-11T12:05:07.449053Z"
|
||||
description: Time when the journal occurred.
|
||||
attributes:
|
||||
type: object
|
||||
description: Journal attributes.
|
||||
example:
|
||||
{
|
||||
"created_at": "2024-06-12T11:34:32.991591Z",
|
||||
"id": "29d425c8-542b-4614-8a4d-a5951945d720",
|
||||
"identity": "Gawne-Havlicek@email.com",
|
||||
"name": "Newgard-Frisina",
|
||||
"status": "enabled",
|
||||
"updated_at": "2024-06-12T11:34:33.116795Z",
|
||||
"updated_by": "ad228f20-4741-47c5-bef7-d871b541c019",
|
||||
}
|
||||
metadata:
|
||||
type: object
|
||||
description: Journal payload.
|
||||
example: { "Update": "Calvo-Felkins" }
|
||||
xml:
|
||||
name: journal
|
||||
|
||||
JournalPage:
|
||||
type: object
|
||||
properties:
|
||||
journals:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Journal"
|
||||
total:
|
||||
type: integer
|
||||
example: 1
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- journals
|
||||
- total
|
||||
- offset
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
example: { "error": "malformed entity specification" }
|
||||
|
||||
parameters:
|
||||
domain_id:
|
||||
name: domainID
|
||||
description: Unique identifier for a domain.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
entity_type:
|
||||
name: entityType
|
||||
description: Type of entity, e.g. user, group, client, etc.entityType
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- group
|
||||
- client
|
||||
- channel
|
||||
required: true
|
||||
example: group
|
||||
|
||||
user_id:
|
||||
name: userID
|
||||
description: Unique identifier for a user.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
id:
|
||||
name: id
|
||||
description: Unique identifier for an entity, e.g. group, channel or thing. Used together with entity_type.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
|
||||
limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 10
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "10"
|
||||
|
||||
operation:
|
||||
name: operation
|
||||
description: Journal operation.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: user.create
|
||||
|
||||
with_attributes:
|
||||
name: with_attributes
|
||||
description: Include journal attributes.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
example: true
|
||||
|
||||
with_metadata:
|
||||
name: with_metadata
|
||||
description: Include journal metadata.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
example: true
|
||||
|
||||
from:
|
||||
name: from
|
||||
description: Start date in unix time.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: int64
|
||||
required: false
|
||||
example: 1966777289
|
||||
|
||||
to:
|
||||
name: to
|
||||
description: End date in unix time.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: int64
|
||||
required: false
|
||||
example: 1966777289
|
||||
|
||||
dir:
|
||||
name: dir
|
||||
description: Sort direction.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
required: false
|
||||
example: desc
|
||||
|
||||
responses:
|
||||
JournalsPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/JournalPage"
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* User access: "Authorization: Bearer <user_access_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,292 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ Notifiers service
|
||||
description: |
|
||||
HTTP API for Notifiers service.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9014
|
||||
- url: https://localhost:9014
|
||||
- url: http://localhost:9015
|
||||
- url: https://localhost:9015
|
||||
|
||||
tags:
|
||||
- name: notifiers
|
||||
description: Everything about your Notifiers
|
||||
externalDocs:
|
||||
description: Find out more about notifiers
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/subscriptions:
|
||||
post:
|
||||
operationId: createSubscription
|
||||
summary: Create subscription
|
||||
description: Creates a new subscription give a topic and contact.
|
||||
tags:
|
||||
- notifiers
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/Create"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/Create"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"409":
|
||||
description: Failed due to using an existing topic and contact.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
operationId: listSubscriptions
|
||||
summary: List subscriptions
|
||||
description: List subscriptions given list parameters.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Topic"
|
||||
- $ref: "#/components/parameters/Contact"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Page"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/subscriptions/{id}:
|
||||
get:
|
||||
operationId: viewSubscription
|
||||
summary: Get subscription with the provided id
|
||||
description: Retrieves a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/View"
|
||||
"400":
|
||||
description: Failed due to malformed ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeSubscription
|
||||
summary: Delete subscription with the provided id
|
||||
description: Removes a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"204":
|
||||
description: Subscription removed
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Subscription:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
example: 01EWDVKBQSG80B6PQRS9PAAY35
|
||||
description: ULID id of the subscription.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: An id of the owner who created subscription.
|
||||
topic:
|
||||
type: string
|
||||
example: topic.subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
CreateSubscription:
|
||||
type: object
|
||||
properties:
|
||||
topic:
|
||||
type: string
|
||||
example: topic.subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
Page:
|
||||
type: object
|
||||
properties:
|
||||
subscriptions:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
|
||||
parameters:
|
||||
Id:
|
||||
name: id
|
||||
description: Unique identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Topic:
|
||||
name: topic
|
||||
description: Topic name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Contact:
|
||||
name: contact
|
||||
description: Subscription contact.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
Create:
|
||||
description: JSON-formatted document describing the new subscription to be created
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateSubscription"
|
||||
|
||||
responses:
|
||||
Create:
|
||||
description: Created a new subscription.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created subscription relative URL
|
||||
example: /subscriptions/{id}
|
||||
View:
|
||||
description: View subscription.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
links:
|
||||
delete:
|
||||
operationId: removeSubscription
|
||||
parameters:
|
||||
id: $response.body#/id
|
||||
Page:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Page"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ Provision service
|
||||
description: |
|
||||
HTTP API for Provision service
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstracmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9016
|
||||
- url: https://localhost:9016
|
||||
|
||||
tags:
|
||||
- name: provision
|
||||
description: Everything about your Provision
|
||||
externalDocs:
|
||||
description: Find out more about provision
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/{domainID}/mapping:
|
||||
post:
|
||||
summary: Adds new device to proxy
|
||||
description: Adds new device to proxy
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ProvisionReq"
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets current mapping.
|
||||
description: Gets current mapping. This can be used in UI
|
||||
so that when bootstrap config is created from UI matches
|
||||
configuration created with provision service.
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ProvisionRes"
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
requestBodies:
|
||||
ProvisionReq:
|
||||
description: MAC address of device or other identifier
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
external_key:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
ProvisionRes:
|
||||
description: Current mapping JSON representation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,313 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ reader service
|
||||
description: |
|
||||
HTTP API for reading messages.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9003
|
||||
- url: https://localhost:9003
|
||||
- url: http://localhost:9005
|
||||
- url: https://localhost:9005
|
||||
- url: http://localhost:9007
|
||||
- url: https://localhost:9007
|
||||
- url: http://localhost:9009
|
||||
- url: https://localhost:9009
|
||||
- url: http://localhost:9011
|
||||
- url: https://localhost:9011
|
||||
|
||||
tags:
|
||||
- name: readers
|
||||
description: Everything about your Readers
|
||||
externalDocs:
|
||||
description: Find out more about readers
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/channels/{chanId}/messages:
|
||||
get:
|
||||
operationId: getMessages
|
||||
summary: Retrieves messages sent to single channel
|
||||
description: |
|
||||
Retrieves a list of messages sent to specific channel. Due to
|
||||
performance concerns, data is retrieved in subsets. The API readers must
|
||||
ensure that the entire dataset is consumed either by making subsequent
|
||||
requests, or by increasing the subset size of the initial request.
|
||||
tags:
|
||||
- readers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Publisher"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Value"
|
||||
- $ref: "#/components/parameters/BoolValue"
|
||||
- $ref: "#/components/parameters/StringValue"
|
||||
- $ref: "#/components/parameters/DataValue"
|
||||
- $ref: "#/components/parameters/From"
|
||||
- $ref: "#/components/parameters/To"
|
||||
- $ref: "#/components/parameters/Aggregation"
|
||||
- $ref: "#/components/parameters/Interval"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MessagesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
operationId: health
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
MessagesPage:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: number
|
||||
description: Total number of items that are present on the system.
|
||||
offset:
|
||||
type: number
|
||||
description: Number of items that were skipped during retrieval.
|
||||
limit:
|
||||
type: number
|
||||
description: Size of the subset that was retrieved.
|
||||
messages:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
channel:
|
||||
type: integer
|
||||
description: Unique channel id.
|
||||
publisher:
|
||||
type: integer
|
||||
description: Unique publisher id.
|
||||
protocol:
|
||||
type: string
|
||||
description: Protocol name.
|
||||
name:
|
||||
type: string
|
||||
description: Measured parameter name.
|
||||
unit:
|
||||
type: string
|
||||
description: Value unit.
|
||||
value:
|
||||
type: number
|
||||
description: Measured value in number.
|
||||
stringValue:
|
||||
type: string
|
||||
description: Measured value in string format.
|
||||
boolValue:
|
||||
type: boolean
|
||||
description: Measured value in boolean format.
|
||||
dataValue:
|
||||
type: string
|
||||
description: Measured value in binary format.
|
||||
valueSum:
|
||||
type: number
|
||||
description: Sum value.
|
||||
time:
|
||||
type: number
|
||||
description: Time of measurement.
|
||||
updateTime:
|
||||
type: number
|
||||
description: Time of updating measurement.
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Publisher:
|
||||
name: Publisher
|
||||
description: Unique client identifier.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: SenML message name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Value:
|
||||
name: v
|
||||
description: SenML message value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
BoolValue:
|
||||
name: vb
|
||||
description: SenML message bool value.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
StringValue:
|
||||
name: vs
|
||||
description: SenML message string value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
DataValue:
|
||||
name: vd
|
||||
description: SenML message data value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Comparator:
|
||||
name: comparator
|
||||
description: Value comparison operator.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: eq
|
||||
enum:
|
||||
- eq
|
||||
- lt
|
||||
- le
|
||||
- gt
|
||||
- ge
|
||||
required: false
|
||||
From:
|
||||
name: from
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218556069
|
||||
required: false
|
||||
To:
|
||||
name: to
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218757503
|
||||
required: false
|
||||
Aggregation:
|
||||
name: aggregation
|
||||
description: Aggregation function.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- MAX
|
||||
- AVG
|
||||
- MIN
|
||||
- SUM
|
||||
- COUNT
|
||||
- max
|
||||
- min
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
example: MAX
|
||||
required: false
|
||||
Interval:
|
||||
name: interval
|
||||
description: Aggregation interval.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
example: 10s
|
||||
required: false
|
||||
|
||||
responses:
|
||||
MessagesPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
clientAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <client_key>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- clientAuth: []
|
||||
@@ -1,30 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Service status.
|
||||
enum:
|
||||
- pass
|
||||
version:
|
||||
type: string
|
||||
description: Service version.
|
||||
example: v0.14.0
|
||||
commit:
|
||||
type: string
|
||||
description: Service commit hash.
|
||||
example: 73362210dd2e04e389eaddb802cab3fe03976593
|
||||
description:
|
||||
type: string
|
||||
description: Service description.
|
||||
example: <service_name> service
|
||||
build_time:
|
||||
type: string
|
||||
description: Service build time.
|
||||
example: 2024-02-01_12:18:15
|
||||
instance_id:
|
||||
type: string
|
||||
description: Service instance ID.
|
||||
example: 8edbf8af-7db7-4218-bb4f-a8a929ff5266
|
||||
@@ -1,278 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
components:
|
||||
schemas:
|
||||
NewRole:
|
||||
type: object
|
||||
properties:
|
||||
role_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role unique identifier.
|
||||
role_name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
example: "roleName"
|
||||
entity_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Entity unique identifier.
|
||||
created_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role creator unique identifier.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
updated_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: ""
|
||||
description: Role updater unique identifier.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: ""
|
||||
description: Time when the channel was created.
|
||||
|
||||
Role:
|
||||
type: object
|
||||
properties:
|
||||
role_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role unique identifier.
|
||||
role_name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
example: "roleName"
|
||||
entity_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Entity unique identifier.
|
||||
created_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role creator unique identifier.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
updated_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role updater unique identifier.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
|
||||
CreateRoleObj:
|
||||
type: object
|
||||
properties:
|
||||
role_name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
example: "roleName"
|
||||
optional_actions:
|
||||
type: array
|
||||
description: List of optional actions.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"read",
|
||||
"update",
|
||||
]
|
||||
optional_members:
|
||||
type: array
|
||||
minItems: 0
|
||||
description: List of optional members.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb",
|
||||
"c01ed106-e52d-4aa4-bed3-39f360177cfa",
|
||||
]
|
||||
|
||||
UpdateRoleObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
|
||||
RolesPage:
|
||||
type: object
|
||||
properties:
|
||||
roles:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Role'
|
||||
description: List of roles.
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of roles.
|
||||
example: 1
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
|
||||
RoleActionsObj:
|
||||
type: object
|
||||
properties:
|
||||
actions:
|
||||
type: array
|
||||
description: List of actions to be added to a role.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"read",
|
||||
"update",
|
||||
]
|
||||
|
||||
RoleMembersObj:
|
||||
type: object
|
||||
properties:
|
||||
members:
|
||||
type: array
|
||||
description: List of members to be added to a role.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb",
|
||||
"c01ed106-e52d-4aa4-bed3-39f360177cfa",
|
||||
]
|
||||
|
||||
AvailableActionsObj:
|
||||
type: object
|
||||
properties:
|
||||
available_actions:
|
||||
type: array
|
||||
description: List of all available actions.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"read",
|
||||
"update",
|
||||
]
|
||||
|
||||
parameters:
|
||||
RoleName:
|
||||
name: roleName
|
||||
description: Role's name.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
example: "roleName"
|
||||
|
||||
requestBodies:
|
||||
CreateRoleReq:
|
||||
description: JSON- formatted object decsribing a new role to be created.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateRoleObj'
|
||||
|
||||
UpdateRoleReq:
|
||||
description: JSON- formatted object decsribing a role to be updated.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateRoleObj'
|
||||
|
||||
AddRoleActionsReq:
|
||||
description: JSON- formatted object decsribing an action to be added to a role.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleActionsObj'
|
||||
|
||||
AddRoleMembersReq:
|
||||
description: JSON- formatted object decsribing a member to be added to a role.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleMembersObj'
|
||||
|
||||
responses:
|
||||
CreateRoleRes:
|
||||
description: Role created successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NewRole'
|
||||
|
||||
ListRolesRes:
|
||||
description: Role retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RolesPage'
|
||||
|
||||
GetRoleRes:
|
||||
description: Role retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Role'
|
||||
|
||||
AddRoleActionsRes:
|
||||
description: Action added to role successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleActionsObj'
|
||||
|
||||
ListRoleActionsRes:
|
||||
description: Role actions retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleActionsObj'
|
||||
|
||||
AddRoleMembersRes:
|
||||
description: Member added to role successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleMembersObj'
|
||||
|
||||
ListRoleMembersRes:
|
||||
description: Role members retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleMembersObj'
|
||||
|
||||
ListAvailableActionsRes:
|
||||
description: Available actions retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AvailableActionsObj'
|
||||
@@ -1,431 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ twins service
|
||||
description: |
|
||||
HTTP API for managing digital twins and their states.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9018
|
||||
- url: https://localhost:9018
|
||||
|
||||
tags:
|
||||
- name: twins
|
||||
description: Everything about your Twins
|
||||
externalDocs:
|
||||
description: Find out more about twins
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/twins:
|
||||
post:
|
||||
operationId: createTwin
|
||||
summary: Adds new twin
|
||||
description: |
|
||||
Adds new twin to the list of twins owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- twins
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TwinReq"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/TwinCreateRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: getTwins
|
||||
summary: Retrieves twins
|
||||
description: |
|
||||
Retrieves a list of twins. Due to performance concerns, data
|
||||
is retrieved in subsets.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/TwinsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/twins/{twinID}:
|
||||
get:
|
||||
operationId: getTwin
|
||||
summary: Retrieves twin info
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/TwinRes"
|
||||
"400":
|
||||
description: Failed due to malformed twin's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateTwin
|
||||
summary: Updates twin info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the twin's ID cannot be changed.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TwinReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Twin updated.
|
||||
"400":
|
||||
description: Failed due to malformed twin's ID or malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeTwin
|
||||
summary: Removes a twin
|
||||
description: Removes a twin.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
responses:
|
||||
"204":
|
||||
description: Twin removed.
|
||||
"400":
|
||||
description: Failed due to malformed twin's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/states/{twinID}:
|
||||
get:
|
||||
operationId: getStates
|
||||
summary: Retrieves states of twin with id twinID
|
||||
description: |
|
||||
Retrieves a list of states. Due to performance concerns, data
|
||||
is retrieved in subsets.
|
||||
tags:
|
||||
- states
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/StatesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Twin name
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: |
|
||||
Metadata filter. Filtering is performed matching the parameter with
|
||||
metadata on top level. Parameter is json.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
minimum: 0
|
||||
required: false
|
||||
TwinID:
|
||||
name: twinID
|
||||
description: Unique twin identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
minimum: 1
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Attribute:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name of the attribute.
|
||||
channel:
|
||||
type: string
|
||||
description: SuperMQ channel used by attribute.
|
||||
subtopic:
|
||||
type: string
|
||||
description: Subtopic used by attribute.
|
||||
persist_state:
|
||||
type: boolean
|
||||
description: Trigger state creation based on the attribute.
|
||||
Definition:
|
||||
type: object
|
||||
properties:
|
||||
delta:
|
||||
type: number
|
||||
description: Minimal time delay before new state creation.
|
||||
attributes:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/Attribute"
|
||||
TwinReqObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Free-form twin name.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded twin's data.
|
||||
definition:
|
||||
$ref: "#/components/schemas/Definition"
|
||||
TwinResObj:
|
||||
type: object
|
||||
properties:
|
||||
owner:
|
||||
type: string
|
||||
description: Email address of SuperMQ user that owns twin.
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique twin identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form twin name.
|
||||
revision:
|
||||
type: number
|
||||
description: Oridnal revision number of twin.
|
||||
created:
|
||||
type: string
|
||||
format: date
|
||||
description: Twin creation date and time.
|
||||
updated:
|
||||
type: string
|
||||
format: date
|
||||
description: Twin update date and time.
|
||||
definitions:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/Definition"
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded twin's data.
|
||||
TwinsPage:
|
||||
type: object
|
||||
properties:
|
||||
twins:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/TwinResObj"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- twins
|
||||
State:
|
||||
type: object
|
||||
properties:
|
||||
twin_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of twin state belongs to.
|
||||
id:
|
||||
type: number
|
||||
description: State position in a time row of states.
|
||||
created:
|
||||
type: string
|
||||
format: date
|
||||
description: State creation date.
|
||||
payload:
|
||||
type: object
|
||||
description: Object-encoded states's payload.
|
||||
StatesPage:
|
||||
type: object
|
||||
properties:
|
||||
states:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/State"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- states
|
||||
|
||||
requestBodies:
|
||||
TwinReq:
|
||||
description: JSON-formatted document describing the twin to create or update.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TwinReqObj"
|
||||
required: true
|
||||
|
||||
responses:
|
||||
TwinCreateRes:
|
||||
description: Created twin's relative URL (i.e. /twins/{twinID}).
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
TwinRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TwinResObj"
|
||||
links:
|
||||
update:
|
||||
operationId: updateTwin
|
||||
parameters:
|
||||
twinID: $response.body#/id
|
||||
delete:
|
||||
operationId: removeTwin
|
||||
parameters:
|
||||
twinID: $response.body#/id
|
||||
states:
|
||||
operationId: getStates
|
||||
parameters:
|
||||
twinID: $response.body#/id
|
||||
TwinsPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TwinsPage"
|
||||
StatesPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StatesPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
File diff suppressed because it is too large
Load Diff
-159
@@ -1,159 +0,0 @@
|
||||
# Auth - Authentication and Authorization service
|
||||
|
||||
Auth service provides authentication features as an API for managing authentication keys as well as administering groups of entities - `clients` and `users`.
|
||||
|
||||
## Authentication
|
||||
|
||||
User service is using Auth service gRPC API to obtain login token or password reset token. Authentication key consists of the following fields:
|
||||
|
||||
- ID - key ID
|
||||
- Type - one of the three types described below
|
||||
- IssuerID - an ID of the SuperMQ User who issued the key
|
||||
- Subject - user ID for which the key is issued
|
||||
- IssuedAt - the timestamp when the key is issued
|
||||
- ExpiresAt - the timestamp after which the key is invalid
|
||||
|
||||
There are four types of authentication keys:
|
||||
|
||||
- Access key - keys issued to the user upon login request
|
||||
- Refresh key - keys used to generate new access keys
|
||||
- Recovery key - password recovery key
|
||||
- API key - keys issued upon the user request
|
||||
- Invitation key - keys used to invite new users
|
||||
|
||||
Authentication keys are represented and distributed by the corresponding [JWT](jwt.io).
|
||||
|
||||
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user.
|
||||
|
||||
API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. This also means that, despite being used as a JWT, it requires a query to the database to validate the API key. The user with API key can perform all the same actions as the user with login key (can act on behalf of the user for Client, Channel, or user profile management), _except issuing new API keys_.
|
||||
|
||||
Recovery key is the password recovery key. It's short-lived token used for password recovery process.
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough understanding of SuperMQ, please check out the [official documentation][doc].
|
||||
|
||||
The following actions are supported:
|
||||
|
||||
- create (all key types)
|
||||
- verify (all key types)
|
||||
- obtain (API keys only)
|
||||
- revoke (API keys only)
|
||||
|
||||
## Domains
|
||||
|
||||
Domains are used to group users and clients. Each domain has a unique alias that is used to identify the domain. Domains are used to group users and their entities.
|
||||
|
||||
Domain consists of the following fields:
|
||||
|
||||
- ID - UUID uniquely representing domain
|
||||
- Name - name of the domain
|
||||
- Tags - array of tags
|
||||
- Metadata - Arbitrary, object-encoded domain's data
|
||||
- Alias - unique alias of the domain
|
||||
- CreatedAt - timestamp at which the domain is created
|
||||
- UpdatedAt - timestamp at which the domain is updated
|
||||
- UpdatedBy - user that updated the domain
|
||||
- CreatedBy - user that created the domain
|
||||
- Status - domain status
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------- | ----------------------------------------------------------------------- | ------------------------------ |
|
||||
| SMQ_AUTH_LOG_LEVEL | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| SMQ_AUTH_DB_HOST | Database host address | localhost |
|
||||
| SMQ_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| SMQ_AUTH_DB_USER | Database user | supermq |
|
||||
| SMQ_AUTH_DB_PASSWORD | Database password | supermq |
|
||||
| SMQ_AUTH_DB_NAME | Name of the database used by the service | auth |
|
||||
| SMQ_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| SMQ_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| SMQ_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| SMQ_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| SMQ_AUTH_HTTP_HOST | Auth service HTTP host | "" |
|
||||
| SMQ_AUTH_HTTP_PORT | Auth service HTTP port | 8189 |
|
||||
| SMQ_AUTH_HTTP_SERVER_CERT | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| SMQ_AUTH_HTTP_SERVER_KEY | Path to the PEM encoded HTTP server key file | "" |
|
||||
| SMQ_AUTH_GRPC_HOST | Auth service gRPC host | "" |
|
||||
| SMQ_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| SMQ_AUTH_GRPC_SERVER_CERT | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| SMQ_AUTH_GRPC_SERVER_KEY | Path to the PEM encoded gRPC server key file | "" |
|
||||
| SMQ_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| SMQ_AUTH_GRPC_CLIENT_CA_CERTS | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| SMQ_AUTH_SECRET_KEY | String used for signing tokens | secret |
|
||||
| SMQ_AUTH_ACCESS_TOKEN_DURATION | The access token expiration period | 1h |
|
||||
| SMQ_AUTH_REFRESH_TOKEN_DURATION | The refresh token expiration period | 24h |
|
||||
| SMQ_AUTH_INVITATION_DURATION | The invitation token expiration period | 168h |
|
||||
| SMQ_SPICEDB_HOST | SpiceDB host address | localhost |
|
||||
| SMQ_SPICEDB_PORT | SpiceDB host port | 50051 |
|
||||
| SMQ_SPICEDB_PRE_SHARED_KEY | SpiceDB pre-shared key | 12345678 |
|
||||
| SMQ_SPICEDB_SCHEMA_FILE | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| SMQ_JAEGER_URL | Jaeger server URL | <http://jaeger:4318/v1/traces> |
|
||||
| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true |
|
||||
| SMQ_AUTH_ADAPTER_INSTANCE_ID | Adapter instance ID | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`auth`](https://github.com/absmach/supermq/blob/main/docker/docker-compose.yml) service section in docker-compose file to see how service is deployed.
|
||||
|
||||
Running this service outside of container requires working instance of the postgres database, SpiceDB, and Jaeger server.
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/supermq
|
||||
|
||||
cd supermq
|
||||
|
||||
# compile the service
|
||||
make auth
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
SMQ_AUTH_LOG_LEVEL=info \
|
||||
SMQ_AUTH_DB_HOST=localhost \
|
||||
SMQ_AUTH_DB_PORT=5432 \
|
||||
SMQ_AUTH_DB_USER=supermq \
|
||||
SMQ_AUTH_DB_PASSWORD=supermq \
|
||||
SMQ_AUTH_DB_NAME=auth \
|
||||
SMQ_AUTH_DB_SSL_MODE=disable \
|
||||
SMQ_AUTH_DB_SSL_CERT="" \
|
||||
SMQ_AUTH_DB_SSL_KEY="" \
|
||||
SMQ_AUTH_DB_SSL_ROOT_CERT="" \
|
||||
SMQ_AUTH_HTTP_HOST=localhost \
|
||||
SMQ_AUTH_HTTP_PORT=8189 \
|
||||
SMQ_AUTH_HTTP_SERVER_CERT="" \
|
||||
SMQ_AUTH_HTTP_SERVER_KEY="" \
|
||||
SMQ_AUTH_GRPC_HOST=localhost \
|
||||
SMQ_AUTH_GRPC_PORT=8181 \
|
||||
SMQ_AUTH_GRPC_SERVER_CERT="" \
|
||||
SMQ_AUTH_GRPC_SERVER_KEY="" \
|
||||
SMQ_AUTH_GRPC_SERVER_CA_CERTS="" \
|
||||
SMQ_AUTH_GRPC_CLIENT_CA_CERTS="" \
|
||||
SMQ_AUTH_SECRET_KEY=secret \
|
||||
SMQ_AUTH_ACCESS_TOKEN_DURATION=1h \
|
||||
SMQ_AUTH_REFRESH_TOKEN_DURATION=24h \
|
||||
SMQ_AUTH_INVITATION_DURATION=168h \
|
||||
SMQ_SPICEDB_HOST=localhost \
|
||||
SMQ_SPICEDB_PORT=50051 \
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY=12345678 \
|
||||
SMQ_SPICEDB_SCHEMA_FILE=./docker/spicedb/schema.zed \
|
||||
SMQ_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
SMQ_JAEGER_TRACE_RATIO=1.0 \
|
||||
SMQ_SEND_TELEMETRY=true \
|
||||
SMQ_AUTH_ADAPTER_INSTANCE_ID="" \
|
||||
$GOBIN/supermq-auth
|
||||
```
|
||||
|
||||
Setting `SMQ_AUTH_HTTP_SERVER_CERT` and `SMQ_AUTH_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
Setting `SMQ_AUTH_GRPC_SERVER_CERT` and `SMQ_AUTH_GRPC_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key. Setting `SMQ_AUTH_GRPC_SERVER_CA_CERTS` will enable TLS against the service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs. Setting `SMQ_AUTH_GRPC_CLIENT_CA_CERTS` will enable TLS against the service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.supermq.abstractmachines.fr/?urls.primaryName=auth.yml).
|
||||
|
||||
[doc]: https://docs.supermq.abstractmachines.fr
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of Auth service HTTP API.
|
||||
package api
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
grpcapi "github.com/absmach/supermq/auth/api/grpc"
|
||||
grpcAuthV1 "github.com/absmach/supermq/internal/grpc/auth/v1"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const authSvcName = "auth.v1.AuthService"
|
||||
|
||||
type authGrpcClient struct {
|
||||
authenticate endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
var _ grpcAuthV1.AuthServiceClient = (*authGrpcClient)(nil)
|
||||
|
||||
// NewAuthClient returns new auth gRPC client instance.
|
||||
func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.AuthServiceClient {
|
||||
return &authGrpcClient{
|
||||
authenticate: kitgrpc.NewClient(
|
||||
conn,
|
||||
authSvcName,
|
||||
"Authenticate",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
grpcAuthV1.AuthNRes{},
|
||||
).Endpoint(),
|
||||
authorize: kitgrpc.NewClient(
|
||||
conn,
|
||||
authSvcName,
|
||||
"Authorize",
|
||||
encodeAuthorizeRequest,
|
||||
decodeAuthorizeResponse,
|
||||
grpcAuthV1.AuthZRes{},
|
||||
).Endpoint(),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client authGrpcClient) Authenticate(ctx context.Context, token *grpcAuthV1.AuthNReq, _ ...grpc.CallOption) (*grpcAuthV1.AuthNRes, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authenticate(ctx, authenticateReq{token: token.GetToken()})
|
||||
if err != nil {
|
||||
return &grpcAuthV1.AuthNRes{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
ir := res.(authenticateRes)
|
||||
return &grpcAuthV1.AuthNRes{Id: ir.id, UserId: ir.userID, DomainId: ir.domainID}, nil
|
||||
}
|
||||
|
||||
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authenticateReq)
|
||||
return &grpcAuthV1.AuthNReq{Token: req.token}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*grpcAuthV1.AuthNRes)
|
||||
return authenticateRes{id: res.GetId(), userID: res.GetUserId(), domainID: res.GetDomainId()}, nil
|
||||
}
|
||||
|
||||
func (client authGrpcClient) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authorize(ctx, authReq{
|
||||
Domain: req.GetDomain(),
|
||||
SubjectType: req.GetSubjectType(),
|
||||
Subject: req.GetSubject(),
|
||||
SubjectKind: req.GetSubjectKind(),
|
||||
Relation: req.GetRelation(),
|
||||
Permission: req.GetPermission(),
|
||||
ObjectType: req.GetObjectType(),
|
||||
Object: req.GetObject(),
|
||||
})
|
||||
if err != nil {
|
||||
return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
return &grpcAuthV1.AuthZRes{Authorized: ar.authorized, Id: ar.id}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*grpcAuthV1.AuthZRes)
|
||||
return authorizeRes{authorized: res.Authorized, id: res.Id}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &grpcAuthV1.AuthZReq{
|
||||
Domain: req.Domain,
|
||||
SubjectType: req.SubjectType,
|
||||
Subject: req.Subject,
|
||||
SubjectKind: req.SubjectKind,
|
||||
Relation: req.Relation,
|
||||
Permission: req.Permission,
|
||||
ObjectType: req.ObjectType,
|
||||
Object: req.Object,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package auth contains implementation of Auth service gRPC API.
|
||||
package auth
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func authenticateEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(authenticateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return authenticateRes{}, err
|
||||
}
|
||||
|
||||
key, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return authenticateRes{}, err
|
||||
}
|
||||
|
||||
return authenticateRes{id: key.Subject, userID: key.User, domainID: key.Domain}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(authReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
err := svc.Authorize(ctx, policies.Policy{
|
||||
Domain: req.Domain,
|
||||
SubjectType: req.SubjectType,
|
||||
SubjectKind: req.SubjectKind,
|
||||
Subject: req.Subject,
|
||||
Relation: req.Relation,
|
||||
Permission: req.Permission,
|
||||
ObjectType: req.ObjectType,
|
||||
Object: req.Object,
|
||||
})
|
||||
if err != nil {
|
||||
return authorizeRes{authorized: false}, err
|
||||
}
|
||||
return authorizeRes{authorized: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
grpcapi "github.com/absmach/supermq/auth/api/grpc/auth"
|
||||
grpcAuthV1 "github.com/absmach/supermq/internal/grpc/auth/v1"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
clientsType = "clients"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
groupName = "smqx"
|
||||
adminpermission = "admin"
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
refreshDuration = 24 * time.Hour
|
||||
invalidDuration = 7 * 24 * time.Hour
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
validPolicy = "valid"
|
||||
)
|
||||
|
||||
var (
|
||||
domainID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcAuthV1.RegisterAuthServiceServer(server, grpcapi.NewAuthServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
defer conn.Close()
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
idt *grpcAuthV1.AuthNRes
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authenticate user with valid user token",
|
||||
token: validToken,
|
||||
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: email, DomainId: domainID},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid user token",
|
||||
token: "invalid",
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with empty token",
|
||||
token: "",
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Identify", mock.Anything, mock.Anything, mock.Anything).Return(auth.Key{Subject: id, User: email, Domain: domainID}, tc.svcErr)
|
||||
idt, err := grpcClient.Authenticate(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
defer conn.Close()
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
authRequest *grpcAuthV1.AuthZReq
|
||||
authResponse *grpcAuthV1.AuthZRes
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminpermission,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminpermission,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty subject",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: "",
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminpermission,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty subject type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: "",
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminpermission,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty object",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: "",
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminpermission,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty object type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: "",
|
||||
Relation: memberRelation,
|
||||
Permission: adminpermission,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty permission",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: "",
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMalformedPolicyPer,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
svccall := svc.On("Authorize", mock.Anything, mock.Anything).Return(tc.err)
|
||||
ar, err := grpcClient.Authorize(context.Background(), tc.authRequest)
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svccall.Unset()
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
)
|
||||
|
||||
type authenticateReq struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (req authenticateReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// authReq represents authorization request. It contains:
|
||||
// 1. subject - an action invoker
|
||||
// 2. object - an entity over which action will be executed
|
||||
// 3. action - type of action that will be executed (read/write).
|
||||
type authReq struct {
|
||||
Domain string
|
||||
SubjectType string
|
||||
SubjectKind string
|
||||
Subject string
|
||||
Relation string
|
||||
Permission string
|
||||
ObjectType string
|
||||
Object string
|
||||
}
|
||||
|
||||
func (req authReq) validate() error {
|
||||
if req.Subject == "" || req.SubjectType == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Object == "" || req.ObjectType == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Permission == "" {
|
||||
return apiutil.ErrMalformedPolicyPer
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
type authenticateRes struct {
|
||||
id string
|
||||
userID string
|
||||
domainID string
|
||||
}
|
||||
|
||||
type authorizeRes struct {
|
||||
id string
|
||||
authorized bool
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
grpcapi "github.com/absmach/supermq/auth/api/grpc"
|
||||
grpcAuthV1 "github.com/absmach/supermq/internal/grpc/auth/v1"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
)
|
||||
|
||||
var _ grpcAuthV1.AuthServiceServer = (*authGrpcServer)(nil)
|
||||
|
||||
type authGrpcServer struct {
|
||||
grpcAuthV1.UnimplementedAuthServiceServer
|
||||
authorize kitgrpc.Handler
|
||||
authenticate kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewAuthServer returns new AuthnServiceServer instance.
|
||||
func NewAuthServer(svc auth.Service) grpcAuthV1.AuthServiceServer {
|
||||
return &authGrpcServer{
|
||||
authorize: kitgrpc.NewServer(
|
||||
(authorizeEndpoint(svc)),
|
||||
decodeAuthorizeRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
|
||||
authenticate: kitgrpc.NewServer(
|
||||
(authenticateEndpoint(svc)),
|
||||
decodeAuthenticateRequest,
|
||||
encodeAuthenticateResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *authGrpcServer) Authenticate(ctx context.Context, req *grpcAuthV1.AuthNReq) (*grpcAuthV1.AuthNRes, error) {
|
||||
_, res, err := s.authenticate.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcAuthV1.AuthNRes), nil
|
||||
}
|
||||
|
||||
func (s *authGrpcServer) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq) (*grpcAuthV1.AuthZRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcAuthV1.AuthZRes), nil
|
||||
}
|
||||
|
||||
func decodeAuthenticateRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*grpcAuthV1.AuthNReq)
|
||||
return authenticateReq{token: req.GetToken()}, nil
|
||||
}
|
||||
|
||||
func encodeAuthenticateResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(authenticateRes)
|
||||
return &grpcAuthV1.AuthNRes{Id: res.id, UserId: res.userID, DomainId: res.domainID}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*grpcAuthV1.AuthZReq)
|
||||
return authReq{
|
||||
Domain: req.GetDomain(),
|
||||
SubjectType: req.GetSubjectType(),
|
||||
SubjectKind: req.GetSubjectKind(),
|
||||
Subject: req.GetSubject(),
|
||||
Relation: req.GetRelation(),
|
||||
Permission: req.GetPermission(),
|
||||
ObjectType: req.GetObjectType(),
|
||||
Object: req.GetObject(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &grpcAuthV1.AuthZRes{Authorized: res.authorized, Id: res.id}, nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
grpcapi "github.com/absmach/supermq/auth/api/grpc"
|
||||
grpcTokenV1 "github.com/absmach/supermq/internal/grpc/token/v1"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const tokenSvcName = "token.v1.TokenService"
|
||||
|
||||
type tokenGrpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
refresh endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
var _ grpcTokenV1.TokenServiceClient = (*tokenGrpcClient)(nil)
|
||||
|
||||
// NewAuthClient returns new auth gRPC client instance.
|
||||
func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) grpcTokenV1.TokenServiceClient {
|
||||
return &tokenGrpcClient{
|
||||
issue: kitgrpc.NewClient(
|
||||
conn,
|
||||
tokenSvcName,
|
||||
"Issue",
|
||||
encodeIssueRequest,
|
||||
decodeIssueResponse,
|
||||
grpcTokenV1.Token{},
|
||||
).Endpoint(),
|
||||
refresh: kitgrpc.NewClient(
|
||||
conn,
|
||||
tokenSvcName,
|
||||
"Refresh",
|
||||
encodeRefreshRequest,
|
||||
decodeRefreshResponse,
|
||||
grpcTokenV1.Token{},
|
||||
).Endpoint(),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client tokenGrpcClient) Issue(ctx context.Context, req *grpcTokenV1.IssueReq, _ ...grpc.CallOption) (*grpcTokenV1.Token, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.issue(ctx, issueReq{
|
||||
userID: req.GetUserId(),
|
||||
keyType: auth.KeyType(req.GetType()),
|
||||
})
|
||||
if err != nil {
|
||||
return &grpcTokenV1.Token{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.Token), nil
|
||||
}
|
||||
|
||||
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &grpcTokenV1.IssueReq{
|
||||
UserId: req.userID,
|
||||
Type: uint32(req.keyType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
return grpcRes, nil
|
||||
}
|
||||
|
||||
func (client tokenGrpcClient) Refresh(ctx context.Context, req *grpcTokenV1.RefreshReq, _ ...grpc.CallOption) (*grpcTokenV1.Token, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.refresh(ctx, refreshReq{refreshToken: req.GetRefreshToken()})
|
||||
if err != nil {
|
||||
return &grpcTokenV1.Token{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.Token), nil
|
||||
}
|
||||
|
||||
func encodeRefreshRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(refreshReq)
|
||||
return &grpcTokenV1.RefreshReq{RefreshToken: req.refreshToken}, nil
|
||||
}
|
||||
|
||||
func decodeRefreshResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
return grpcRes, nil
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of Auth service gRPC API.
|
||||
package token
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
|
||||
key := auth.Key{
|
||||
Type: req.keyType,
|
||||
User: req.userID,
|
||||
}
|
||||
tkn, err := svc.Issue(ctx, "", key)
|
||||
if err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
ret := issueRes{
|
||||
accessToken: tkn.AccessToken,
|
||||
refreshToken: tkn.RefreshToken,
|
||||
accessType: tkn.AccessType,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func refreshEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(refreshReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
|
||||
key := auth.Key{Type: auth.RefreshKey}
|
||||
tkn, err := svc.Issue(ctx, req.refreshToken, key)
|
||||
if err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
ret := issueRes{
|
||||
accessToken: tkn.AccessToken,
|
||||
refreshToken: tkn.RefreshToken,
|
||||
accessType: tkn.AccessType,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
grpcapi "github.com/absmach/supermq/auth/api/grpc/token"
|
||||
grpcTokenV1 "github.com/absmach/supermq/internal/grpc/token/v1"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8082
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
clientsType = "clients"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
groupName = "smqx"
|
||||
adminpermission = "admin"
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
refreshDuration = 24 * time.Hour
|
||||
invalidDuration = 7 * 24 * time.Hour
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
validPolicy = "valid"
|
||||
)
|
||||
|
||||
var (
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcTokenV1.RegisterTokenServiceServer(server, grpcapi.NewTokenServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
defer conn.Close()
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userId string
|
||||
kind auth.KeyType
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
userId: validID,
|
||||
kind: auth.AccessKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
userId: validID,
|
||||
kind: auth.RecoveryKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthenticated",
|
||||
userId: validID,
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
userId: validID,
|
||||
kind: 32,
|
||||
issueResponse: auth.Token{},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that does notexist",
|
||||
userId: "",
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Issue(context.Background(), &grpcTokenV1.IssueReq{UserId: tc.userId, Type: uint32(tc.kind)})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
defer conn.Close()
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "refresh token with valid token",
|
||||
token: validToken,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with invalid token",
|
||||
token: inValidToken,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with empty token",
|
||||
token: "",
|
||||
issueResponse: auth.Token{},
|
||||
err: apiutil.ErrMissingSecret,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
)
|
||||
|
||||
type issueReq struct {
|
||||
userID string
|
||||
keyType auth.KeyType
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.keyType != auth.AccessKey &&
|
||||
req.keyType != auth.APIKey &&
|
||||
req.keyType != auth.RecoveryKey &&
|
||||
req.keyType != auth.InvitationKey {
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type refreshReq struct {
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
func (req refreshReq) validate() error {
|
||||
if req.refreshToken == "" {
|
||||
return apiutil.ErrMissingSecret
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token
|
||||
|
||||
type issueRes struct {
|
||||
accessToken string
|
||||
refreshToken string
|
||||
accessType string
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
grpcapi "github.com/absmach/supermq/auth/api/grpc"
|
||||
grpcTokenV1 "github.com/absmach/supermq/internal/grpc/token/v1"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
)
|
||||
|
||||
var _ grpcTokenV1.TokenServiceServer = (*tokenGrpcServer)(nil)
|
||||
|
||||
type tokenGrpcServer struct {
|
||||
grpcTokenV1.UnimplementedTokenServiceServer
|
||||
issue kitgrpc.Handler
|
||||
refresh kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewAuthServer returns new AuthnServiceServer instance.
|
||||
func NewTokenServer(svc auth.Service) grpcTokenV1.TokenServiceServer {
|
||||
return &tokenGrpcServer{
|
||||
issue: kitgrpc.NewServer(
|
||||
(issueEndpoint(svc)),
|
||||
decodeIssueRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
refresh: kitgrpc.NewServer(
|
||||
(refreshEndpoint(svc)),
|
||||
decodeRefreshRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tokenGrpcServer) Issue(ctx context.Context, req *grpcTokenV1.IssueReq) (*grpcTokenV1.Token, error) {
|
||||
_, res, err := s.issue.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.Token), nil
|
||||
}
|
||||
|
||||
func (s *tokenGrpcServer) Refresh(ctx context.Context, req *grpcTokenV1.RefreshReq) (*grpcTokenV1.Token, error) {
|
||||
_, res, err := s.refresh.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.Token), nil
|
||||
}
|
||||
|
||||
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*grpcTokenV1.IssueReq)
|
||||
return issueReq{
|
||||
userID: req.GetUserId(),
|
||||
keyType: auth.KeyType(req.GetType()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeRefreshRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*grpcTokenV1.RefreshReq)
|
||||
return refreshReq{refreshToken: req.GetRefreshToken()}, nil
|
||||
}
|
||||
|
||||
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(issueRes)
|
||||
|
||||
return &grpcTokenV1.Token{
|
||||
AccessToken: res.accessToken,
|
||||
RefreshToken: &res.refreshToken,
|
||||
AccessType: res.accessType,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func EncodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
errors.Contains(err, svcerr.ErrInvalidPolicy),
|
||||
err == apiutil.ErrInvalidAuthKey,
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMalformedPolicyAct:
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrAuthentication),
|
||||
errors.Contains(err, auth.ErrKeyExpired),
|
||||
err == apiutil.ErrMissingEmail,
|
||||
err == apiutil.ErrBearerToken:
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrAuthorization),
|
||||
errors.Contains(err, svcerr.ErrDomainAuthorization):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Contains(err, svcerr.ErrConflict):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeError(err error) error {
|
||||
if st, ok := status.FromError(err); ok {
|
||||
switch st.Code() {
|
||||
case codes.NotFound:
|
||||
return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message()))
|
||||
case codes.InvalidArgument:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message()))
|
||||
case codes.AlreadyExists:
|
||||
return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message()))
|
||||
case codes.Unauthenticated:
|
||||
return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message()))
|
||||
case codes.OK:
|
||||
if msg := st.Message(); msg != "" {
|
||||
return errors.Wrap(errors.ErrUnidentified, errors.New(msg))
|
||||
}
|
||||
return nil
|
||||
case codes.FailedPrecondition:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message()))
|
||||
case codes.PermissionDenied:
|
||||
return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message()))
|
||||
default:
|
||||
return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message()))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package http
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueKeyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
newKey := auth.Key{
|
||||
IssuedAt: now,
|
||||
Type: req.Type,
|
||||
}
|
||||
|
||||
duration := time.Duration(req.Duration * time.Second)
|
||||
if duration != 0 {
|
||||
exp := now.Add(duration)
|
||||
newKey.ExpiresAt = exp
|
||||
}
|
||||
|
||||
tkn, err := svc.Issue(ctx, req.token, newKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := issueKeyRes{
|
||||
Value: tkn.AccessToken,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(keyReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := svc.RetrieveKey(ctx, req.token, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := retrieveKeyRes{
|
||||
ID: key.ID,
|
||||
IssuerID: key.Issuer,
|
||||
Subject: key.Subject,
|
||||
Type: key.Type,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
ret.ExpiresAt = &key.ExpiresAt
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(keyReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Revoke(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return revokeKeyRes{}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
httpapi "github.com/absmach/supermq/auth/api/http"
|
||||
"github.com/absmach/supermq/auth/jwt"
|
||||
"github.com/absmach/supermq/auth/mocks"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
policymocks "github.com/absmach/supermq/pkg/policies/mocks"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
refreshDuration = 24 * time.Hour
|
||||
invalidDuration = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type issueRequest struct {
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
Type uint32 `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
|
||||
req.Header.Set("Referer", "http://localhost")
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() (auth.Service, *mocks.KeyRepository) {
|
||||
krepo := new(mocks.KeyRepository)
|
||||
idProvider := uuid.NewMock()
|
||||
pService := new(policymocks.Service)
|
||||
pEvaluator := new(policymocks.Evaluator)
|
||||
t := jwt.New([]byte(secret))
|
||||
|
||||
return auth.New(krepo, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), krepo
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc, smqlog.NewMock(), "")
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc, krepo := newService()
|
||||
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
lk := issueRequest{Type: uint32(auth.AccessKey)}
|
||||
ak := issueRequest{Type: uint32(auth.APIKey), Duration: time.Hour}
|
||||
rk := issueRequest{Type: uint32(auth.RecoveryKey)}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
ct string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "issue login key with empty token",
|
||||
req: toJSON(lk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue login key wrong content type",
|
||||
req: toJSON(lk),
|
||||
ct: "",
|
||||
token: token.AccessToken,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key wrong content type",
|
||||
req: toJSON(rk),
|
||||
ct: "",
|
||||
token: token.AccessToken,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key with an invalid token",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key with empty token",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid request",
|
||||
req: "{",
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON",
|
||||
req: "{invalid}",
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON content",
|
||||
req: `{"Type":{"key":"AccessToken"}}`,
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/keys", ts.URL),
|
||||
contentType: tc.ct,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
repocall := krepo.On("Save", mock.Anything, mock.Anything).Return("", nil)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
repocall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc, krepo := newService()
|
||||
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), Subject: id}
|
||||
|
||||
repocall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil)
|
||||
k, err := svc.Issue(context.Background(), token.AccessToken, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
repocall.Unset()
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
key auth.Key
|
||||
status int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: k.AccessToken,
|
||||
token: token.AccessToken,
|
||||
key: auth.Key{
|
||||
Subject: id,
|
||||
Type: auth.AccessKey,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(refreshDuration),
|
||||
},
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing key",
|
||||
id: "non-existing",
|
||||
token: token.AccessToken,
|
||||
status: http.StatusBadRequest,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key with an invalid token",
|
||||
id: k.AccessToken,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key with an empty token",
|
||||
token: "",
|
||||
id: k.AccessToken,
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
repocall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(tc.key, tc.err)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
repocall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc, krepo := newService()
|
||||
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), Subject: id}
|
||||
|
||||
repocall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil)
|
||||
k, err := svc.Issue(context.Background(), token.AccessToken, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
repocall.Unset()
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "revoke an existing key",
|
||||
id: k.AccessToken,
|
||||
token: token.AccessToken,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a non-existing key",
|
||||
id: "non-existing",
|
||||
token: token.AccessToken,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke key with invalid token",
|
||||
id: k.AccessToken,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "revoke key with empty token",
|
||||
id: k.AccessToken,
|
||||
token: "",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
repocall := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
repocall.Unset()
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
)
|
||||
|
||||
type issueKeyReq struct {
|
||||
token string
|
||||
Type auth.KeyType `json:"type,omitempty"`
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
// It is not possible to issue Reset key using HTTP API.
|
||||
func (req issueKeyReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Type != auth.AccessKey &&
|
||||
req.Type != auth.RecoveryKey &&
|
||||
req.Type != auth.APIKey {
|
||||
return apiutil.ErrInvalidAPIKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type keyReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req keyReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var valid = "valid"
|
||||
|
||||
func TestIssueKeyReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req issueKeyReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: issueKeyReq{
|
||||
token: valid,
|
||||
Type: auth.AccessKey,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: issueKeyReq{
|
||||
token: "",
|
||||
Type: auth.AccessKey,
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "invalid key type",
|
||||
req: issueKeyReq{
|
||||
token: valid,
|
||||
Type: auth.KeyType(100),
|
||||
},
|
||||
err: apiutil.ErrInvalidAPIKey,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req keyReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: keyReq{
|
||||
token: valid,
|
||||
id: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: keyReq{
|
||||
token: "",
|
||||
id: valid,
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: keyReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err)
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
_ supermq.Response = (*issueKeyRes)(nil)
|
||||
_ supermq.Response = (*revokeKeyRes)(nil)
|
||||
)
|
||||
|
||||
type issueKeyRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (res issueKeyRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res issueKeyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res issueKeyRes) Empty() bool {
|
||||
return res.Value == ""
|
||||
}
|
||||
|
||||
type retrieveKeyRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
IssuerID string `json:"issuer_id,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Type auth.KeyType `json:"type,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (res retrieveKeyRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res retrieveKeyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res retrieveKeyRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type revokeKeyRes struct{}
|
||||
|
||||
func (res revokeKeyRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res revokeKeyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res revokeKeyRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/internal/api"
|
||||
"github.com/absmach/supermq/pkg/apiutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
mux.Route("/keys", func(r chi.Router) {
|
||||
r.Post("/", kithttp.NewServer(
|
||||
issueEndpoint(svc),
|
||||
decodeIssue,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
|
||||
r.Get("/{id}", kithttp.NewServer(
|
||||
(retrieveEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
|
||||
r.Delete("/{id}", kithttp.NewServer(
|
||||
(revokeEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := issueKeyReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package http
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/auth/api/http/keys"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, logger *slog.Logger, instanceID string) http.Handler {
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux = keys.MakeHandler(svc, mux, logger)
|
||||
|
||||
mux.Get("/health", supermq.Health("auth", instanceID))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc auth.Service, logger *slog.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Issue(ctx context.Context, token string, key auth.Key) (tkn auth.Token, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("key",
|
||||
slog.String("subject", key.Subject),
|
||||
slog.Any("type", key.Type),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Issue key failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Issue key completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Issue(ctx, token, key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Revoke(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("key_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Revoke key failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Revoke key completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Revoke(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RetrieveKey(ctx context.Context, token, id string) (key auth.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("key_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Retrieve key failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Retrieve key completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identify(ctx context.Context, token string) (id auth.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("key",
|
||||
slog.String("subject", id.Subject),
|
||||
slog.Any("type", id.Type),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Identify key failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Identify key completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("object",
|
||||
slog.String("id", pr.Object),
|
||||
slog.String("type", pr.ObjectType),
|
||||
),
|
||||
slog.Group("subject",
|
||||
slog.String("id", pr.Subject),
|
||||
slog.String("kind", pr.SubjectKind),
|
||||
slog.String("type", pr.SubjectType),
|
||||
),
|
||||
slog.String("permission", pr.Permission),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Authorize failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Authorize completed successfully", args...)
|
||||
}(time.Now())
|
||||
return lm.svc.Authorize(ctx, pr)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.Key) (auth.Token, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_key").Add(1)
|
||||
ms.latency.With("method", "issue_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Issue(ctx, token, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Revoke(ctx context.Context, token, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_key").Add(1)
|
||||
ms.latency.With("method", "revoke_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Revoke(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RetrieveKey(ctx context.Context, token, id string) (auth.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve_key").Add(1)
|
||||
ms.latency.With("method", "retrieve_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identify").Add(1)
|
||||
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "authorize").Add(1)
|
||||
ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.Authorize(ctx, pr)
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
authjwt "github.com/absmach/supermq/auth/jwt"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenType = "type"
|
||||
userField = "user"
|
||||
domainField = "domain"
|
||||
issuerName = "supermq.auth"
|
||||
secret = "test"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidIssuer = errors.New("invalid token issuer value")
|
||||
reposecret = []byte("test")
|
||||
)
|
||||
|
||||
func newToken(issuerName string, key auth.Key) string {
|
||||
builder := jwt.NewBuilder()
|
||||
builder.
|
||||
Issuer(issuerName).
|
||||
IssuedAt(key.IssuedAt).
|
||||
Claim(tokenType, "r").
|
||||
Expiration(key.ExpiresAt)
|
||||
builder.Claim(userField, key.User)
|
||||
if key.Domain != "" {
|
||||
builder.Claim(domainField, key.Domain)
|
||||
}
|
||||
if key.Subject != "" {
|
||||
builder.Subject(key.Subject)
|
||||
}
|
||||
if key.ID != "" {
|
||||
builder.JwtID(key.ID)
|
||||
}
|
||||
tkn, _ := builder.Build()
|
||||
tokn, _ := jwt.Sign(tkn, jwt.WithKey(jwa.HS512, reposecret))
|
||||
return string(tokn)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
tokenizer := authjwt.New([]byte(secret))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue new token",
|
||||
key: key(),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token with OAuth token",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: testsutil.GenerateUUID(t),
|
||||
User: testsutil.GenerateUUID(t),
|
||||
Domain: testsutil.GenerateUUID(t),
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without a domain",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: testsutil.GenerateUUID(t),
|
||||
User: testsutil.GenerateUUID(t),
|
||||
Domain: "",
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without a subject",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: "",
|
||||
User: testsutil.GenerateUUID(t),
|
||||
Domain: testsutil.GenerateUUID(t),
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without a domain and subject",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: "",
|
||||
User: testsutil.GenerateUUID(t),
|
||||
Domain: "",
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
tkn, err := tokenizer.Issue(tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
if err != nil {
|
||||
assert.NotEmpty(t, tkn, fmt.Sprintf("%s expected token, got empty string", tc.desc))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tokenizer := authjwt.New([]byte(secret))
|
||||
|
||||
token, err := tokenizer.Issue(key())
|
||||
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
|
||||
|
||||
apiKey := key()
|
||||
apiKey.Type = auth.APIKey
|
||||
apiKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
apiToken, err := tokenizer.Issue(apiKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
expKey := key()
|
||||
expKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
expToken, err := tokenizer.Issue(expKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing expired key expected to succeed: %s", err))
|
||||
|
||||
emptyDomainKey := key()
|
||||
emptyDomainKey.Domain = ""
|
||||
emptyDomainToken, err := tokenizer.Issue(emptyDomainKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
emptySubjectKey := key()
|
||||
emptySubjectKey.Subject = ""
|
||||
emptySubjectToken, err := tokenizer.Issue(emptySubjectKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
emptyKey := key()
|
||||
emptyKey.Domain = ""
|
||||
emptyKey.Subject = ""
|
||||
emptyToken, err := tokenizer.Issue(emptyKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
inValidToken := newToken("invalid", key())
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "parse valid key",
|
||||
key: key(),
|
||||
token: token,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse invalid key",
|
||||
key: auth.Key{},
|
||||
token: "invalid",
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "parse expired key",
|
||||
key: auth.Key{},
|
||||
token: expToken,
|
||||
err: auth.ErrExpiry,
|
||||
},
|
||||
{
|
||||
desc: "parse expired API key",
|
||||
key: apiKey,
|
||||
token: apiToken,
|
||||
err: auth.ErrExpiry,
|
||||
},
|
||||
{
|
||||
desc: "parse token with invalid issuer",
|
||||
key: auth.Key{},
|
||||
token: inValidToken,
|
||||
err: errInvalidIssuer,
|
||||
},
|
||||
{
|
||||
desc: "parse token with invalid content",
|
||||
key: auth.Key{},
|
||||
token: newToken(issuerName, key()),
|
||||
err: authjwt.ErrJSONHandle,
|
||||
},
|
||||
{
|
||||
desc: "parse token with empty domain",
|
||||
key: emptyDomainKey,
|
||||
token: emptyDomainToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse token with empty subject",
|
||||
key: emptySubjectKey,
|
||||
token: emptySubjectToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse token with empty domain and subject",
|
||||
key: emptyKey,
|
||||
token: emptyToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
key, err := tokenizer.Parse(tc.token)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func key() auth.Key {
|
||||
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
|
||||
return auth.Key{
|
||||
ID: "66af4a67-3823-438a-abd7-efdb613eaef6",
|
||||
Type: auth.AccessKey,
|
||||
Issuer: "supermq.auth",
|
||||
Subject: "66af4a67-3823-438a-abd7-efdb613eaef6",
|
||||
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: exp,
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidIssuer = errors.New("invalid token issuer value")
|
||||
// errJWTExpiryKey is used to check if the token is expired.
|
||||
errJWTExpiryKey = errors.New(`"exp" not satisfied`)
|
||||
// ErrSignJWT indicates an error in signing jwt token.
|
||||
ErrSignJWT = errors.New("failed to sign jwt token")
|
||||
// ErrValidateJWTToken indicates a failure to validate JWT token.
|
||||
ErrValidateJWTToken = errors.New("failed to validate jwt token")
|
||||
// ErrJSONHandle indicates an error in handling JSON.
|
||||
ErrJSONHandle = errors.New("failed to perform operation JSON")
|
||||
)
|
||||
|
||||
const (
|
||||
issuerName = "supermq.auth"
|
||||
tokenType = "type"
|
||||
userField = "user"
|
||||
oauthProviderField = "oauth_provider"
|
||||
oauthAccessTokenField = "access_token"
|
||||
oauthRefreshTokenField = "refresh_token"
|
||||
)
|
||||
|
||||
type tokenizer struct {
|
||||
secret []byte
|
||||
}
|
||||
|
||||
var _ auth.Tokenizer = (*tokenizer)(nil)
|
||||
|
||||
// NewRepository instantiates an implementation of Token repository.
|
||||
func New(secret []byte) auth.Tokenizer {
|
||||
return &tokenizer{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
func (tok *tokenizer) Issue(key auth.Key) (string, error) {
|
||||
builder := jwt.NewBuilder()
|
||||
builder.
|
||||
Issuer(issuerName).
|
||||
IssuedAt(key.IssuedAt).
|
||||
Claim(tokenType, key.Type).
|
||||
Expiration(key.ExpiresAt)
|
||||
builder.Claim(userField, key.User)
|
||||
if key.Subject != "" {
|
||||
builder.Subject(key.Subject)
|
||||
}
|
||||
if key.ID != "" {
|
||||
builder.JwtID(key.ID)
|
||||
}
|
||||
tkn, err := builder.Build()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
signedTkn, err := jwt.Sign(tkn, jwt.WithKey(jwa.HS512, tok.secret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrSignJWT, err)
|
||||
}
|
||||
return string(signedTkn), nil
|
||||
}
|
||||
|
||||
func (tok *tokenizer) Parse(token string) (auth.Key, error) {
|
||||
tkn, err := tok.validateToken(token)
|
||||
if err != nil {
|
||||
return auth.Key{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
key, err := toKey(tkn)
|
||||
if err != nil {
|
||||
return auth.Key{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (tok *tokenizer) validateToken(token string) (jwt.Token, error) {
|
||||
tkn, err := jwt.Parse(
|
||||
[]byte(token),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithKey(jwa.HS512, tok.secret),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Contains(err, errJWTExpiryKey) {
|
||||
return nil, auth.ErrExpiry
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) jwt.ValidationError {
|
||||
if t.Issuer() != issuerName {
|
||||
return jwt.NewValidationError(errInvalidIssuer)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err := jwt.Validate(tkn, jwt.WithValidator(validator)); err != nil {
|
||||
return nil, errors.Wrap(ErrValidateJWTToken, err)
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
func toKey(tkn jwt.Token) (auth.Key, error) {
|
||||
data, err := json.Marshal(tkn.PrivateClaims())
|
||||
if err != nil {
|
||||
return auth.Key{}, errors.Wrap(ErrJSONHandle, err)
|
||||
}
|
||||
var key auth.Key
|
||||
if err := json.Unmarshal(data, &key); err != nil {
|
||||
return auth.Key{}, errors.Wrap(ErrJSONHandle, err)
|
||||
}
|
||||
|
||||
tType, ok := tkn.Get(tokenType)
|
||||
if !ok {
|
||||
return auth.Key{}, err
|
||||
}
|
||||
ktype, err := strconv.ParseInt(fmt.Sprintf("%v", tType), 10, 64)
|
||||
if err != nil {
|
||||
return auth.Key{}, err
|
||||
}
|
||||
|
||||
key.ID = tkn.JwtID()
|
||||
key.Type = auth.KeyType(ktype)
|
||||
key.Issuer = tkn.Issuer()
|
||||
key.Subject = tkn.Subject()
|
||||
key.IssuedAt = tkn.IssuedAt()
|
||||
key.ExpiresAt = tkn.Expiration()
|
||||
|
||||
return key, nil
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrKeyExpired indicates that the Key is expired.
|
||||
var ErrKeyExpired = errors.New("use of expired key")
|
||||
|
||||
type Token struct {
|
||||
AccessToken string // AccessToken contains the security credentials for a login session and identifies the client.
|
||||
RefreshToken string // RefreshToken is a credential artifact that OAuth can use to get a new access token without client interaction.
|
||||
AccessType string // AccessType is the specific type of access token issued. It can be Bearer, Client or Basic.
|
||||
}
|
||||
|
||||
type KeyType uint32
|
||||
|
||||
const (
|
||||
// AccessKey is temporary User key received on successful login.
|
||||
AccessKey KeyType = iota
|
||||
// RefreshKey is a temporary User key used to generate a new access key.
|
||||
RefreshKey
|
||||
// RecoveryKey represents a key for resseting password.
|
||||
RecoveryKey
|
||||
// APIKey enables the one to act on behalf of the user.
|
||||
APIKey
|
||||
// InvitationKey is a key for inviting new users.
|
||||
InvitationKey
|
||||
)
|
||||
|
||||
func (kt KeyType) String() string {
|
||||
switch kt {
|
||||
case AccessKey:
|
||||
return "access"
|
||||
case RefreshKey:
|
||||
return "refresh"
|
||||
case RecoveryKey:
|
||||
return "recovery"
|
||||
case APIKey:
|
||||
return "API"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Key represents API key.
|
||||
type Key struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type KeyType `json:"type,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
Subject string `json:"subject,omitempty"` // user ID
|
||||
User string `json:"user,omitempty"`
|
||||
Domain string `json:"domain,omitempty"` // domain user ID
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (key Key) String() string {
|
||||
return fmt.Sprintf(`{
|
||||
id: %s,
|
||||
type: %s,
|
||||
issuer_id: %s,
|
||||
subject: %s,
|
||||
user: %s,
|
||||
domain: %s,
|
||||
iat: %v,
|
||||
eat: %v
|
||||
}`, key.ID, key.Type, key.Issuer, key.Subject, key.User, key.Domain, key.IssuedAt, key.ExpiresAt)
|
||||
}
|
||||
|
||||
// Expired verifies if the key is expired.
|
||||
func (key Key) Expired() bool {
|
||||
if key.Type == APIKey && key.ExpiresAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
return key.ExpiresAt.UTC().Before(time.Now().UTC())
|
||||
}
|
||||
|
||||
// KeyRepository specifies Key persistence API.
|
||||
//
|
||||
//go:generate mockery --name KeyRepository --output=./mocks --filename keys.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type KeyRepository interface {
|
||||
// Save persists the Key. A non-nil error is returned to indicate
|
||||
// operation failure
|
||||
Save(ctx context.Context, key Key) (id string, err error)
|
||||
|
||||
// Retrieve retrieves Key by its unique identifier.
|
||||
Retrieve(ctx context.Context, issuer string, id string) (key Key, err error)
|
||||
|
||||
// Remove removes Key with provided ID.
|
||||
Remove(ctx context.Context, issuer string, id string) error
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
policies "github.com/absmach/supermq/pkg/policies"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Authz is an autogenerated mock type for the Authz type
|
||||
type Authz struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Authorize provides a mock function with given fields: ctx, pr
|
||||
func (_m *Authz) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
ret := _m.Called(ctx, pr)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Authorize")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok {
|
||||
r0 = rf(ctx, pr)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewAuthz creates a new instance of Authz. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewAuthz(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Authz {
|
||||
mock := &Authz{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
auth "github.com/absmach/supermq/auth"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// KeyRepository is an autogenerated mock type for the KeyRepository type
|
||||
type KeyRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Remove provides a mock function with given fields: ctx, issuer, id
|
||||
func (_m *KeyRepository) Remove(ctx context.Context, issuer string, id string) error {
|
||||
ret := _m.Called(ctx, issuer, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, issuer, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Retrieve provides a mock function with given fields: ctx, issuer, id
|
||||
func (_m *KeyRepository) Retrieve(ctx context.Context, issuer string, id string) (auth.Key, error) {
|
||||
ret := _m.Called(ctx, issuer, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Retrieve")
|
||||
}
|
||||
|
||||
var r0 auth.Key
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) (auth.Key, error)); ok {
|
||||
return rf(ctx, issuer, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) auth.Key); ok {
|
||||
r0 = rf(ctx, issuer, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(auth.Key)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, issuer, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: ctx, key
|
||||
func (_m *KeyRepository) Save(ctx context.Context, key auth.Key) (string, error) {
|
||||
ret := _m.Called(ctx, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, auth.Key) (string, error)); ok {
|
||||
return rf(ctx, key)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, auth.Key) string); ok {
|
||||
r0 = rf(ctx, key)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, auth.Key) error); ok {
|
||||
r1 = rf(ctx, key)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewKeyRepository creates a new instance of KeyRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewKeyRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *KeyRepository {
|
||||
mock := &KeyRepository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
auth "github.com/absmach/supermq/auth"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
policies "github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
// Service is an autogenerated mock type for the Service type
|
||||
type Service struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Authorize provides a mock function with given fields: ctx, pr
|
||||
func (_m *Service) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
ret := _m.Called(ctx, pr)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Authorize")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok {
|
||||
r0 = rf(ctx, pr)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Identify provides a mock function with given fields: ctx, token
|
||||
func (_m *Service) Identify(ctx context.Context, token string) (auth.Key, error) {
|
||||
ret := _m.Called(ctx, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Identify")
|
||||
}
|
||||
|
||||
var r0 auth.Key
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (auth.Key, error)); ok {
|
||||
return rf(ctx, token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) auth.Key); ok {
|
||||
r0 = rf(ctx, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(auth.Key)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, token)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Issue provides a mock function with given fields: ctx, token, key
|
||||
func (_m *Service) Issue(ctx context.Context, token string, key auth.Key) (auth.Token, error) {
|
||||
ret := _m.Called(ctx, token, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Issue")
|
||||
}
|
||||
|
||||
var r0 auth.Token
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, auth.Key) (auth.Token, error)); ok {
|
||||
return rf(ctx, token, key)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, auth.Key) auth.Token); ok {
|
||||
r0 = rf(ctx, token, key)
|
||||
} else {
|
||||
r0 = ret.Get(0).(auth.Token)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, auth.Key) error); ok {
|
||||
r1 = rf(ctx, token, key)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RetrieveKey provides a mock function with given fields: ctx, token, id
|
||||
func (_m *Service) RetrieveKey(ctx context.Context, token string, id string) (auth.Key, error) {
|
||||
ret := _m.Called(ctx, token, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveKey")
|
||||
}
|
||||
|
||||
var r0 auth.Key
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) (auth.Key, error)); ok {
|
||||
return rf(ctx, token, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) auth.Key); ok {
|
||||
r0 = rf(ctx, token, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(auth.Key)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, token, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Revoke provides a mock function with given fields: ctx, token, id
|
||||
func (_m *Service) Revoke(ctx context.Context, token string, id string) error {
|
||||
ret := _m.Called(ctx, token, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Revoke")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, token, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Service {
|
||||
mock := &Service{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
v1 "github.com/absmach/supermq/internal/grpc/token/v1"
|
||||
)
|
||||
|
||||
// TokenServiceClient is an autogenerated mock type for the TokenServiceClient type
|
||||
type TokenServiceClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type TokenServiceClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *TokenServiceClient) EXPECT() *TokenServiceClient_Expecter {
|
||||
return &TokenServiceClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Issue provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *TokenServiceClient) Issue(ctx context.Context, in *v1.IssueReq, opts ...grpc.CallOption) (*v1.Token, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Issue")
|
||||
}
|
||||
|
||||
var r0 *v1.Token
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.IssueReq, ...grpc.CallOption) (*v1.Token, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.IssueReq, ...grpc.CallOption) *v1.Token); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.Token)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v1.IssueReq, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// TokenServiceClient_Issue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Issue'
|
||||
type TokenServiceClient_Issue_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Issue is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *v1.IssueReq
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *TokenServiceClient_Expecter) Issue(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_Issue_Call {
|
||||
return &TokenServiceClient_Issue_Call{Call: _e.mock.On("Issue",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Issue_Call) Run(run func(ctx context.Context, in *v1.IssueReq, opts ...grpc.CallOption)) *TokenServiceClient_Issue_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*v1.IssueReq), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Issue_Call) Return(_a0 *v1.Token, _a1 error) *TokenServiceClient_Issue_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Issue_Call) RunAndReturn(run func(context.Context, *v1.IssueReq, ...grpc.CallOption) (*v1.Token, error)) *TokenServiceClient_Issue_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Refresh provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *TokenServiceClient) Refresh(ctx context.Context, in *v1.RefreshReq, opts ...grpc.CallOption) (*v1.Token, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Refresh")
|
||||
}
|
||||
|
||||
var r0 *v1.Token
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.RefreshReq, ...grpc.CallOption) (*v1.Token, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.RefreshReq, ...grpc.CallOption) *v1.Token); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.Token)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v1.RefreshReq, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// TokenServiceClient_Refresh_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Refresh'
|
||||
type TokenServiceClient_Refresh_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Refresh is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *v1.RefreshReq
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *TokenServiceClient_Expecter) Refresh(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_Refresh_Call {
|
||||
return &TokenServiceClient_Refresh_Call{Call: _e.mock.On("Refresh",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Refresh_Call) Run(run func(ctx context.Context, in *v1.RefreshReq, opts ...grpc.CallOption)) *TokenServiceClient_Refresh_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*v1.RefreshReq), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Refresh_Call) Return(_a0 *v1.Token, _a1 error) *TokenServiceClient_Refresh_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Refresh_Call) RunAndReturn(run func(context.Context, *v1.RefreshReq, ...grpc.CallOption) (*v1.Token, error)) *TokenServiceClient_Refresh_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewTokenServiceClient creates a new instance of TokenServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewTokenServiceClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *TokenServiceClient {
|
||||
mock := &TokenServiceClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains Key repository implementations using
|
||||
// PostgreSQL as the underlying database.
|
||||
package postgres
|
||||
@@ -1,70 +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 Auth service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "auth_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS keys (
|
||||
id VARCHAR(254) NOT NULL,
|
||||
type SMALLINT,
|
||||
subject VARCHAR(254) NOT NULL,
|
||||
issuer_id VARCHAR(254) NOT NULL,
|
||||
issued_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP,
|
||||
PRIMARY KEY (id, issuer_id)
|
||||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS domains (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(254),
|
||||
tags TEXT[],
|
||||
metadata JSONB,
|
||||
alias VARCHAR(254) NULL UNIQUE,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
updated_by VARCHAR(254),
|
||||
created_by VARCHAR(254),
|
||||
status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0)
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS policies (
|
||||
subject_type VARCHAR(254) NOT NULL,
|
||||
subject_id VARCHAR(254) NOT NULL,
|
||||
subject_relation VARCHAR(254) NOT NULL,
|
||||
relation VARCHAR(254) NOT NULL,
|
||||
object_type VARCHAR(254) NOT NULL,
|
||||
object_id VARCHAR(254) NOT NULL,
|
||||
CONSTRAINT unique_policy_constraint UNIQUE (subject_type, subject_id, subject_relation, relation, object_type, object_id)
|
||||
);`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS keys`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "auth_2",
|
||||
Up: []string{
|
||||
`ALTER TABLE domains ALTER COLUMN alias SET NOT NULL`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "auth_3",
|
||||
Up: []string{
|
||||
`DROP TABLE IF EXISTS policies;
|
||||
DROP TABLE IF EXISTS domains;
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
)
|
||||
|
||||
var (
|
||||
errSave = errors.New("failed to save key in database")
|
||||
errRetrieve = errors.New("failed to retrieve key from database")
|
||||
errDelete = errors.New("failed to delete key from database")
|
||||
)
|
||||
var _ auth.KeyRepository = (*repo)(nil)
|
||||
|
||||
type repo struct {
|
||||
db postgres.Database
|
||||
}
|
||||
|
||||
// New instantiates a PostgreSQL implementation of key repository.
|
||||
func New(db postgres.Database) auth.KeyRepository {
|
||||
return &repo{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (kr *repo) Save(ctx context.Context, key auth.Key) (string, error) {
|
||||
q := `INSERT INTO keys (id, type, issuer_id, subject, issued_at, expires_at)
|
||||
VALUES (:id, :type, :issuer_id, :subject, :issued_at, :expires_at)`
|
||||
|
||||
dbKey := toDBKey(key)
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, dbKey); err != nil {
|
||||
return "", postgres.HandleError(errSave, err)
|
||||
}
|
||||
|
||||
return dbKey.ID, nil
|
||||
}
|
||||
|
||||
func (kr *repo) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, error) {
|
||||
q := `SELECT id, type, issuer_id, subject, issued_at, expires_at FROM keys WHERE issuer_id = $1 AND id = $2`
|
||||
key := dbKey{}
|
||||
if err := kr.db.QueryRowxContext(ctx, q, issuerID, id).StructScan(&key); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return auth.Key{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return auth.Key{}, postgres.HandleError(errRetrieve, err)
|
||||
}
|
||||
|
||||
return toKey(key), nil
|
||||
}
|
||||
|
||||
func (kr *repo) Remove(ctx context.Context, issuerID, id string) error {
|
||||
q := `DELETE FROM keys WHERE issuer_id = :issuer_id AND id = :id`
|
||||
key := dbKey{
|
||||
ID: id,
|
||||
Issuer: issuerID,
|
||||
}
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, key); err != nil {
|
||||
return errors.Wrap(errDelete, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbKey struct {
|
||||
ID string `db:"id"`
|
||||
Type uint32 `db:"type"`
|
||||
Issuer string `db:"issuer_id"`
|
||||
Subject string `db:"subject"`
|
||||
IssuedAt time.Time `db:"issued_at"`
|
||||
ExpiresAt sql.NullTime `db:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func toDBKey(key auth.Key) dbKey {
|
||||
ret := dbKey{
|
||||
ID: key.ID,
|
||||
Type: uint32(key.Type),
|
||||
Issuer: key.Issuer,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
ret.ExpiresAt = sql.NullTime{Time: key.ExpiresAt, Valid: true}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func toKey(key dbKey) auth.Key {
|
||||
ret := auth.Key{
|
||||
ID: key.ID,
|
||||
Type: auth.KeyType(key.Type),
|
||||
Issuer: key.Issuer,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if key.ExpiresAt.Valid {
|
||||
ret.ExpiresAt = key.ExpiresAt.Time
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/auth/postgres"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
expTime = time.Now().Add(5 * time.Minute)
|
||||
idProvider = uuid.New()
|
||||
invalidID = strings.Repeat("a", 255)
|
||||
)
|
||||
|
||||
func generateID(t *testing.T) string {
|
||||
id, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
return id
|
||||
}
|
||||
|
||||
func TestKeySave(t *testing.T) {
|
||||
repo := postgres.New(database)
|
||||
|
||||
keyID := generateID(t)
|
||||
issuer := generateID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a new key",
|
||||
key: auth.Key{
|
||||
ID: keyID,
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with duplicate id",
|
||||
key: auth.Key{
|
||||
ID: keyID,
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save with empty id",
|
||||
key: auth.Key{
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with empty subject",
|
||||
key: auth.Key{
|
||||
ID: generateID(t),
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with empty issuer",
|
||||
key: auth.Key{
|
||||
ID: generateID(t),
|
||||
Type: auth.APIKey,
|
||||
Issuer: "",
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with empty issued at",
|
||||
key: auth.Key{
|
||||
ID: generateID(t),
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Time{},
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with invalid id",
|
||||
key: auth.Key{
|
||||
ID: invalidID,
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "save with invalid subject",
|
||||
key: auth.Key{
|
||||
ID: generateID(t),
|
||||
Type: auth.APIKey,
|
||||
Issuer: issuer,
|
||||
Subject: invalidID,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "save with invalid issuer",
|
||||
key: auth.Key{
|
||||
ID: generateID(t),
|
||||
Type: auth.APIKey,
|
||||
Issuer: invalidID,
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(context.Background(), tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyRetrieve(t *testing.T) {
|
||||
repo := postgres.New(database)
|
||||
|
||||
key := auth.Key{
|
||||
ID: generateID(t),
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
Issuer: generateID(t),
|
||||
ExpiresAt: expTime,
|
||||
}
|
||||
_, err := repo.Save(context.Background(), key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: key.ID,
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve key with empty issuer id",
|
||||
id: key.ID,
|
||||
issuer: "",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existent key",
|
||||
id: "",
|
||||
issuer: key.Issuer,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existent key with empty issuer id",
|
||||
id: "",
|
||||
issuer: "",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Retrieve(context.Background(), tc.issuer, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyRemove(t *testing.T) {
|
||||
repo := postgres.New(database)
|
||||
|
||||
key := auth.Key{
|
||||
ID: generateID(t),
|
||||
Subject: generateID(t),
|
||||
IssuedAt: time.Now(),
|
||||
Issuer: generateID(t),
|
||||
ExpiresAt: expTime,
|
||||
}
|
||||
_, err := repo.Save(context.Background(), key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing key",
|
||||
id: key.ID,
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key that has already been removed",
|
||||
id: key.ID,
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key that does not exist",
|
||||
id: generateID(t),
|
||||
issuer: generateID(t),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key with empty issuer id",
|
||||
id: key.ID,
|
||||
issuer: "",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key with empty id",
|
||||
id: "",
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key with empty id and issuer id",
|
||||
id: "",
|
||||
issuer: "",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Remove(context.Background(), tc.issuer, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres_test contains tests for PostgreSQL repository
|
||||
// implementations.
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apostgres "github.com/absmach/supermq/auth/postgres"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
dockertest "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.2-alpine",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
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, *apostgres.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)
|
||||
}
|
||||
-436
@@ -1,436 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
const (
|
||||
recoveryDuration = 5 * time.Minute
|
||||
defLimit = 100
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrExpiry indicates that the token is expired.
|
||||
ErrExpiry = errors.New("token is expired")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new login key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
errPlatform = errors.New("invalid platform id")
|
||||
)
|
||||
|
||||
// Authz represents a authorization service. It exposes
|
||||
// functionalities through `auth` to perform authorization.
|
||||
//
|
||||
//go:generate mockery --name Authz --output=./mocks --filename authz.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type Authz interface {
|
||||
// Authorize checks authorization of the given `subject`. Basically,
|
||||
// Authorize verifies that Is `subject` allowed to `relation` on
|
||||
// `object`. Authorize returns a non-nil error if the subject has
|
||||
// no relation on the object (which simply means the operation is
|
||||
// denied).
|
||||
Authorize(ctx context.Context, pr policies.Policy) error
|
||||
}
|
||||
|
||||
// Authn specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
type Authn interface {
|
||||
// Issue issues a new Key, returning its token value alongside.
|
||||
Issue(ctx context.Context, token string, key Key) (Token, error)
|
||||
|
||||
// Revoke removes the Key with the provided id that is
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(ctx context.Context, token, id string) error
|
||||
|
||||
// RetrieveKey retrieves data for the Key identified by the provided
|
||||
// ID, that is issued by the user identified by the provided key.
|
||||
RetrieveKey(ctx context.Context, token, id string) (Key, error)
|
||||
|
||||
// Identify validates token token. If token is valid, content
|
||||
// is returned. If token is invalid, or invocation failed for some
|
||||
// other reason, non-nil error value is returned in response.
|
||||
Identify(ctx context.Context, token string) (Key, error)
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
|
||||
//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type Service interface {
|
||||
Authn
|
||||
Authz
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
idProvider supermq.IDProvider
|
||||
evaluator policies.Evaluator
|
||||
policysvc policies.Service
|
||||
tokenizer Tokenizer
|
||||
loginDuration time.Duration
|
||||
refreshDuration time.Duration
|
||||
invitationDuration time.Duration
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, idp supermq.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
idProvider: idp,
|
||||
evaluator: policyEvaluator,
|
||||
policysvc: policyService,
|
||||
loginDuration: loginDuration,
|
||||
refreshDuration: refreshDuration,
|
||||
invitationDuration: invitationDuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Issue(ctx context.Context, token string, key Key) (Token, error) {
|
||||
key.IssuedAt = time.Now().UTC()
|
||||
switch key.Type {
|
||||
case APIKey:
|
||||
return svc.userKey(ctx, token, key)
|
||||
case RefreshKey:
|
||||
return svc.refreshKey(ctx, token, key)
|
||||
case RecoveryKey:
|
||||
return svc.tmpKey(recoveryDuration, key)
|
||||
case InvitationKey:
|
||||
return svc.invitationKey(ctx, key)
|
||||
default:
|
||||
return svc.accessKey(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, token, id string) error {
|
||||
issuerID, _, err := svc.authenticate(token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
if err := svc.keys.Remove(ctx, issuerID, id); err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
|
||||
issuerID, _, err := svc.authenticate(token)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
key, err := svc.keys.Retrieve(ctx, issuerID, id)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (svc service) Identify(ctx context.Context, token string) (Key, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
if errors.Contains(err, ErrExpiry) {
|
||||
err = svc.keys.Remove(ctx, key.Issuer, key.ID)
|
||||
return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(ErrKeyExpired, err))
|
||||
}
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(errIdentify, err))
|
||||
}
|
||||
|
||||
switch key.Type {
|
||||
case RecoveryKey, AccessKey, InvitationKey, RefreshKey:
|
||||
return key, nil
|
||||
case APIKey:
|
||||
_, err := svc.keys.Retrieve(ctx, key.Issuer, key.ID)
|
||||
if err != nil {
|
||||
return Key{}, svcerr.ErrAuthentication
|
||||
}
|
||||
return key, nil
|
||||
default:
|
||||
return Key{}, svcerr.ErrAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
if err := svc.PolicyValidation(pr); err != nil {
|
||||
return errors.Wrap(svcerr.ErrMalformedEntity, err)
|
||||
}
|
||||
if pr.SubjectKind == policies.TokenKind {
|
||||
key, err := svc.Identify(ctx, pr.Subject)
|
||||
if err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
if key.Subject == "" {
|
||||
if pr.ObjectType == policies.GroupType || pr.ObjectType == policies.ClientType || pr.ObjectType == policies.DomainType {
|
||||
return svcerr.ErrDomainAuthorization
|
||||
}
|
||||
return svcerr.ErrAuthentication
|
||||
}
|
||||
pr.Subject = key.Subject
|
||||
pr.Domain = key.Domain
|
||||
}
|
||||
if err := svc.checkPolicy(ctx, pr); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) checkPolicy(ctx context.Context, pr policies.Policy) error {
|
||||
// Domain status is required for if user sent authorization request on clients, channels, groups and domains
|
||||
if pr.SubjectType == policies.UserType && (pr.ObjectType == policies.GroupType || pr.ObjectType == policies.ClientType || pr.ObjectType == policies.DomainType) {
|
||||
domainID := pr.Domain
|
||||
if domainID == "" {
|
||||
if pr.ObjectType != policies.DomainType {
|
||||
return svcerr.ErrDomainAuthorization
|
||||
}
|
||||
domainID = pr.Object
|
||||
}
|
||||
if err := svc.checkDomain(ctx, pr.SubjectType, pr.Subject, domainID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := svc.evaluator.CheckPolicy(ctx, pr); err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) checkDomain(ctx context.Context, subjectType, subject, domainID string) error {
|
||||
if err := svc.evaluator.CheckPolicy(ctx, policies.Policy{
|
||||
Subject: subject,
|
||||
SubjectType: subjectType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: domainID,
|
||||
ObjectType: policies.DomainType,
|
||||
}); err != nil {
|
||||
return svcerr.ErrDomainAuthorization
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) PolicyValidation(pr policies.Policy) error {
|
||||
if pr.ObjectType == policies.PlatformType && pr.Object != policies.SuperMQObject {
|
||||
return errPlatform
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) tmpKey(duration time.Duration, key Key) (Token, error) {
|
||||
key.ExpiresAt = time.Now().Add(duration)
|
||||
value, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
return Token{AccessToken: value}, nil
|
||||
}
|
||||
|
||||
func (svc service) accessKey(ctx context.Context, key Key) (Token, error) {
|
||||
var err error
|
||||
key.Type = AccessKey
|
||||
key.ExpiresAt = time.Now().Add(svc.loginDuration)
|
||||
|
||||
key.Subject, err = svc.checkUserDomain(ctx, key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
|
||||
access, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
key.ExpiresAt = time.Now().Add(svc.refreshDuration)
|
||||
key.Type = RefreshKey
|
||||
refresh, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
return Token{AccessToken: access, RefreshToken: refresh}, nil
|
||||
}
|
||||
|
||||
func (svc service) invitationKey(ctx context.Context, key Key) (Token, error) {
|
||||
var err error
|
||||
key.Type = InvitationKey
|
||||
key.ExpiresAt = time.Now().Add(svc.invitationDuration)
|
||||
|
||||
key.Subject, err = svc.checkUserDomain(ctx, key)
|
||||
if err != nil {
|
||||
return Token{}, err
|
||||
}
|
||||
|
||||
access, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
return Token{AccessToken: access}, nil
|
||||
}
|
||||
|
||||
func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token, error) {
|
||||
k, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
if k.Type != RefreshKey {
|
||||
return Token{}, errIssueUser
|
||||
}
|
||||
key.ID = k.ID
|
||||
if key.Domain == "" {
|
||||
key.Domain = k.Domain
|
||||
}
|
||||
key.User = k.User
|
||||
key.Type = AccessKey
|
||||
|
||||
key.Subject, err = svc.checkUserDomain(ctx, key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
|
||||
key.ExpiresAt = time.Now().Add(svc.loginDuration)
|
||||
access, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
key.ExpiresAt = time.Now().Add(svc.refreshDuration)
|
||||
key.Type = RefreshKey
|
||||
refresh, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
return Token{AccessToken: access, RefreshToken: refresh}, nil
|
||||
}
|
||||
|
||||
func (svc service) checkUserDomain(ctx context.Context, key Key) (subject string, err error) {
|
||||
if key.Domain != "" {
|
||||
// Check user is platform admin.
|
||||
if err = svc.Authorize(ctx, policies.Policy{
|
||||
Subject: key.User,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.AdminPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}); err == nil {
|
||||
return key.User, nil
|
||||
}
|
||||
// Check user is domain member.
|
||||
domainUserSubject := EncodeDomainUserID(key.Domain, key.User)
|
||||
if err = svc.Authorize(ctx, policies.Policy{
|
||||
Subject: domainUserSubject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: key.Domain,
|
||||
ObjectType: policies.DomainType,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return domainUserSubject, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (svc service) userKey(ctx context.Context, token string, key Key) (Token, error) {
|
||||
id, sub, err := svc.authenticate(token)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
key.Issuer = id
|
||||
if key.Subject == "" {
|
||||
key.Subject = sub
|
||||
}
|
||||
|
||||
keyID, err := svc.idProvider.ID()
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.ID = keyID
|
||||
|
||||
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||||
return Token{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
tkn, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
return Token{AccessToken: tkn}, nil
|
||||
}
|
||||
|
||||
func (svc service) authenticate(token string) (string, string, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
// Only login key token is valid for login.
|
||||
if key.Type != AccessKey || key.Issuer == "" {
|
||||
return "", "", svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
return key.Issuer, key.Subject, nil
|
||||
}
|
||||
|
||||
// Switch the relative permission for the relation.
|
||||
func SwitchToPermission(relation string) string {
|
||||
switch relation {
|
||||
case policies.AdministratorRelation:
|
||||
return policies.AdminPermission
|
||||
case policies.EditorRelation:
|
||||
return policies.EditPermission
|
||||
case policies.ContributorRelation:
|
||||
return policies.ViewPermission
|
||||
case policies.MemberRelation:
|
||||
return policies.MembershipPermission
|
||||
case policies.GuestRelation:
|
||||
return policies.ViewPermission
|
||||
default:
|
||||
return relation
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeDomainUserID(domainID, userID string) string {
|
||||
if domainID == "" || userID == "" {
|
||||
return ""
|
||||
}
|
||||
return domainID + "_" + userID
|
||||
}
|
||||
|
||||
func DecodeDomainUserID(domainUserID string) (string, string) {
|
||||
if domainUserID == "" {
|
||||
return domainUserID, domainUserID
|
||||
}
|
||||
duid := strings.Split(domainUserID, "_")
|
||||
|
||||
switch {
|
||||
case len(duid) == 2:
|
||||
return duid[0], duid[1]
|
||||
case len(duid) == 1:
|
||||
return duid[0], ""
|
||||
case len(duid) == 0 || len(duid) > 2:
|
||||
fallthrough
|
||||
default:
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
// Tokenizer specifies API for encoding and decoding between string and Key.
|
||||
type Tokenizer interface {
|
||||
// Issue converts API Key to its string representation.
|
||||
Issue(key Key) (token string, err error)
|
||||
|
||||
// Parse extracts API Key data from string token.
|
||||
Parse(token string) (key Key, err error)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing provides tracing instrumentation for SuperMQ Users service.
|
||||
//
|
||||
// This package provides tracing middleware for SuperMQ Users service.
|
||||
// It can be used to trace incoming requests and add tracing capabilities to
|
||||
// SuperMQ Users service.
|
||||
//
|
||||
// For more details about tracing instrumentation for SuperMQ messaging refer
|
||||
// to the documentation at https://docs.supermq.abstractmachines.fr/tracing/.
|
||||
package tracing
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*tracingMiddleware)(nil)
|
||||
|
||||
type tracingMiddleware struct {
|
||||
tracer trace.Tracer
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// New returns a new group service with tracing capabilities.
|
||||
func New(svc auth.Service, tracer trace.Tracer) auth.Service {
|
||||
return &tracingMiddleware{tracer, svc}
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) Issue(ctx context.Context, token string, key auth.Key) (auth.Token, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "issue", trace.WithAttributes(
|
||||
attribute.String("type", fmt.Sprintf("%d", key.Type)),
|
||||
attribute.String("subject", key.Subject),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Issue(ctx, token, key)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) Revoke(ctx context.Context, token, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "revoke", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Revoke(ctx, token, id)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) RetrieveKey(ctx context.Context, token, id string) (auth.Key, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "retrieve_key", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) Identify(ctx context.Context, token string) (auth.Key, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "identify")
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "authorize", trace.WithAttributes(
|
||||
attribute.String("subject", pr.Subject),
|
||||
attribute.String("subject_type", pr.SubjectType),
|
||||
attribute.String("subject_relation", pr.SubjectRelation),
|
||||
attribute.String("object", pr.Object),
|
||||
attribute.String("object_type", pr.ObjectType),
|
||||
attribute.String("relation", pr.Relation),
|
||||
attribute.String("permission", pr.Permission),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Authorize(ctx, pr)
|
||||
}
|
||||
+1666
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package mainflux;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service ThingsService {
|
||||
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
|
||||
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
|
||||
rpc Identify(Token) returns (ThingID) {}
|
||||
}
|
||||
|
||||
service AuthNService {
|
||||
rpc Issue(IssueReq) returns (Token) {}
|
||||
rpc Identify(Token) returns (UserID) {}
|
||||
}
|
||||
|
||||
message AccessByKeyReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ThingID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message AccessByIDReq {
|
||||
string thingID = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
// If a token is not carrying any information itself, the type
|
||||
// field can be used to determine how to validate the token.
|
||||
// Also, different tokens can be encoded in different ways.
|
||||
message Token {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message UserID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message IssueReq {
|
||||
string issuer = 1;
|
||||
uint32 type = 2;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# Authentication service
|
||||
|
||||
Authentication service provides an API for managing authentication keys.
|
||||
|
||||
There are *three types of authentication keys*:
|
||||
|
||||
- user key - keys issued to the user upon login request
|
||||
- API key - keys issued upon the user request
|
||||
- recovery key - password recovery key
|
||||
|
||||
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user. API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. Recovery key is the password recovery key. It's short-lived token used for password recovery process.
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
The following actions are supported:
|
||||
|
||||
- create (all key types)
|
||||
- verify (all key types)
|
||||
- obtain (API keys only; secret is never obtained)
|
||||
- revoke (API keys only)
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------|
|
||||
| MF_AUTHN_LOG_LEVEL | Service level (debug, info, warn, error) | error |
|
||||
| MF_AUTHN_DB_HOST | Database host address | localhost |
|
||||
| MF_AUTHN_DB_PORT | Database host port | 5432 |
|
||||
| MF_AUTHN_DB_USER | Database user | mainflux |
|
||||
| MF_AUTHN_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_AUTHN_DB | Name of the database used by the service | auth |
|
||||
| MF_AUTHN_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_AUTHN_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_AUTHN_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_AUTHN_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_AUTHN_HTTP_PORT | Authn service HTTP port | 8180 |
|
||||
| MF_AUTHN_GRPC_PORT | Authn service gRPC port | 8181 |
|
||||
| MF_AUTHN_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTHN_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTHN_SECRET | String used for signing tokens | auth |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
authn:
|
||||
image: mainflux/authn:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_AUTHN_LOG_LEVEL: [Service log level]
|
||||
MF_AUTHN_DB_HOST: [Database host address]
|
||||
MF_AUTHN_DB_PORT: [Database host port]
|
||||
MF_AUTHN_DB_USER: [Database user]
|
||||
MF_AUTHN_DB_PASS: [Database password]
|
||||
MF_AUTHN_DB: [Name of the database used by the service]
|
||||
MF_AUTHN_DB_SSL_MODE: [SSL mode to connect to the database with]
|
||||
MF_AUTHN_DB_SSL_CERT: [Path to the PEM encoded certificate file]
|
||||
MF_AUTHN_DB_SSL_KEY: [Path to the PEM encoded key file]
|
||||
MF_AUTHN_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
|
||||
MF_AUTHN_HTTP_PORT: [Service HTTP port]
|
||||
MF_AUTHN_GRPC_PORT: [Service gRPC port]
|
||||
MF_AUTHN_SECRET: [String used for signing tokens]
|
||||
MF_AUTHN_SERVER_CERT: [String path to server certificate in pem format]
|
||||
MF_AUTHN_SERVER_KEY: [String path to server key in pem format]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the service
|
||||
make authn
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_AUTHN_LOG_LEVEL=[Service log level] MF_AUTHN_DB_HOST=[Database host address] MF_AUTHN_DB_PORT=[Database host port] MF_AUTHN_DB_USER=[Database user] MF_AUTHN_DB_PASS=[Database password] MF_AUTHN_DB=[Name of the database used by the service] MF_AUTHN_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTHN_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTHN_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTHN_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTHN_HTTP_PORT=[Service HTTP port] MF_AUTHN_GRPC_PORT=[Service gRPC port] MF_AUTHN_SECRET=[String used for signing tokens] MF_AUTHN_SERVER_CERT=[Path to server certificate] MF_AUTHN_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-authn
|
||||
```
|
||||
|
||||
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of AuthN service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthNServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthNServiceClient {
|
||||
return &grpcClient{
|
||||
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.AuthNService",
|
||||
"Issue",
|
||||
encodeIssueRequest,
|
||||
decodeIssueResponse,
|
||||
mainflux.UserID{},
|
||||
).Endpoint()),
|
||||
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.AuthNService",
|
||||
"Identify",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
mainflux.UserID{},
|
||||
).Endpoint()),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.issue(ctx, issueReq{issuer: req.GetIssuer(), keyType: req.Type})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.Token{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &mainflux.IssueReq{Issuer: req.issuer, Type: req.keyType}, nil
|
||||
}
|
||||
|
||||
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserID)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserID, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.UserID{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(identityReq)
|
||||
return &mainflux.Token{Value: req.token}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserID)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of AuthN service gRPC API.
|
||||
package grpc
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
key := authn.Key{
|
||||
Type: req.keyType,
|
||||
IssuedAt: now,
|
||||
}
|
||||
|
||||
k, err := svc.Issue(ctx, req.issuer, key)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
return identityRes{k.Secret, nil}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identifyEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
return identityRes{id, nil}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
)
|
||||
|
||||
var svc authn.Service
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc authn.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterAuthNServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
kind uint32
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
id: email,
|
||||
kind: authn.UserKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
id: email,
|
||||
kind: authn.RecoveryKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
id: userKey.Secret,
|
||||
kind: authn.APIKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
id: email,
|
||||
kind: 32,
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
kind: authn.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Issuer: tc.id, Type: tc.kind})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
|
||||
|
||||
apiKey, err := svc.Issue(context.Background(), userKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "identify user with recovery token",
|
||||
token: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with API token",
|
||||
token: apiKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
id: "",
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user that doesn't exist",
|
||||
token: "",
|
||||
id: "",
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.id, id.GetValue()))
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import "github.com/mainflux/mainflux/authn"
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
kind uint32
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
if req.kind != authn.UserKey &&
|
||||
req.kind != authn.APIKey &&
|
||||
req.kind != authn.RecoveryKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type issueReq struct {
|
||||
issuer string
|
||||
keyType uint32
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.issuer == "" {
|
||||
return authn.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.keyType != authn.UserKey &&
|
||||
req.keyType != authn.APIKey &&
|
||||
req.keyType != authn.RecoveryKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user