mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ada09577e | |||
| 7a747fade7 | |||
| 2d2d23b3ce | |||
| f18e96e990 | |||
| e95e0e4472 | |||
| 4f18a129b3 | |||
| b78b1eb706 | |||
| ee2c0435fc | |||
| 8fde65fee0 | |||
| 02b2facbb0 | |||
| 01125764fb | |||
| 41c9067328 | |||
| 7a6241a80d | |||
| 7b1267e394 | |||
| 2ae21cb38a | |||
| 2380af8a85 | |||
| e289794cfa | |||
| 1a977ff4b1 | |||
| 3a0a958d22 | |||
| 1c0b6623ad | |||
| 461f464329 | |||
| 391203effd | |||
| 86a63dce92 | |||
| d02df10d33 | |||
| 9849b60d68 | |||
| fa2d4c13f4 | |||
| dd9d6f0a88 | |||
| d12b1b164a | |||
| 88f5a60ebb | |||
| 7d6a8998b8 | |||
| bcf6eb5449 | |||
| 0c355c69f0 | |||
| f2eb2f76ef | |||
| e72f7e700f | |||
| 56c5e6acc7 | |||
| 639a945777 | |||
| 16471f4ab1 | |||
| c2731d34b2 | |||
| f9383cd8b0 | |||
| b2e067341c | |||
| cc99241a2b | |||
| c9b214be86 | |||
| 7cef070f47 | |||
| 375d6a83d7 | |||
| 11791c9582 | |||
| 3c2a7c00ac | |||
| 6d1136c0a0 | |||
| 7153e8aa4a | |||
| 7ef8d6f101 | |||
| 5c3d74a7e7 | |||
| a3121a6850 | |||
| ff0fba9914 | |||
| 3159cfc32a | |||
| cc45fe5006 | |||
| d42287f43a | |||
| 96144cf37b | |||
| 456adc3a6a | |||
| 3cffdfe0c4 | |||
| 052db951e3 | |||
| f47ac0753e | |||
| 161ca011ee | |||
| f3bf4af82f | |||
| 53c95f1425 | |||
| 96ea848b4f | |||
| d9cbba5f23 | |||
| 5b99f1f137 | |||
| 6641438d2a | |||
| 3f8e14c593 | |||
| 93086ce204 | |||
| dc41f3a5f1 | |||
| ef58136b85 | |||
| 045517783f | |||
| 98a21ad6d1 | |||
| 450dc6320b | |||
| 5ebd3710e3 | |||
| 44a7e1cc89 | |||
| b2f345f07c | |||
| 58670a8286 | |||
| 854b37724a | |||
| df182e3623 | |||
| fad9f86a7f | |||
| 2a312d8b4b | |||
| d736867bb5 | |||
| bd9dade57d | |||
| 937231fe2d | |||
| 3043d938c1 | |||
| ecf9bb3dc1 | |||
| 0169fe9653 | |||
| 7b640b9d36 | |||
| 714f621be8 | |||
| ad4cfc64ee | |||
| 296a315369 | |||
| 589c6d290b | |||
| 4bd83baad5 | |||
| bbbfc67c66 | |||
| 518495dfc7 | |||
| 0e4f98c0dc | |||
| 79fed471e3 | |||
| 9d41c15920 | |||
| c27a1cb982 | |||
| 321cb1d1dd | |||
| 6a31b3c3bc | |||
| 9ff6dd25c0 | |||
| a5cac6d108 | |||
| 99e1f52c23 | |||
| f4da6959d2 | |||
| e87b5f89ba | |||
| 46ed0cc584 | |||
| 9e297c4a75 | |||
| db09b536bf | |||
| 9e0226eda6 | |||
| 4629441c1f | |||
| 8502263d1f | |||
| c5203886a1 | |||
| 0e0ff18466 | |||
| 3211e79a29 | |||
| f1da6fb601 | |||
| da4471eb63 | |||
| 017ca584aa | |||
| ea9d5857e7 | |||
| 74161fa2fd | |||
| b4edae0070 | |||
| 077d9e53e6 | |||
| 59c1ab659e | |||
| b240a2dc2b | |||
| f859efc984 | |||
| 9280d7b714 | |||
| 8b1188a200 | |||
| 13bf66ca8f | |||
| 64bed5b859 | |||
| 79e3de997f | |||
| ed1b174a84 | |||
| b9aee21eff | |||
| 73240ffedd | |||
| 7f7da3bb80 | |||
| e1fe70766e | |||
| 641ada78da | |||
| 3b76ee386d | |||
| 56d8ff7a7c | |||
| 276531fcf1 | |||
| 2a3de350cc | |||
| 04d614df43 | |||
| 2e49885404 | |||
| 0e0a0b031c |
@@ -1,6 +0,0 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
docker
|
||||
metrics
|
||||
scripts
|
||||
@@ -1,244 +0,0 @@
|
||||
# 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=1
|
||||
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
|
||||
|
||||
### 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=1
|
||||
MF_THINGS_DB_PORT=5432
|
||||
MF_THINGS_DB_USER=mainflux
|
||||
MF_THINGS_DB_PASS=mainflux
|
||||
MF_THINGS_DB=things
|
||||
MF_THINGS_SECRET=secret
|
||||
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_CA_CERTS=
|
||||
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://localhost/certs
|
||||
MF_PROVISION_X509_PROVISIONING=false
|
||||
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things/configs
|
||||
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=
|
||||
|
||||
### 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=messages
|
||||
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=messages
|
||||
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_MQTT_URL=tcp://mqtt-adapter:1883
|
||||
MF_TWINS_THING_ID=
|
||||
MF_TWINS_THING_KEY=
|
||||
MF_TWINS_CHANNEL_ID=
|
||||
@@ -1 +0,0 @@
|
||||
* @mainflux/maintainers
|
||||
@@ -1,30 +0,0 @@
|
||||
<!--
|
||||
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,15 +0,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?
|
||||
|
||||
### Which issue(s) does this PR fix/relate to?
|
||||
Put here `Resolves #XXX` to auto-close the issue that your PR fixes (if such)
|
||||
|
||||
### List any changes that modify/break current functionality
|
||||
|
||||
### Have you included tests for your changes?
|
||||
|
||||
### Did you document any new/modified functionality?
|
||||
|
||||
### Notes
|
||||
@@ -1,8 +0,0 @@
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Set your private global .gitignore:
|
||||
# https://digitalfortress.tech/tricks/creating-a-global-gitignore/
|
||||
|
||||
build
|
||||
|
||||
-502
@@ -1,502 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,87 +0,0 @@
|
||||
# Contributing to Mainflux
|
||||
|
||||
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@mainflux.com](mailto:abuse@mainflux.com).
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written,
|
||||
thorough bug report.
|
||||
|
||||
Before raising a new issue, check [our issue
|
||||
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
|
||||
report:
|
||||
|
||||
- What were you trying to achieve?
|
||||
- What are the expected results?
|
||||
- What are the received results?
|
||||
- What are the steps to reproduce the issue?
|
||||
- In what environment did you encounter the issue?
|
||||
|
||||
## Pull requests
|
||||
|
||||
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should
|
||||
remain focused in scope and avoid unrelated commits.
|
||||
|
||||
**Please ask first** before embarking on any significant pull request (e.g. implementing new features,
|
||||
refactoring code etc.), otherwise you risk spending a lot of time working on something that the
|
||||
maintainers might not want to merge into the project.
|
||||
|
||||
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
|
||||
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
|
||||
|
||||
To contribute to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
|
||||
clone your fork repository, and configure the remotes:
|
||||
|
||||
```
|
||||
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 master
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
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 MF-[issue-number]
|
||||
```
|
||||
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're unsure of what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of work before making it public.
|
||||
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you
|
||||
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
|
||||
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
|
||||
Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
Push your topic branch up to your fork:
|
||||
|
||||
```
|
||||
git push origin MF-[issue-number]
|
||||
```
|
||||
|
||||
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
and detailed description.
|
||||
@@ -1,191 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
# 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.
|
||||
|
||||
[bdfl]
|
||||
|
||||
[[drasko]]
|
||||
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
|
||||
# when BDFL completely disagrees with the decisions made.
|
||||
# In the normal flow of events, decisions on the project design are made through discussions,
|
||||
# most often on the Pull Requests.
|
||||
#
|
||||
# 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 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@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,123 +0,0 @@
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
|
||||
define compile_service
|
||||
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
|
||||
$(eval svc=$(subst docker_,,$(1)))
|
||||
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--tag=mainflux/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
define make_docker_dev
|
||||
$(eval svc=$(subst docker_dev_,,$(1)))
|
||||
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(svc) \
|
||||
--tag=mainflux/$(svc) \
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
|
||||
cleandocker:
|
||||
# Stop all containers (if running)
|
||||
docker-compose -f docker/docker-compose.yml stop
|
||||
# Remove mainflux containers
|
||||
docker ps -f name=mainflux -aq | xargs -r docker rm
|
||||
|
||||
# Remove exited containers
|
||||
docker ps -f name=mainflux -f status=dead -f status=exited -aq | xargs -r docker rm -v
|
||||
|
||||
# Remove unused images
|
||||
docker images "mainflux\/*" -f dangling=true -q | xargs -r docker rmi
|
||||
|
||||
# Remove old mainflux images
|
||||
docker images -q mainflux\/* | xargs -r docker rmi
|
||||
|
||||
ifdef pv
|
||||
# Remove unused volumes
|
||||
docker volume ls -f name=mainflux -f dangling=true -q | xargs -r docker volume rm
|
||||
endif
|
||||
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
test:
|
||||
go test -mod=vendor -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
|
||||
|
||||
proto:
|
||||
protoc --gofast_out=plugins=grpc:. *.proto
|
||||
protoc --gofast_out=plugins=grpc:. messaging/*.proto
|
||||
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
|
||||
$(DOCKERS):
|
||||
$(call make_docker,$(@),$(GOARCH))
|
||||
|
||||
$(DOCKERS_DEV):
|
||||
$(call make_docker_dev,$(@))
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc:$(1); \
|
||||
done
|
||||
endef
|
||||
|
||||
changelog:
|
||||
git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s"
|
||||
|
||||
latest: dockers
|
||||
$(call docker_push,latest)
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
done
|
||||
$(call docker_push,$(version))
|
||||
|
||||
rundev:
|
||||
cd scripts && ./run.sh
|
||||
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
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 \
|
||||
|
||||
# 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,166 +0,0 @@
|
||||
# Mainflux
|
||||
|
||||
[![build][ci-badge]][ci-url]
|
||||
[![go report card][grc-badge]][grc-url]
|
||||
[![coverage][cov-badge]][cov-url]
|
||||
[![license][license]](LICENSE)
|
||||
[![chat][gitter-badge]][gitter]
|
||||
|
||||
![banner][banner]
|
||||
|
||||
Mainflux is modern, scalable, secure, open-source, and patent-free IoT cloud platform written in Go.
|
||||
|
||||
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].
|
||||
|
||||
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)
|
||||
- Device management and provisioning (Zero Touch provisioning)
|
||||
- Mutual TLS Authentication (mTLS) using X.509 Certificates
|
||||
- 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]
|
||||
- [LoRaWAN][lora] network integration
|
||||
- SDK
|
||||
- CLI
|
||||
- Small memory footprint and fast execution
|
||||
- Domain-driven design architecture, high-quality code and test coverage
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following are needed to run Mainflux:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/) (version 18.09)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.24.1)
|
||||
|
||||
Developing Mainflux will also require:
|
||||
|
||||
- [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 up
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
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/mainflux-cli version
|
||||
```
|
||||
|
||||
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 [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 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.
|
||||
|
||||
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 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 Mainflux and the desire to contribute!
|
||||
|
||||
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.
|
||||
|
||||
### We're Hiring
|
||||
|
||||
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 by sending PRs :sunglasses:.
|
||||
|
||||
## Community
|
||||
|
||||
- [Google group][forum]
|
||||
- [Gitter][gitter]
|
||||
- [Twitter][twitter]
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](LICENSE)
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fmainflux%2Fmainflux?ref=badge_large)
|
||||
|
||||
[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/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/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/mainflux
|
||||
[lora]: https://lora-alliance.org/
|
||||
[kubernetes]: https://kubernetes.io/
|
||||
[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
|
||||
[manu]: https://github.com/manuIO
|
||||
[darko]: https://github.com/darkodraskovic
|
||||
[janko]: https://github.com/janko-isidorovic
|
||||
[alex]: https://github.com/anovakovic01
|
||||
[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
|
||||
+508
@@ -0,0 +1,508 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Alarms API
|
||||
description: |
|
||||
HTTP API for managing alarms service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8050
|
||||
- url: https://localhost:8050
|
||||
|
||||
tags:
|
||||
- name: alarms
|
||||
description: Everything about your Alarms
|
||||
externalDocs:
|
||||
description: Find out more about alarms
|
||||
url: https://magistrala.absmach.eu/docs/
|
||||
|
||||
paths:
|
||||
/{domainID}/alarms:
|
||||
get:
|
||||
operationId: listAlarms
|
||||
summary: List Alarms
|
||||
description: |
|
||||
Retrieves a list of alarms with optional filtering
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Order'
|
||||
- $ref: '#/components/parameters/Dir'
|
||||
- $ref: '#/components/parameters/ChannelID'
|
||||
- $ref: '#/components/parameters/ClientID'
|
||||
- $ref: '#/components/parameters/Subtopic'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
- $ref: '#/components/parameters/AssigneeID'
|
||||
- $ref: '#/components/parameters/Severity'
|
||||
- $ref: '#/components/parameters/UpdatedBy'
|
||||
- $ref: '#/components/parameters/AssignedBy'
|
||||
- $ref: '#/components/parameters/AcknowledgedBy'
|
||||
- $ref: '#/components/parameters/ResolvedBy'
|
||||
- $ref: '#/components/parameters/CreatedFrom'
|
||||
- $ref: '#/components/parameters/CreatedTo'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmsPageRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/alarms/{alarmID}:
|
||||
get:
|
||||
operationId: viewAlarm
|
||||
summary: View Alarm
|
||||
description: Retrieves an alarm by ID
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmRes'
|
||||
'400':
|
||||
description: Missing or invalid alarm ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
put:
|
||||
operationId: updateAlarm
|
||||
summary: Update Alarm
|
||||
description: Updates an existing alarm
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/AlarmUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'415':
|
||||
description: Missing or invalid content type
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
delete:
|
||||
operationId: deleteAlarm
|
||||
summary: Delete Alarm
|
||||
description: Deletes an alarm
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Alarm deleted successfully
|
||||
'400':
|
||||
description: Failed due to malformed alarm ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm 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:
|
||||
schemas:
|
||||
Alarm:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique alarm identifier
|
||||
readOnly: true
|
||||
rule_id:
|
||||
type: string
|
||||
description: Rule ID that triggered this alarm
|
||||
domain_id:
|
||||
type: string
|
||||
description: Domain ID this alarm belongs to
|
||||
channel_id:
|
||||
type: string
|
||||
description: Channel ID where the alarm was triggered
|
||||
client_id:
|
||||
type: string
|
||||
description: Client ID that triggered the alarm
|
||||
subtopic:
|
||||
type: string
|
||||
description: Subtopic associated with the alarm
|
||||
status:
|
||||
type: string
|
||||
description: Alarm status
|
||||
enum: [active, cleared]
|
||||
measurement:
|
||||
type: string
|
||||
description: Measurement that triggered the alarm
|
||||
value:
|
||||
type: string
|
||||
description: Value that triggered the alarm
|
||||
unit:
|
||||
type: string
|
||||
description: Unit of measurement
|
||||
threshold:
|
||||
type: string
|
||||
description: Threshold value that was exceeded
|
||||
cause:
|
||||
type: string
|
||||
description: Cause or description of the alarm
|
||||
severity:
|
||||
type: integer
|
||||
description: Severity level (0-100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
assignee_id:
|
||||
type: string
|
||||
description: ID of the user assigned to this alarm
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
description: User who last updated the alarm
|
||||
readOnly: true
|
||||
assigned_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was assigned
|
||||
readOnly: true
|
||||
assigned_by:
|
||||
type: string
|
||||
description: User who assigned the alarm
|
||||
readOnly: true
|
||||
acknowledged_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was acknowledged
|
||||
readOnly: true
|
||||
acknowledged_by:
|
||||
type: string
|
||||
description: User who acknowledged the alarm
|
||||
readOnly: true
|
||||
resolved_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was resolved
|
||||
readOnly: true
|
||||
resolved_by:
|
||||
type: string
|
||||
description: User who resolved the alarm
|
||||
readOnly: true
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties: true
|
||||
|
||||
AlarmsPage:
|
||||
type: object
|
||||
properties:
|
||||
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
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
default: 10
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results
|
||||
minimum: 0
|
||||
alarms:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: '#/components/schemas/Alarm'
|
||||
required:
|
||||
- alarms
|
||||
- total
|
||||
- offset
|
||||
- limit
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Domain ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
AlarmID:
|
||||
name: alarmID
|
||||
description: Alarm ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
Order:
|
||||
name: order
|
||||
description: Order by field
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [created_at, updated_at]
|
||||
default: created_at
|
||||
Dir:
|
||||
name: dir
|
||||
description: Sort direction
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
default: desc
|
||||
ChannelID:
|
||||
name: channel_id
|
||||
description: Filter by channel ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
ClientID:
|
||||
name: client_id
|
||||
description: Filter by client ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Subtopic:
|
||||
name: subtopic
|
||||
description: Filter by subtopic
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
RuleID:
|
||||
name: rule_id
|
||||
description: Filter by rule ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Status:
|
||||
name: status
|
||||
description: Filter by alarm status
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [active, cleared, all]
|
||||
default: all
|
||||
AssigneeID:
|
||||
name: assignee_id
|
||||
description: Filter by assignee ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Severity:
|
||||
name: severity
|
||||
description: Filter by severity level
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
UpdatedBy:
|
||||
name: updated_by
|
||||
description: Filter by user who updated
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
AssignedBy:
|
||||
name: assigned_by
|
||||
description: Filter by user who assigned
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
AcknowledgedBy:
|
||||
name: acknowledged_by
|
||||
description: Filter by user who acknowledged
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
ResolvedBy:
|
||||
name: resolved_by
|
||||
description: Filter by user who resolved
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter alarms created after this time (RFC3339 format)
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter alarms created before this time (RFC3339 format)
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
requestBodies:
|
||||
AlarmUpdateReq:
|
||||
description: JSON-formatted document describing the alarm update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Alarm status
|
||||
enum: [active, cleared]
|
||||
assignee_id:
|
||||
type: string
|
||||
description: ID of the user assigned to this alarm
|
||||
severity:
|
||||
type: integer
|
||||
description: Severity level (0-100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties: true
|
||||
|
||||
responses:
|
||||
AlarmRes:
|
||||
description: Alarm data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Alarm'
|
||||
links:
|
||||
update:
|
||||
operationId: updateAlarm
|
||||
parameters:
|
||||
alarmID: $response.body#/id
|
||||
domainID: $response.body#/domain_id
|
||||
delete:
|
||||
operationId: deleteAlarm
|
||||
parameters:
|
||||
alarmID: $response.body#/id
|
||||
domainID: $response.body#/domain_id
|
||||
AlarmsPageRes:
|
||||
description: Alarms page retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlarmsPage'
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred
|
||||
HealthRes:
|
||||
description: Service Health Check
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mainflux
|
||||
|
||||
// Response contains HTTP response specific methods.
|
||||
type Response interface {
|
||||
// Code returns HTTP response code.
|
||||
Code() int
|
||||
|
||||
// Headers returns map of HTTP headers with their values.
|
||||
Headers() map[string]string
|
||||
|
||||
// Empty indicates if HTTP response has content.
|
||||
Empty() bool
|
||||
}
|
||||
@@ -0,0 +1,851 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Magistrala 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 Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.0
|
||||
|
||||
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://magistrala.absmach.eu/docs/
|
||||
- name: PATs
|
||||
description: Everything about your Personal Access Tokens.
|
||||
externalDocs:
|
||||
description: Find out more about Personal Access Tokens
|
||||
url: https://magistrala.absmach.eu/docs/
|
||||
- name: Health
|
||||
description: Service health check endpoint.
|
||||
externalDocs:
|
||||
description: Find out more about health check
|
||||
url: https://magistrala.absmach.eu/docs/
|
||||
|
||||
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.
|
||||
"422":
|
||||
description: Service can't process 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.
|
||||
"422":
|
||||
description: Service can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats:
|
||||
post:
|
||||
operationId: createPAT
|
||||
tags:
|
||||
- PATs
|
||||
summary: Create a new Personal Access Token
|
||||
description: |
|
||||
Creates a new Personal Access Token (PAT) for the authenticated user.
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CreatePATRequest"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/PATRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON or validation errors.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: listPATs
|
||||
tags:
|
||||
- PATs
|
||||
summary: List all Personal Access Tokens
|
||||
description: |
|
||||
Lists all Personal Access Tokens (PATs) for the authenticated user.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/PATsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
delete:
|
||||
operationId: clearAllPATs
|
||||
tags:
|
||||
- PATs
|
||||
summary: Remove all Personal Access Tokens
|
||||
description: |
|
||||
Removes all Personal Access Tokens (PATs) for the authenticated user.
|
||||
responses:
|
||||
"200":
|
||||
description: All PATs removed successfully.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}:
|
||||
get:
|
||||
operationId: retrievePAT
|
||||
tags:
|
||||
- PATs
|
||||
summary: Retrieve a Personal Access Token
|
||||
description: |
|
||||
Retrieves details of a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/PATRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
delete:
|
||||
operationId: deletePAT
|
||||
tags:
|
||||
- PATs
|
||||
summary: Delete a Personal Access Token
|
||||
description: |
|
||||
Deletes a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
responses:
|
||||
"204":
|
||||
description: PAT deleted successfully.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/name:
|
||||
patch:
|
||||
operationId: updatePATName
|
||||
tags:
|
||||
- PATs
|
||||
summary: Update Personal Access Token name
|
||||
description: |
|
||||
Updates the name of a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UpdatePATNameRequest"
|
||||
responses:
|
||||
"202":
|
||||
$ref: "#/components/responses/PATRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON or validation errors.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/description:
|
||||
patch:
|
||||
operationId: updatePATDescription
|
||||
tags:
|
||||
- PATs
|
||||
summary: Update Personal Access Token description
|
||||
description: |
|
||||
Updates the description of a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UpdatePATDescriptionRequest"
|
||||
responses:
|
||||
"202":
|
||||
$ref: "#/components/responses/PATRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON or validation errors.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/secret/reset:
|
||||
patch:
|
||||
operationId: resetPATSecret
|
||||
tags:
|
||||
- PATs
|
||||
summary: Reset Personal Access Token secret
|
||||
description: |
|
||||
Resets the secret of a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ResetPATSecretRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/PATRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON or validation errors.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/secret/revoke:
|
||||
patch:
|
||||
operationId: revokePATSecret
|
||||
tags:
|
||||
- PATs
|
||||
summary: Revoke Personal Access Token secret
|
||||
description: |
|
||||
Revokes the secret of a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
responses:
|
||||
"204":
|
||||
description: PAT secret revoked successfully.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/scope:
|
||||
get:
|
||||
operationId: listScopes
|
||||
tags:
|
||||
- PATs
|
||||
summary: List scopes for a Personal Access Token
|
||||
description: |
|
||||
Lists all scopes for a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ScopesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
delete:
|
||||
operationId: clearAllScopes
|
||||
tags:
|
||||
- PATs
|
||||
summary: Remove all scopes from a Personal Access Token
|
||||
description: |
|
||||
Removes all scopes from a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
responses:
|
||||
"200":
|
||||
description: All scopes removed successfully.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/scope/add:
|
||||
patch:
|
||||
operationId: addScope
|
||||
tags:
|
||||
- PATs
|
||||
summary: Add scope to a Personal Access Token
|
||||
description: |
|
||||
Adds a scope to a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/AddScopeRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Scope added successfully.
|
||||
"400":
|
||||
description: Failed due to malformed JSON or validation errors.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database cannot process the request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/pats/{patID}/scope/remove:
|
||||
patch:
|
||||
operationId: removeScope
|
||||
tags:
|
||||
- PATs
|
||||
summary: Remove scope from a Personal Access Token
|
||||
description: |
|
||||
Removes a scope from a specific Personal Access Token (PAT).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PatID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/RemoveScopeRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Scope removed successfully.
|
||||
"400":
|
||||
description: Failed due to malformed JSON or validation errors.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: PAT not found.
|
||||
"415":
|
||||
description: Missing or invalid 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:
|
||||
PAT:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: Personal Access Token unique identifier
|
||||
user_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "9118de62-c680-46b7-ad0a-21748a52833a"
|
||||
description: User ID of the PAT owner
|
||||
name:
|
||||
type: string
|
||||
example: "My PAT"
|
||||
description: Name of the Personal Access Token
|
||||
description:
|
||||
type: string
|
||||
example: "Token for automation"
|
||||
description: Description of the Personal Access Token
|
||||
secret:
|
||||
type: string
|
||||
example: "pat_1234567890abcdef"
|
||||
description: Secret value of the Personal Access Token
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26T13:31:52Z"
|
||||
description: Time when the PAT was issued
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2020-11-26T13:31:52Z"
|
||||
description: Time when the PAT expires
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26T13:31:52Z"
|
||||
description: Time when the PAT was last updated
|
||||
last_used_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26T13:31:52Z"
|
||||
description: Time when the PAT was last used
|
||||
revoked:
|
||||
type: boolean
|
||||
example: false
|
||||
description: Whether the PAT is revoked
|
||||
revoked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26T13:31:52Z"
|
||||
description: Time when the PAT was revoked
|
||||
|
||||
PATsPage:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Total number of PATs
|
||||
offset:
|
||||
type: integer
|
||||
example: 0
|
||||
description: Number of items to skip during retrieval
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Size of the subset to retrieve
|
||||
pats:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PAT"
|
||||
description: List of Personal Access Tokens
|
||||
|
||||
Scope:
|
||||
type: object
|
||||
properties:
|
||||
optional_domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
|
||||
description: Optional domain ID for the scope
|
||||
entity_type:
|
||||
type: string
|
||||
enum:
|
||||
[groups, channels, clients, domains, users, dashboards, messages]
|
||||
example: "groups"
|
||||
description: Type of entity the scope applies to
|
||||
entity_id:
|
||||
type: string
|
||||
example: "*"
|
||||
description: ID of the entity the scope applies to. '*' means all entities of the specified type.
|
||||
operation:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
create,
|
||||
read,
|
||||
list,
|
||||
update,
|
||||
delete,
|
||||
share,
|
||||
unshare,
|
||||
publish,
|
||||
subscribe,
|
||||
]
|
||||
example: "read"
|
||||
description: Operation allowed by this scope
|
||||
|
||||
ScopesPage:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Total number of scopes
|
||||
offset:
|
||||
type: integer
|
||||
example: 0
|
||||
description: Number of items to skip during retrieval
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Size of the subset to retrieve
|
||||
scopes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Scope"
|
||||
description: List of scopes
|
||||
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:
|
||||
PatID:
|
||||
name: patID
|
||||
description: Personal Access Token ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
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:
|
||||
CreatePATRequest:
|
||||
description: JSON-formatted document describing PAT creation request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- duration
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
example: "My PAT"
|
||||
description: Name of the Personal Access Token
|
||||
description:
|
||||
type: string
|
||||
example: "Token for automation"
|
||||
description: Description of the Personal Access Token
|
||||
duration:
|
||||
type: string
|
||||
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
|
||||
example: "30d"
|
||||
description: Duration for which the PAT is valid. Format is a duration string (e.g. "30d", "24h", "1y").
|
||||
|
||||
UpdatePATNameRequest:
|
||||
description: JSON-formatted document describing PAT name update request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: "New PAT Name"
|
||||
description: New name for the Personal Access Token
|
||||
|
||||
UpdatePATDescriptionRequest:
|
||||
description: JSON-formatted document describing PAT description update request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- description
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
example: "New PAT Description"
|
||||
description: New description for the Personal Access Token
|
||||
|
||||
ResetPATSecretRequest:
|
||||
description: JSON-formatted document describing PAT secret reset request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- duration
|
||||
properties:
|
||||
duration:
|
||||
type: string
|
||||
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
|
||||
example: "30d"
|
||||
description: Duration for which the new PAT secret is valid. Format is a duration string (e.g. "30d", "24h", "1y").
|
||||
|
||||
AddScopeRequest:
|
||||
description: JSON-formatted document describing add scope request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- scopes
|
||||
properties:
|
||||
scopes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Scope"
|
||||
description: List of scopes to add
|
||||
|
||||
RemoveScopeRequest:
|
||||
description: JSON-formatted document describing remove scope request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- scopes_id
|
||||
properties:
|
||||
scopes_id:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: List of scope IDs to remove
|
||||
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:
|
||||
PATRes:
|
||||
description: Personal Access Token data.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PAT"
|
||||
|
||||
PATsPageRes:
|
||||
description: Page of Personal Access Tokens.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PATsPage"
|
||||
|
||||
ScopesPageRes:
|
||||
description: Page of scopes.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ScopesPage"
|
||||
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.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
-1666
File diff suppressed because it is too large
Load Diff
-49
@@ -1,49 +0,0 @@
|
||||
// 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
@@ -1,106 +0,0 @@
|
||||
# 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
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of AuthN service HTTP API.
|
||||
package api
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"golang.org/x/net/context"
|
||||
"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
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of AuthN service gRPC API.
|
||||
package grpc
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"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/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
"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()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
t := jwt.New(secret)
|
||||
|
||||
return authn.New(repo, idp, 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()))
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthNServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
issue kitgrpc.Handler
|
||||
identify kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new AuthnServiceServer instance.
|
||||
func NewServer(tracer opentracing.Tracer, svc authn.Service) mainflux.AuthNServiceServer {
|
||||
return &grpcServer{
|
||||
issue: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssueRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
identify: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
|
||||
decodeIdentifyRequest,
|
||||
encodeIdentifyResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) Issue(ctx context.Context, req *mainflux.IssueReq) (*mainflux.Token, error) {
|
||||
_, res, err := s.issue.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.Token), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.UserID, error) {
|
||||
_, res, err := s.identify.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.UserID), nil
|
||||
}
|
||||
|
||||
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.IssueReq)
|
||||
return issueReq{issuer: req.GetIssuer(), keyType: req.GetType()}, nil
|
||||
}
|
||||
|
||||
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(identityRes)
|
||||
return &mainflux.Token{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.Token)
|
||||
return identityReq{token: req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(identityRes)
|
||||
return &mainflux.UserID{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, authn.ErrMalformedEntity):
|
||||
return status.Error(codes.InvalidArgument, "received invalid token request")
|
||||
case errors.Contains(err, authn.ErrUnauthorizedAccess):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, authn.ErrKeyExpired):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = newService()
|
||||
startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
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.(issueKeyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
newKey := authn.Key{
|
||||
Issuer: req.issuer,
|
||||
IssuedAt: now,
|
||||
Type: req.Type,
|
||||
}
|
||||
|
||||
duration := time.Duration(req.Duration * time.Second)
|
||||
if duration != 0 {
|
||||
exp := now.Add(duration)
|
||||
newKey.ExpiresAt = exp
|
||||
}
|
||||
|
||||
key, err := svc.Issue(ctx, req.issuer, newKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := issueKeyRes{
|
||||
ID: key.ID,
|
||||
Value: key.Secret,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
res.ExpiresAt = &key.ExpiresAt
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeEndpoint(svc authn.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.issuer, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return revokeKeyRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveEndpoint(svc authn.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.Retrieve(ctx, req.issuer, req.id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
authn "github.com/mainflux/mainflux/authn"
|
||||
httpapi "github.com/mainflux/mainflux/authn/api/http"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
)
|
||||
|
||||
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", 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() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, idp, t)
|
||||
}
|
||||
|
||||
func newServer(svc authn.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New())
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
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))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
uk := issueRequest{Type: authn.UserKey}
|
||||
ak := issueRequest{Type: authn.APIKey, Duration: time.Hour}
|
||||
rk := issueRequest{Type: authn.RecoveryKey}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
ct string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "issue user key",
|
||||
req: toJSON(uk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue user key wrong content type",
|
||||
req: toJSON(uk),
|
||||
ct: "", token: userKey.Secret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key wrong content type",
|
||||
req: toJSON(rk),
|
||||
ct: "",
|
||||
token: userKey.Secret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key unauthorized",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key with empty token",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid request",
|
||||
req: "{",
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON",
|
||||
req: "{invalid}",
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON content",
|
||||
req: `{"Type":{"key":"value"}}`,
|
||||
ct: contentType,
|
||||
token: "",
|
||||
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),
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
|
||||
|
||||
k, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: k.ID,
|
||||
token: loginKey.Secret,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing key",
|
||||
id: "non-existing",
|
||||
token: loginKey.Secret,
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key unauthorized",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
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))
|
||||
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
|
||||
|
||||
k, err := svc.Issue(context.Background(), userKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
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.ID,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a non-existing key",
|
||||
id: "non-existing",
|
||||
token: userKey.Secret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a key unauthorized",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
type issueKeyReq struct {
|
||||
issuer string
|
||||
Type uint32 `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.Type == authn.UserKey {
|
||||
return nil
|
||||
}
|
||||
if req.issuer == "" || (req.Type != authn.APIKey) {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type keyReq struct {
|
||||
issuer string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req keyReq) validate() error {
|
||||
if req.issuer == "" || req.id == "" {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*issueKeyRes)(nil)
|
||||
_ mainflux.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 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
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
var errUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc authn.Service, tracer opentracing.Tracer) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
mux := bone.New()
|
||||
|
||||
mux.Post("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssue,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/keys/:id", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "retrieve")(retrieveEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/keys/:id", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "revoke")(revokeEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.GetFunc("/version", mainflux.Version("auth"))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
req := issueKeyReq{
|
||||
issuer: r.Header.Get("Authorization"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(authn.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
issuer: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, authn.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, authn.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, authn.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, authn.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ authn.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc authn.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc authn.Service, logger log.Logger) authn.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Issue(ctx context.Context, issuer string, newKey authn.Key) (key authn.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
d := "infinite duration"
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
|
||||
}
|
||||
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Issue(ctx, issuer, newKey)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Revoke(ctx context.Context, owner, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Revoke(ctx, owner, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Retrieve(ctx context.Context, owner, id string) (key authn.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Retrieve(ctx, owner, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identify(ctx, key)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc authn.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc authn.Service, counter metrics.Counter, latency metrics.Histogram) authn.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Issue(ctx context.Context, issuer string, key authn.Key) (authn.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue").Add(1)
|
||||
ms.latency.With("method", "issue").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Issue(ctx, issuer, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Revoke(ctx context.Context, issuer, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke").Add(1)
|
||||
ms.latency.With("method", "revoke").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Revoke(ctx, issuer, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve").Add(1)
|
||||
ms.latency.With("method", "retrieve").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Retrieve(ctx, issuer, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, 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, key)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
|
||||
// IdentityProvider specifies an API for generating unique identifiers.
|
||||
type IdentityProvider interface {
|
||||
// ID generates the unique identifier.
|
||||
ID() (string, error)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const secret = "test"
|
||||
|
||||
func key() authn.Key {
|
||||
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
|
||||
return authn.Key{
|
||||
ID: "id",
|
||||
Type: authn.UserKey,
|
||||
Issuer: "user@email.com",
|
||||
Secret: "",
|
||||
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: exp,
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
tokenizer := jwt.New(secret)
|
||||
|
||||
emptyIssuer := key()
|
||||
emptyIssuer.Issuer = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue new token",
|
||||
key: key(),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, 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))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tokenizer := jwt.New(secret)
|
||||
|
||||
token, err := tokenizer.Issue(key())
|
||||
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
|
||||
|
||||
userKey := key()
|
||||
userKey.Type = authn.APIKey
|
||||
userKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
userToken, err := tokenizer.Issue(userKey)
|
||||
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))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "parse valid key",
|
||||
key: key(),
|
||||
token: token,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse ivalid key",
|
||||
key: authn.Key{},
|
||||
token: "invalid",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
|
||||
{
|
||||
desc: "parse expired key",
|
||||
key: authn.Key{},
|
||||
token: expToken,
|
||||
err: authn.ErrKeyExpired,
|
||||
},
|
||||
{
|
||||
desc: "parse expired user key",
|
||||
key: userKey,
|
||||
token: userToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
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))
|
||||
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
)
|
||||
|
||||
type claims struct {
|
||||
jwt.StandardClaims
|
||||
Type *uint32 `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (c claims) Valid() error {
|
||||
if c.Type == nil || *c.Type > authn.APIKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return c.StandardClaims.Valid()
|
||||
}
|
||||
|
||||
type tokenizer struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// New returns new JWT Tokenizer.
|
||||
func New(secret string) authn.Tokenizer {
|
||||
return tokenizer{secret: secret}
|
||||
}
|
||||
|
||||
func (svc tokenizer) Issue(key authn.Key) (string, error) {
|
||||
claims := claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: key.Issuer,
|
||||
Subject: key.Secret,
|
||||
IssuedAt: key.IssuedAt.UTC().Unix(),
|
||||
},
|
||||
Type: &key.Type,
|
||||
}
|
||||
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
claims.ExpiresAt = key.ExpiresAt.UTC().Unix()
|
||||
}
|
||||
if key.ID != "" {
|
||||
claims.Id = key.ID
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(svc.secret))
|
||||
}
|
||||
|
||||
func (svc tokenizer) Parse(token string) (authn.Key, error) {
|
||||
c := claims{}
|
||||
_, err := jwt.ParseWithClaims(token, &c, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, authn.ErrUnauthorizedAccess
|
||||
}
|
||||
return []byte(svc.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if e, ok := err.(*jwt.ValidationError); ok && e.Errors == jwt.ValidationErrorExpired {
|
||||
// Expired User key needs to be revoked.
|
||||
if c.Type != nil && *c.Type == authn.APIKey {
|
||||
return c.toKey(), nil
|
||||
}
|
||||
return authn.Key{}, errors.Wrap(authn.ErrKeyExpired, err)
|
||||
}
|
||||
return authn.Key{}, errors.Wrap(authn.ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return c.toKey(), nil
|
||||
}
|
||||
|
||||
func (c claims) toKey() authn.Key {
|
||||
key := authn.Key{
|
||||
ID: c.Id,
|
||||
Issuer: c.Issuer,
|
||||
Secret: c.Subject,
|
||||
IssuedAt: time.Unix(c.IssuedAt, 0).UTC(),
|
||||
}
|
||||
if c.ExpiresAt != 0 {
|
||||
key.ExpiresAt = time.Unix(c.ExpiresAt, 0).UTC()
|
||||
}
|
||||
|
||||
// Default type is 0.
|
||||
if c.Type != nil {
|
||||
key.Type = *c.Type
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidKeyIssuedAt indicates that the Key is being used before it's issued.
|
||||
ErrInvalidKeyIssuedAt = errors.New("invalid issue time")
|
||||
|
||||
// ErrKeyExpired indicates that the Key is expired.
|
||||
ErrKeyExpired = errors.New("use of expired key")
|
||||
)
|
||||
|
||||
const (
|
||||
// UserKey is temporary User key received on successfull login.
|
||||
UserKey uint32 = iota
|
||||
// RecoveryKey represents a key for resseting password.
|
||||
RecoveryKey
|
||||
// APIKey enables the one to act on behalf of the user.
|
||||
APIKey
|
||||
)
|
||||
|
||||
// Key represents API key.
|
||||
type Key struct {
|
||||
ID string
|
||||
Type uint32
|
||||
Issuer string
|
||||
Secret string
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Expired verifies if the key is expired.
|
||||
func (k Key) Expired() bool {
|
||||
if k.Type == APIKey && k.ExpiresAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
return k.ExpiresAt.UTC().Before(time.Now().UTC())
|
||||
}
|
||||
|
||||
// KeyRepository specifies Key persistence API.
|
||||
type KeyRepository interface {
|
||||
// Save persists the Key. A non-nil error is returned to indicate
|
||||
// operation failure
|
||||
Save(context.Context, Key) (string, error)
|
||||
|
||||
// Retrieve retrieves Key by its unique identifier.
|
||||
Retrieve(context.Context, string, string) (Key, error)
|
||||
|
||||
// Remove removes Key with provided ID.
|
||||
Remove(context.Context, string, string) error
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExpired(t *testing.T) {
|
||||
exp := time.Now().Add(5 * time.Minute)
|
||||
exp1 := time.Now()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
expired bool
|
||||
}{
|
||||
{
|
||||
desc: "not expired key",
|
||||
key: authn.Key{
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: exp,
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
{
|
||||
desc: "expired key",
|
||||
key: authn.Key{
|
||||
IssuedAt: time.Now().UTC().Add(2 * time.Minute),
|
||||
ExpiresAt: exp1,
|
||||
},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "user key with no expiration date",
|
||||
key: authn.Key{
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "API key with no expiration date",
|
||||
key: authn.Key{
|
||||
IssuedAt: time.Now(),
|
||||
Type: authn.APIKey,
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res := tc.key.Expired()
|
||||
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.IdentityProvider = (*identityProviderMock)(nil)
|
||||
|
||||
type identityProviderMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) ID() (string, error) {
|
||||
idp.mu.Lock()
|
||||
defer idp.mu.Unlock()
|
||||
|
||||
idp.counter++
|
||||
return fmt.Sprintf("%s%012d", "123e4567-e89b-12d3-a456-", idp.counter), nil
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() authn.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.KeyRepository = (*keyRepositoryMock)(nil)
|
||||
|
||||
type keyRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
keys map[string]authn.Key
|
||||
}
|
||||
|
||||
// NewKeyRepository creates in-memory user repository
|
||||
func NewKeyRepository() authn.KeyRepository {
|
||||
return &keyRepositoryMock{
|
||||
keys: make(map[string]authn.Key),
|
||||
}
|
||||
}
|
||||
|
||||
func (krm *keyRepositoryMock) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
if _, ok := krm.keys[key.ID]; ok {
|
||||
return "", authn.ErrConflict
|
||||
}
|
||||
|
||||
krm.keys[key.ID] = key
|
||||
return key.ID, nil
|
||||
}
|
||||
func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return authn.Key{}, authn.ErrNotFound
|
||||
}
|
||||
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuer, id string) error {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
|
||||
delete(krm.keys, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains Key repository implementations using
|
||||
// PostgreSQL as the underlying database.
|
||||
package postgres
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Config defines the options that are used when connecting to a PostgreSQL instance
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Name string
|
||||
SSLMode string
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
SSLRootCert string
|
||||
}
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance and applies any
|
||||
// unapplied database migrations. A non-nil error is returned to indicate
|
||||
// failure.
|
||||
func Connect(cfg Config) (*sqlx.DB, error) {
|
||||
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
|
||||
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sqlx.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "authn",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS keys (
|
||||
id UUID NOT NULL,
|
||||
type SMALLINT,
|
||||
issuer VARCHAR(254) NOT NULL,
|
||||
issued_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP,
|
||||
PRIMARY KEY (id, issuer)
|
||||
)`,
|
||||
},
|
||||
Down: []string{"DROP TABLE IF EXISTS keys"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
)
|
||||
|
||||
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 _ authn.KeyRepository = (*repo)(nil)
|
||||
|
||||
const (
|
||||
errDuplicate = "unique_violation"
|
||||
errInvalid = "invalid_text_representation"
|
||||
)
|
||||
|
||||
type repo struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// New instantiates a PostgreSQL implementation of key repository.
|
||||
func New(db Database) authn.KeyRepository {
|
||||
return &repo{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
q := `INSERT INTO keys (id, type, issuer, issued_at, expires_at)
|
||||
VALUES (:id, :type, :issuer, :issued_at, :expires_at)`
|
||||
|
||||
dbKey := toDBKey(key)
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, dbKey); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
if pqErr.Code.Name() == errDuplicate {
|
||||
return "", errors.Wrap(authn.ErrConflict, pqErr)
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.Wrap(errSave, err)
|
||||
}
|
||||
|
||||
return dbKey.ID, nil
|
||||
}
|
||||
|
||||
func (kr repo) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
|
||||
q := `SELECT id, type, issuer, issued_at, expires_at FROM keys WHERE issuer = $1 AND id = $2`
|
||||
key := dbKey{}
|
||||
if err := kr.db.QueryRowxContext(ctx, q, issuer, id).StructScan(&key); err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return authn.Key{}, errors.Wrap(authn.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return authn.Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return toKey(key), nil
|
||||
}
|
||||
|
||||
func (kr repo) Remove(ctx context.Context, issuer, id string) error {
|
||||
q := `DELETE FROM keys WHERE issuer = :issuer AND id = :id`
|
||||
key := dbKey{
|
||||
ID: id,
|
||||
Issuer: issuer,
|
||||
}
|
||||
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"`
|
||||
Revoked bool `db:"revoked"`
|
||||
IssuedAt time.Time `db:"issued_at"`
|
||||
ExpiresAt sql.NullTime `db:"expires_at"`
|
||||
}
|
||||
|
||||
func toDBKey(key authn.Key) dbKey {
|
||||
ret := dbKey{
|
||||
ID: key.ID,
|
||||
Type: key.Type,
|
||||
Issuer: key.Issuer,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
ret.ExpiresAt = sql.NullTime{Time: key.ExpiresAt, Valid: true}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func toKey(key dbKey) authn.Key {
|
||||
ret := authn.Key{
|
||||
ID: key.ID,
|
||||
Type: key.Type,
|
||||
Issuer: key.Issuer,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if key.ExpiresAt.Valid {
|
||||
ret.ExpiresAt = key.ExpiresAt.Time
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
"github.com/mainflux/mainflux/authn/uuid"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeySave(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a new key",
|
||||
key: authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with duplicate id",
|
||||
key: authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
},
|
||||
err: authn.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
}
|
||||
_, 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 unauthorized",
|
||||
id: key.ID,
|
||||
issuer: "",
|
||||
err: authn.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unknown key",
|
||||
id: "",
|
||||
issuer: key.Issuer,
|
||||
err: authn.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) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
}
|
||||
_, err := repo.Save(opentracing.ContextWithSpan(context.Background(), opentracing.StartSpan("")), 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 does not exist",
|
||||
id: key.ID,
|
||||
issuer: key.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,78 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// 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"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
var _ Database = (*database)(nil)
|
||||
|
||||
type database struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// Database provides a database interface
|
||||
type Database interface {
|
||||
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
|
||||
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
|
||||
}
|
||||
|
||||
// NewDatabase creates a ThingDatabase instance
|
||||
func NewDatabase(db *sqlx.DB) Database {
|
||||
return &database{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (d database) NamedExecContext(ctx context.Context, query string, args interface{}) (sql.Result, error) {
|
||||
addSpanTags(ctx, query)
|
||||
return d.db.NamedExecContext(ctx, query, args)
|
||||
}
|
||||
|
||||
func (d database) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row {
|
||||
addSpanTags(ctx, query)
|
||||
return d.db.QueryRowxContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func addSpanTags(ctx context.Context, query string) {
|
||||
span := opentracing.SpanFromContext(ctx)
|
||||
if span != nil {
|
||||
span.SetTag("sql.statement", query)
|
||||
span.SetTag("span.kind", "client")
|
||||
span.SetTag("peer.service", "postgres")
|
||||
span.SetTag("db.type", "sql")
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
loginDuration = 10 * time.Hour
|
||||
recoveryDuration = 5 * time.Minute
|
||||
issuerName = "mainflux.authn"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnauthorizedAccess represents unauthorized access.
|
||||
ErrUnauthorizedAccess = errors.New("unauthorized access")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid owner or ID).
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrNotFound indicates a non-existing entity request.
|
||||
ErrNotFound = errors.New("entity not found")
|
||||
|
||||
// ErrConflict indicates that entity already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new user 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")
|
||||
)
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Issue issues a new Key.
|
||||
Issue(context.Context, string, Key) (Key, error)
|
||||
|
||||
// Revoke removes the Key with the provided id that is
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(context.Context, string, string) error
|
||||
|
||||
// Retrieve retrieves data for the Key identified by the provided
|
||||
// ID, that is issued by the user identified by the provided key.
|
||||
Retrieve(context.Context, string, 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(context.Context, string) (string, error)
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
idp IdentityProvider
|
||||
tokenizer Tokenizer
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, idp IdentityProvider, tokenizer Tokenizer) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
idp: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Issue(ctx context.Context, issuer string, key Key) (Key, error) {
|
||||
if key.IssuedAt.IsZero() {
|
||||
return Key{}, ErrInvalidKeyIssuedAt
|
||||
}
|
||||
switch key.Type {
|
||||
case APIKey:
|
||||
return svc.userKey(ctx, issuer, key)
|
||||
case RecoveryKey:
|
||||
return svc.tmpKey(issuer, recoveryDuration, key)
|
||||
default:
|
||||
return svc.tmpKey(issuer, loginDuration, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, issuer, id string) error {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
if err := svc.keys.Remove(ctx, email, id); err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error) {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return svc.keys.Retrieve(ctx, email, id)
|
||||
}
|
||||
|
||||
func (svc service) Identify(ctx context.Context, token string) (string, error) {
|
||||
c, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errIdentify, err)
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case APIKey:
|
||||
k, err := svc.keys.Retrieve(ctx, c.Issuer, c.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Auto revoke expired key.
|
||||
if k.Expired() {
|
||||
svc.keys.Remove(ctx, c.Issuer, c.ID)
|
||||
return "", ErrKeyExpired
|
||||
}
|
||||
return c.Issuer, nil
|
||||
case RecoveryKey, UserKey:
|
||||
if c.Issuer != issuerName {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
return c.Secret, nil
|
||||
default:
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) tmpKey(issuer string, duration time.Duration, key Key) (Key, error) {
|
||||
key.Secret = issuer
|
||||
key.Issuer = issuerName
|
||||
key.ExpiresAt = key.IssuedAt.Add(duration)
|
||||
val, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
key.Secret = val
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, error) {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.Issuer = email
|
||||
|
||||
id, err := svc.idp.ID()
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.ID = id
|
||||
|
||||
value, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.Secret = value
|
||||
|
||||
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (svc service) login(token string) (string, error) {
|
||||
c, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Only user key token is valid for login.
|
||||
if c.Type != UserKey {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if c.Secret == "" {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
return c.Secret, nil
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
)
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, idp, t)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue user key",
|
||||
key: authn.Key{
|
||||
Type: authn.UserKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue user key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.UserKey,
|
||||
},
|
||||
issuer: email,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthorized",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "issue API key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
key: authn.Key{
|
||||
Type: authn.RecoveryKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.RecoveryKey,
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Issue(context.Background(), tc.issuer, tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke user key",
|
||||
id: newKey.ID,
|
||||
issuer: loginKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke non-existing user key",
|
||||
id: newKey.ID,
|
||||
issuer: loginKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke unauthorized",
|
||||
id: newKey.ID,
|
||||
issuer: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Revoke(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 TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := authn.Key{
|
||||
ID: "id",
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
resetKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
|
||||
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve user key",
|
||||
id: newKey.ID,
|
||||
issuer: loginKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existing user key",
|
||||
id: "invalid",
|
||||
issuer: loginKey.Secret,
|
||||
err: authn.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unauthorized",
|
||||
id: newKey.ID,
|
||||
issuer: "wrong",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with user key",
|
||||
id: newKey.ID,
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with reset key",
|
||||
id: newKey.ID,
|
||||
issuer: resetKey.Secret,
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.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 TestIdentify(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login 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 reset key expected to succeed: %s", err))
|
||||
|
||||
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
exp1 := time.Now().Add(-2 * time.Second)
|
||||
expKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing expired user key expected to succeed: %s", err))
|
||||
|
||||
invalidKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: 22, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "identify login key",
|
||||
key: loginKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify recovery key",
|
||||
key: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify user key",
|
||||
key: userKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify expired user key",
|
||||
key: expKey.Secret,
|
||||
id: "",
|
||||
err: authn.ErrKeyExpired,
|
||||
},
|
||||
{
|
||||
desc: "identify expired key",
|
||||
key: invalidKey.Secret,
|
||||
id: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "identify invalid key",
|
||||
key: "invalid",
|
||||
id: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := svc.Identify(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))
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.id, id))
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- name: key
|
||||
description: JSON-formatted document describing the new key.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/KeyRequest"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Issued new key.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using already existing ID.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/Key"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
definitions:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
secret:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
|
||||
description: API Key value.
|
||||
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
|
||||
KeyRequest:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: e-mail
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
|
||||
// Tokenizer specifies API for encoding and decoding between string and Key.
|
||||
type Tokenizer interface {
|
||||
// Issue converts API Key to its string representation.
|
||||
Issue(Key) (string, error)
|
||||
|
||||
// Parse extracts API Key data from string token.
|
||||
Parse(string) (Key, error)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing contains middlewares that will add spans
|
||||
// to existing traces.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
saveOp = "save"
|
||||
retrieveOp = "retrieve_by_id"
|
||||
revokeOp = "remove"
|
||||
)
|
||||
|
||||
var _ authn.KeyRepository = (*keyRepositoryMiddleware)(nil)
|
||||
|
||||
// keyRepositoryMiddleware tracks request and their latency, and adds spans
|
||||
// to context.
|
||||
type keyRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo authn.KeyRepository
|
||||
}
|
||||
|
||||
// New tracks request and their latency, and adds spans
|
||||
// to context.
|
||||
func New(repo authn.KeyRepository, tracer opentracing.Tracer) authn.KeyRepository {
|
||||
return keyRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
span := createSpan(ctx, krm.tracer, saveOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return krm.repo.Save(ctx, key)
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) Retrieve(ctx context.Context, owner, id string) (authn.Key, error) {
|
||||
span := createSpan(ctx, krm.tracer, retrieveOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return krm.repo.Retrieve(ctx, owner, id)
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) Remove(ctx context.Context, owner, id string) error {
|
||||
span := createSpan(ctx, krm.tracer, revokeOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return krm.repo.Remove(ctx, owner, id)
|
||||
}
|
||||
|
||||
func createSpan(ctx context.Context, tracer opentracing.Tracer, opName string) opentracing.Span {
|
||||
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
|
||||
return tracer.StartSpan(
|
||||
opName,
|
||||
opentracing.ChildOf(parentSpan.Context()),
|
||||
)
|
||||
}
|
||||
|
||||
return tracer.StartSpan(opName)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package uuid provides a UUID identity provider.
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
)
|
||||
|
||||
// errGeneratingID indicates error in generating UUID
|
||||
var errGeneratingID = errors.New("failed to generate uuid")
|
||||
|
||||
var _ authn.IdentityProvider = (*uuidIdentityProvider)(nil)
|
||||
|
||||
type uuidIdentityProvider struct{}
|
||||
|
||||
// New instantiates a UUID identity provider.
|
||||
func New() authn.IdentityProvider {
|
||||
return &uuidIdentityProvider{}
|
||||
}
|
||||
|
||||
func (idp *uuidIdentityProvider) ID() (string, error) {
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errGeneratingID, err)
|
||||
}
|
||||
|
||||
return id.String(), nil
|
||||
}
|
||||
+1151
File diff suppressed because it is too large
Load Diff
@@ -1,162 +0,0 @@
|
||||
# BOOTSTRAP SERVICE
|
||||
|
||||
New devices need to be configured properly and connected to the Mainflux. Bootstrap service is used in order to accomplish that. This service provides the following features:
|
||||
|
||||
1) Creating new Mainflux Things
|
||||
2) Providing basic configuration for the newly created Things
|
||||
3) Enabling/disabling Things
|
||||
|
||||
Pre-provisioning a new Thing is as simple as sending Configuration data to the Bootstrap service. Once the Thing is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Things. Only enabled Things can exchange messages over Mainflux. Bootstrapping does not implicitly enable Things, it has to be done manually.
|
||||
|
||||
In order to bootstrap successfully, the Thing needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Thing is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Thing will be saved so that it can be provisioned later.
|
||||
|
||||
## Thing Configuration Entity
|
||||
|
||||
Thing Configuration consists of two logical parts: the custom configuration that can be interpreted by the Thing itself and Mainflux-related configuration. Mainflux config contains:
|
||||
|
||||
1) corresponding Mainflux Thing ID
|
||||
2) corresponding Mainflux Thing key
|
||||
3) list of the Mainflux channels the Thing is connected to
|
||||
|
||||
>Note: list of channels contains IDs of the Mainflux channels. These channels are _pre-provisioned_ on the Mainflux side and, unlike corresponding Mainflux Thing, Bootstrap service is not able to create Mainflux Channels.
|
||||
|
||||
Enabling and disabling Thing (adding Thing to/from whitelist) is as simple as connecting corresponding Mainflux Thing to the given list of Channels. Configuration keeps _state_ of the Thing:
|
||||
|
||||
| State | What it means |
|
||||
|----------|--------------------------------------------------------|
|
||||
| Inactive | Thing is created, but isn't enabled |
|
||||
| Active | Thing is able to communicate using Mainflux |
|
||||
|
||||
Switching between states `Active` and `Inactive` enables and disables Thing, respectively.
|
||||
|
||||
Thing configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Thing. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-------------------------------|-------------------------------------------------------------------------|----------------------- |
|
||||
| MF_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | error |
|
||||
| MF_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MF_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| MF_BOOTSTRAP_DB_USER | Database user | mainflux |
|
||||
| MF_BOOTSTRAP_DB_PASS | Database password | mainflux |
|
||||
| MF_BOOTSTRAP_DB | Name of the database used by the service | bootstrap |
|
||||
| MF_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
|
||||
| MF_BOOTSTRAP_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
|
||||
| MF_BOOTSTRAP_CA_CERTS | Path to trusted CAs in PEM format | |
|
||||
| MF_BOOTSTRAP_PORT | Bootstrap service HTTP port | 8180 |
|
||||
| MF_BOOTSTRAP_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_BOOTSTRAP_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_SDK_BASE_URL | Base url for Mainflux SDK | http://localhost |
|
||||
| MF_SDK_THINGS_PREFIX | SDK prefix for Things service | |
|
||||
| MF_THINGS_ES_URL | Things service event source URL | localhost:6379 |
|
||||
| MF_THINGS_ES_PASS | Things service event source password | |
|
||||
| MF_THINGS_ES_DB | Things service event source database | 0 |
|
||||
| MF_BOOTSTRAP_ES_URL | Bootstrap service event source URL | localhost:6379 |
|
||||
| MF_BOOTSTRAP_ES_PASS | Bootstrap service event source password | |
|
||||
| MF_BOOTSTRAP_ES_DB | Bootstrap service event source database | 0 |
|
||||
| MF_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_AUTHN_GRPC_URL | AuthN service gRPC URL | localhost:8181 |
|
||||
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1 |
|
||||
|
||||
## 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"
|
||||
bootstrap:
|
||||
image: mainflux/bootstrap:latest
|
||||
container_name: mainflux-bootstrap
|
||||
depends_on:
|
||||
- bootstrap-db
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 8200:8200
|
||||
environment:
|
||||
MF_BOOTSTRAP_LOG_LEVEL: [Bootstrap log level]
|
||||
MF_BOOTSTRAP_DB_HOST: [Database host address]
|
||||
MF_BOOTSTRAP_DB_PORT: [Database host port]
|
||||
MF_BOOTSTRAP_DB_USER: [Database user]
|
||||
MF_BOOTSTRAP_DB_PASS: [Database password]
|
||||
MF_BOOTSTRAP_DB: [Name of the database used by the service]
|
||||
MF_BOOTSTRAP_DB_SSL_MODE: [SSL mode to connect to the database with]
|
||||
MF_BOOTSTRAP_DB_SSL_CERT: [Path to the PEM encoded certificate file]
|
||||
MF_BOOTSTRAP_DB_SSL_KEY: [Path to the PEM encoded key file]
|
||||
MF_BOOTSTRAP_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
|
||||
MF_BOOTSTRAP_ENCRYPT_KEY: [Hex-encoded encryption key used for secure bootstrap]
|
||||
MF_BOOTSTRAP_CLIENT_TLS: [Boolean value to enable/disable client TLS]
|
||||
MF_BOOTSTRAP_CA_CERTS: [Path to trusted CAs in PEM format]
|
||||
MF_BOOTSTRAP_PORT: 8200
|
||||
MF_BOOTSTRAP_SERVER_CERT: [String path to server cert in pem format]
|
||||
MF_BOOTSTRAP_SERVER_KEY: [String path to server key in pem format]
|
||||
MF_SDK_BASE_URL: [Base SDK URL for the Mainflux services]
|
||||
MF_SDK_THINGS_PREFIX: [SDK prefix for Things service]
|
||||
MF_THINGS_ES_URL: [Things service event source URL]
|
||||
MF_THINGS_ES_PASS: [Things service event source password]
|
||||
MF_THINGS_ES_DB: [Things service event source database]
|
||||
MF_BOOTSTRAP_ES_URL: [Bootstrap service event source URL]
|
||||
MF_BOOTSTRAP_ES_PASS: [Bootstrap service event source password]
|
||||
MF_BOOTSTRAP_ES_DB: [Bootstrap service event source database]
|
||||
MF_BOOTSTRAP_EVENT_CONSUMER: [Bootstrap service event source consumer name]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
MF_AUTHN_GRPC_URL: [AuthN service gRPC URL]
|
||||
MF_AUTHN_GRPC_TIMEOUT: [AuthN service gRPC request timeout in seconds]
|
||||
```
|
||||
|
||||
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/mainflux/mainflux
|
||||
|
||||
cd mainflux
|
||||
|
||||
# compile the service
|
||||
make bootstrap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_BOOTSTRAP_LOG_LEVEL=[Bootstrap log level] \
|
||||
MF_BOOTSTRAP_DB_HOST=[Database host address] \
|
||||
MF_BOOTSTRAP_DB_PORT=[Database host port] \
|
||||
MF_BOOTSTRAP_DB_USER=[Database user] \
|
||||
MF_BOOTSTRAP_DB_PASS=[Database password] \
|
||||
MF_BOOTSTRAP_DB=[Name of the database used by the service] \
|
||||
MF_BOOTSTRAP_DB_SSL_MODE=[SSL mode to connect to the database with] \
|
||||
MF_BOOTSTRAP_DB_SSL_CERT=[Path to the PEM encoded certificate file] \
|
||||
MF_BOOTSTRAP_DB_SSL_KEY=[Path to the PEM encoded key file] \
|
||||
MF_BOOTSTRAP_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] \
|
||||
MF_BOOTSTRAP_ENCRYPT_KEY=[Hex-encoded encryption key used for secure bootstrap] \
|
||||
MF_BOOTSTRAP_CLIENT_TLS=[Boolean value to enable/disable client TLS] \
|
||||
MF_BOOTSTRAP_CA_CERTS=[Path to trusted CAs in PEM format] \
|
||||
MF_BOOTSTRAP_PORT=[Service HTTP port] \
|
||||
MF_BOOTSTRAP_SERVER_CERT=[Path to server certificate] \
|
||||
MF_BOOTSTRAP_SERVER_KEY=[Path to server key] \
|
||||
MF_SDK_BASE_URL=[Base SDK URL for the Mainflux services] \
|
||||
MF_SDK_THINGS_PREFIX=[SDK prefix for Things service] \
|
||||
MF_JAEGER_URL=[Jaeger server URL] \
|
||||
MF_AUTHN_GRPC_URL=[AuthN service gRPC URL] \
|
||||
MF_AUTHN_GRPC_TIMEOUT=[AuthN service gRPC request timeout in seconds] \
|
||||
$GOBIN/mainflux-bootstrap
|
||||
```
|
||||
|
||||
Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. This will enable TLS against the Users gRPC endpoint trusting only those CAs that are provided.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of bootstrap service HTTP API.
|
||||
package api
|
||||
@@ -1,257 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for _, c := range req.Channels {
|
||||
channels = append(channels, bootstrap.Channel{ID: c})
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
MFThing: req.ThingID,
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
MFChannels: channels,
|
||||
Name: req.Name,
|
||||
ClientCert: req.ClientCert,
|
||||
ClientKey: req.ClientKey,
|
||||
CACert: req.CACert,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: saved.MFThing,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.UpdateCert(req.key, req.thingID, req.ClientCert, req.ClientKey, req.CACert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := svc.View(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range config.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
MFThing: config.MFThing,
|
||||
MFKey: config.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
MFThing: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(req.key, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: config.MFThing,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateConnReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.UpdateConnections(req.key, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: req.id,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.List(req.key, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case req.filter.Unknown:
|
||||
res := listUnknownRes{}
|
||||
for _, cfg := range page.Configs {
|
||||
res.Configs = append(res.Configs, unknownRes{
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
MFThing: cfg.MFThing,
|
||||
MFKey: cfg.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.Remove(req.key, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := svc.Bootstrap(req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader.ReadConfig(cfg, secure)
|
||||
}
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(changeStateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.ChangeState(req.key, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stateRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,195 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update for token %s and thing %s took %s to complete", token, cfg.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_cert for thing with id %s took %s to complete", thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(token, thingID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_connections for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(token, id, connections)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list for token %s and offset %d and limit %d took %s to complete", token, offset, limit, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Remove(token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method bootstrap for thing with external id %s took %s to complete", externalID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method change_state for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_channel_handler for channel %s took %s to complete", channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_config_handler for config %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_channel_handler for channel %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method disconnect_thing_handler for channel %s and thing %s took %s to complete", channelID, thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Add(token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Update(token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(token, id, connections)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Remove(token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(token, id, state)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_thing_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_thing_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/bootstrap"
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type addReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.ExternalID == "" || req.ExternalKey == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
key string
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
key string
|
||||
thingID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.thingID == "" {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
key string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
key string
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.limit == 0 || req.limit > maxLimit {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bootstrapReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
key string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
key: tc.key,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
thingID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
thingID: "thingID",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty thing key",
|
||||
key: "key",
|
||||
thingID: "",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
key: tc.key,
|
||||
thingID: tc.thingID,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
key string
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "too large limit",
|
||||
key: "key",
|
||||
offset: 0,
|
||||
limit: maxLimit + 1,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "zero limit",
|
||||
key: "key",
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
key: tc.key,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
externKey string
|
||||
externID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := bootstrapReq{
|
||||
id: tc.externID,
|
||||
key: tc.externKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
key: "key",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*removeRes)(nil)
|
||||
_ mainflux.Response = (*configRes)(nil)
|
||||
_ mainflux.Response = (*stateRes)(nil)
|
||||
_ mainflux.Response = (*viewRes)(nil)
|
||||
_ mainflux.Response = (*listRes)(nil)
|
||||
)
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type configRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res configRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res configRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/things/configs/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res configRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
MFThing string `json:"mainflux_id,omitempty"`
|
||||
MFKey string `json:"mainflux_key,omitempty"`
|
||||
Channels []channelRes `json:"mainflux_channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (res viewRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type unknownRes struct {
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
}
|
||||
|
||||
type listUnknownRes struct {
|
||||
Configs []unknownRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type stateRes struct{}
|
||||
|
||||
func (res stateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res stateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxLimit = 100
|
||||
defaultLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errInvalidLimitParam = errors.New("invalid limit query param")
|
||||
errInvalidOffsetParam = errors.New("invalid offset query param")
|
||||
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
partialMatch = []string{"name"}
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/things/configs", kithttp.NewServer(
|
||||
addEndpoint(svc),
|
||||
decodeAddRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/configs/:id", kithttp.NewServer(
|
||||
viewEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/configs/:id", kithttp.NewServer(
|
||||
updateEndpoint(svc),
|
||||
decodeUpdateRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Patch("/things/configs/certs/:id", kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/configs/connections/:id", kithttp.NewServer(
|
||||
updateConnEndpoint(svc),
|
||||
decodeUpdateConnRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/configs", kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeListRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/unknown/configs", kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeUnknownRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/bootstrap/secure/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, true),
|
||||
decodeBootstrapRequest,
|
||||
encodeSecureRes,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/state/:id", kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Delete("/things/configs/:id", kithttp.NewServer(
|
||||
removeEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version("bootstrap"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
thingID: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUnknownRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
offset, limit, err := parsePagePrams(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
offset, limit, err := parsePagePrams(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := parseFilter(q)
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: filter,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := bootstrapReq{
|
||||
id: bone.GetValue(r, "external_id"),
|
||||
key: r.Header.Get("Authorization"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := entityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if b, ok := response.([]byte); ok {
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch errorVal := err.(type) {
|
||||
case errors.Error:
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
switch {
|
||||
case errors.Contains(errorVal, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errors.Contains(errorVal, errInvalidQueryParams):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, bootstrap.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, bootstrap.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(errorVal, bootstrap.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(errorVal, bootstrap.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(errorVal, bootstrap.ErrThings):
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
case errors.Contains(errorVal, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if errorVal.Msg() != "" {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func parseUint(s string) (uint64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
ret, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func parsePagePrams(q url.Values) (uint64, uint64, error) {
|
||||
offset, err := parseUint(q.Get("offset"))
|
||||
q.Del("offset")
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(errInvalidOffsetParam, err)
|
||||
}
|
||||
|
||||
limit, err := parseUint(q.Get("limit"))
|
||||
q.Del("limit")
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(errInvalidLimitParam, err)
|
||||
}
|
||||
|
||||
if limit > maxLimit {
|
||||
limit = maxLimit
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = defaultLimit
|
||||
}
|
||||
|
||||
return offset, limit, nil
|
||||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
ret := bootstrap.Filter{
|
||||
FullMatch: make(map[string]string),
|
||||
PartialMatch: make(map[string]string),
|
||||
}
|
||||
for k := range values {
|
||||
if contains(fullMatch, k) {
|
||||
ret.FullMatch[k] = values.Get(k)
|
||||
}
|
||||
if contains(partialMatch, k) {
|
||||
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func contains(l []string, s string) bool {
|
||||
for _, v := range l {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding Mainflux entities.
|
||||
// MFThing represents corresponding Mainflux Thing ID.
|
||||
// MFKey is key of corresponding Mainflux Thing.
|
||||
// MFChannels is a list of Mainflux Channels corresponding Mainflux Thing connects to.
|
||||
type Config struct {
|
||||
MFThing string
|
||||
Owner string
|
||||
Name string
|
||||
ClientCert string
|
||||
ClientKey string
|
||||
CACert string
|
||||
MFKey string
|
||||
MFChannels []Channel
|
||||
ExternalID string
|
||||
ExternalKey string
|
||||
Content string
|
||||
State State
|
||||
}
|
||||
|
||||
// Channel represents Mainflux channel corresponding Mainflux Thing is connected to.
|
||||
type Channel struct {
|
||||
ID string
|
||||
Name string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
Unknown bool
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Configs []Config
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(owner, id string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(owner string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(cfg Config) error
|
||||
|
||||
// UpdateCerts updates an existing Config certificate and owner.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(owner, id string, channels []Channel, connections []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(owner, id string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(owner, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(owner string, ids []string) ([]Channel, error)
|
||||
|
||||
// SaveUnknown saves Thing which unsuccessfully bootstrapped.
|
||||
SaveUnknown(owner, id string) error
|
||||
|
||||
// RetrieveUnknown returns a subset of unsuccessfully bootstrapped Things.
|
||||
RetrieveUnknown(offset, limit uint64) ConfigsPage
|
||||
|
||||
// Methods RemoveThing, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveThing removes Config of the Thing with the given ID.
|
||||
RemoveThing(id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when the corresponding Thing is
|
||||
// disconnected from the Channel.
|
||||
DisconnectThing(channelID, thingID string) error
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// Mainflux bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
const (
|
||||
emptyState = -1
|
||||
notFoundIdx = -1
|
||||
)
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepositoryMock)(nil)
|
||||
|
||||
type configRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
configs map[string]bootstrap.Config
|
||||
channels map[string]bootstrap.Channel
|
||||
unknown map[string]string
|
||||
}
|
||||
|
||||
// NewConfigsRepository creates in-memory config repository.
|
||||
func NewConfigsRepository(unknown map[string]string) bootstrap.ConfigRepository {
|
||||
return &configRepositoryMock{
|
||||
configs: make(map[string]bootstrap.Config),
|
||||
channels: make(map[string]bootstrap.Channel),
|
||||
unknown: unknown,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []string) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for _, v := range crm.configs {
|
||||
if v.MFThing == config.MFThing || v.ExternalID == config.ExternalID {
|
||||
return "", bootstrap.ErrConflict
|
||||
}
|
||||
}
|
||||
|
||||
crm.counter++
|
||||
config.MFThing = strconv.FormatUint(crm.counter, 10)
|
||||
crm.configs[config.MFThing] = config
|
||||
|
||||
for _, ch := range config.MFChannels {
|
||||
crm.channels[ch.ID] = ch
|
||||
}
|
||||
|
||||
config.MFChannels = []bootstrap.Channel{}
|
||||
|
||||
for _, ch := range connections {
|
||||
config.MFChannels = append(config.MFChannels, crm.channels[ch])
|
||||
}
|
||||
|
||||
crm.configs[config.MFThing] = config
|
||||
delete(crm.unknown, config.ExternalID)
|
||||
|
||||
return config.MFThing, nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByID(token, id string) (bootstrap.Config, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
c, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
}
|
||||
if c.Owner != token {
|
||||
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveAll(token string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
configs := make([]bootstrap.Config, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
first := uint64(offset) + 1
|
||||
last := first + uint64(limit)
|
||||
var state bootstrap.State = emptyState
|
||||
var name string
|
||||
if s, ok := filter.FullMatch["state"]; ok {
|
||||
val, _ := strconv.Atoi(s)
|
||||
state = bootstrap.State(val)
|
||||
}
|
||||
|
||||
if s, ok := filter.PartialMatch["name"]; ok {
|
||||
name = strings.ToLower(s)
|
||||
}
|
||||
|
||||
var total uint64
|
||||
for _, v := range crm.configs {
|
||||
id, _ := strconv.ParseUint(v.MFThing, 10, 64)
|
||||
if (state == emptyState || v.State == state) &&
|
||||
(name == "" || strings.Index(strings.ToLower(v.Name), name) != notFoundIdx) &&
|
||||
v.Owner == token {
|
||||
if id >= first && id < last {
|
||||
configs = append(configs, v)
|
||||
}
|
||||
total++
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(configs, func(i, j int) bool {
|
||||
return configs[i].MFThing < configs[j].MFThing
|
||||
})
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByExternalID(externalID string) (bootstrap.Config, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for _, cfg := range crm.configs {
|
||||
if cfg.ExternalID == externalID {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
cfg, ok := crm.configs[config.MFThing]
|
||||
if !ok || cfg.Owner != config.Owner {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
cfg.Name = config.Name
|
||||
cfg.Content = config.Content
|
||||
crm.configs[config.MFThing] = cfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
var forUpdate bootstrap.Config
|
||||
for _, v := range crm.configs {
|
||||
if v.MFThing == thingID && v.Owner == owner {
|
||||
forUpdate = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, ok := crm.configs[forUpdate.MFThing]; !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
forUpdate.ClientCert = clientCert
|
||||
forUpdate.ClientKey = clientKey
|
||||
forUpdate.CACert = caCert
|
||||
crm.configs[forUpdate.MFThing] = forUpdate
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
config, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
for _, ch := range channels {
|
||||
crm.channels[ch.ID] = ch
|
||||
}
|
||||
|
||||
config.MFChannels = []bootstrap.Channel{}
|
||||
for _, conn := range connections {
|
||||
ch, ok := crm.channels[conn]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
config.MFChannels = append(config.MFChannels, ch)
|
||||
}
|
||||
crm.configs[id] = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Remove(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.configs {
|
||||
if v.Owner == token && k == id {
|
||||
delete(crm.configs, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.State) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
config, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
if config.Owner != token {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
config.State = state
|
||||
crm.configs[id] = config
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
configs := []bootstrap.Config{}
|
||||
i := uint64(0)
|
||||
l := int(limit)
|
||||
var keys []string
|
||||
for k := range crm.unknown {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
if i >= offset && len(configs) < l {
|
||||
configs = append(configs, bootstrap.Config{
|
||||
ExternalID: k,
|
||||
ExternalKey: crm.unknown[k],
|
||||
})
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: uint64(len(crm.unknown)),
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveUnknown(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.unknown {
|
||||
if k == id && v == token {
|
||||
delete(crm.unknown, k)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) SaveUnknown(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.unknown[id] = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ListExisting(token string, connections []string) ([]bootstrap.Channel, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
var ret []bootstrap.Channel
|
||||
|
||||
for k, v := range crm.channels {
|
||||
for _, conn := range connections {
|
||||
if conn == k {
|
||||
ret = append(ret, v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveThing(id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
delete(crm.configs, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateChannel(ch bootstrap.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
channel, ok := crm.channels[ch.ID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
channel.Name = ch.Name
|
||||
channel.Metadata = ch.Metadata
|
||||
crm.channels[ch.ID] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveChannel(id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
delete(crm.channels, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) DisconnectThing(channelID, thingID string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
idx := -1
|
||||
if config, ok := crm.configs[thingID]; ok {
|
||||
for i, ch := range config.MFChannels {
|
||||
if ch.ID == channelID {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx != -1 {
|
||||
config.MFChannels = append(config.MFChannels[0:idx], config.MFChannels[idx:]...)
|
||||
}
|
||||
crm.configs[thingID] = config
|
||||
}
|
||||
|
||||
delete(crm.channels, channelID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
var _ things.Service = (*mainfluxThings)(nil)
|
||||
|
||||
type mainfluxThings struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
things map[string]things.Thing
|
||||
channels map[string]things.Channel
|
||||
auth mainflux.AuthNServiceClient
|
||||
connections map[string][]string
|
||||
}
|
||||
|
||||
// NewThingsService returns Mainflux Things service mock.
|
||||
// Only methods used by SDK are mocked.
|
||||
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, authn mainflux.AuthNServiceClient) things.Service {
|
||||
return &mainfluxThings{
|
||||
things: things,
|
||||
channels: channels,
|
||||
auth: authn,
|
||||
connections: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...things.Thing) ([]things.Thing, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return []things.Thing{}, things.ErrUnauthorizedAccess
|
||||
}
|
||||
for i := range ths {
|
||||
svc.counter++
|
||||
ths[i].Owner = userID.Value
|
||||
ths[i].ID = strconv.FormatUint(svc.counter, 10)
|
||||
ths[i].Key = ths[i].ID
|
||||
svc.things[ths[i].ID] = ths[i]
|
||||
}
|
||||
|
||||
return ths, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (things.Thing, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.Value {
|
||||
return t, nil
|
||||
|
||||
}
|
||||
|
||||
return things.Thing{}, things.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs []string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
for _, chID := range chIDs {
|
||||
if svc.channels[chID].Owner != userID.Value {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
for _, thID := range thIDs {
|
||||
svc.connections[chID] = append(svc.connections[chID], thID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Disconnect(_ context.Context, owner, chanID, thingID string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil || svc.channels[chanID].Owner != userID.Value {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
ids := svc.connections[chanID]
|
||||
i := 0
|
||||
for _, t := range ids {
|
||||
if t == thingID {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i == len(ids) {
|
||||
return things.ErrNotFound
|
||||
}
|
||||
|
||||
var tmp []string
|
||||
if i != len(ids)-2 {
|
||||
tmp = ids[i+1:]
|
||||
}
|
||||
ids = append(ids[:i], tmp...)
|
||||
svc.connections[chanID] = ids
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; !ok || t.Owner != userID.Value {
|
||||
return things.ErrNotFound
|
||||
}
|
||||
|
||||
delete(svc.things, id)
|
||||
conns := make(map[string][]string)
|
||||
for k, v := range svc.connections {
|
||||
i := findIndex(v, id)
|
||||
if i != -1 {
|
||||
var tmp []string
|
||||
if i != len(v)-2 {
|
||||
tmp = v[i+1:]
|
||||
}
|
||||
conns[k] = append(v[:i], tmp...)
|
||||
}
|
||||
}
|
||||
|
||||
svc.connections = conns
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewChannel(_ context.Context, owner, id string) (things.Channel, error) {
|
||||
if c, ok := svc.channels[id]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return things.Channel{}, things.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateThing(context.Context, string, things.Thing) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string, things.Metadata) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateChannels(_ context.Context, owner string, chs ...things.Channel) ([]things.Channel, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return []things.Channel{}, things.ErrUnauthorizedAccess
|
||||
}
|
||||
for i := range chs {
|
||||
svc.counter++
|
||||
chs[i].Owner = userID.Value
|
||||
chs[i].ID = strconv.FormatUint(svc.counter, 10)
|
||||
svc.channels[chs[i].ID] = chs[i]
|
||||
}
|
||||
|
||||
return chs, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateChannel(context.Context, string, things.Channel) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannels(context.Context, string, uint64, uint64, string, things.Metadata) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveChannel(context.Context, string, string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CanAccessByKey(context.Context, string, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CanAccessByID(context.Context, string, string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Identify(context.Context, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func findIndex(list []string, val string) int {
|
||||
for i, v := range list {
|
||||
if v == val {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthNServiceClient = (*serviceMock)(nil)
|
||||
|
||||
type serviceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.AuthNServiceClient {
|
||||
return &serviceMock{users}
|
||||
}
|
||||
|
||||
func (svc serviceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserID, error) {
|
||||
if id, ok := svc.users[in.Value]; ok {
|
||||
return &mainflux.UserID{Value: id}, nil
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
if id, ok := svc.users[in.GetIssuer()]; ok {
|
||||
switch in.Type {
|
||||
default:
|
||||
return &mainflux.Token{Value: id}, nil
|
||||
}
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -1,731 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
duplicateErr = "unique_violation"
|
||||
uuidErr = "invalid input syntax for type uuid"
|
||||
connConstraintErr = "connections_config_id_fkey"
|
||||
fkViolation = "foreign_key_violation"
|
||||
configFieldsNum = 8
|
||||
chanFieldsNum = 3
|
||||
connFieldsNum = 2
|
||||
cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
|
||||
SELECT channel_id FROM connections c WHERE ch.mainflux_channel = c.channel_id);`
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveDB = errors.New("failed to save bootstrap configuration to database")
|
||||
errMarshalChannel = errors.New("failed to marshal channel into json")
|
||||
errUnmarshalChannel = errors.New("failed to unmarshal json to channel")
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errRemoveUnknown = errors.New("failed to remove from uknown configurations in database")
|
||||
errSaveUnknown = errors.New("failed to insert into uknown configurations in database")
|
||||
errRetrieve = errors.New("failed to retreive bootstrap configuration from database")
|
||||
errUpdate = errors.New("failed to update bootstrap configuration in database")
|
||||
errRemove = errors.New("failed to remove bootstrap configuration from database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
|
||||
)
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
db *sqlx.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewConfigRepository instantiates a PostgreSQL implementation of config
|
||||
// repository.
|
||||
func NewConfigRepository(db *sqlx.DB, log logger.Logger) bootstrap.ConfigRepository {
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (string, error) {
|
||||
q := `INSERT INTO configs (mainflux_thing, owner, name, client_cert, client_key, ca_cert, mainflux_key, external_id, external_key, content, state)
|
||||
VALUES (:mainflux_thing, :owner, :name, :client_cert, :client_key, :ca_cert, :mainflux_key, :external_id, :external_key, :content, :state)`
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
}
|
||||
|
||||
dbcfg := toDBConfig(cfg)
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = bootstrap.ErrConflict
|
||||
}
|
||||
|
||||
cr.rollback("Failed to insert a Config", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(cfg, chsConnIDs, tx); err != nil {
|
||||
cr.rollback("Failed to insert connections", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
q = "DELETE FROM unknown_configs WHERE external_id = :external_id AND external_key = :external_key"
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
cr.rollback("Failed to remove from unknown", tx, err)
|
||||
|
||||
return "", errors.Wrap(errRemoveUnknown, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
}
|
||||
|
||||
return cfg.MFThing, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
|
||||
FROM configs
|
||||
WHERE mainflux_thing = $1 AND owner = $2`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
MFThing: id,
|
||||
Owner: owner,
|
||||
}
|
||||
|
||||
if err := cr.db.QueryRowx(q, id, owner).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
|
||||
WHERE conn.config_id = :mainflux_thing AND conn.config_owner = :owner`
|
||||
|
||||
rows, err := cr.db.NamedQuery(q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
chans := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
dbch.Owner = nullString(dbcfg.Owner)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.MFChannels = chans
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(owner string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := cr.retrieveAll(owner, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
|
||||
FROM configs %s ORDER BY mainflux_thing LIMIT $%d OFFSET $%d`
|
||||
q = fmt.Sprintf(q, search, n+1, n+2)
|
||||
|
||||
rows, err := cr.db.Query(q, append(params, limit, offset)...)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var name, content sql.NullString
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{Owner: owner}
|
||||
if err := rows.Scan(&c.MFThing, &c.MFKey, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
c.Name = name.String
|
||||
c.Content = content.String
|
||||
configs = append(configs, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q, params...).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_key, owner, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
WHERE external_id = $1`
|
||||
dbcfg := dbConfig{
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
if err := cr.db.QueryRowx(q, externalID).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
}
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
|
||||
WHERE conn.config_id = :mainflux_thing AND conn.config_owner = :owner`
|
||||
|
||||
rows, err := cr.db.NamedQuery(q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.MFChannels = channels
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET name = $1, content = $2 WHERE mainflux_thing = $3 AND owner = $4`
|
||||
|
||||
content := nullString(cfg.Content)
|
||||
name := nullString(cfg.Name)
|
||||
|
||||
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdate, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdate, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error {
|
||||
q := `UPDATE configs SET client_cert = $1, client_key = $2, ca_cert = $3 WHERE mainflux_thing = $4 AND owner = $5`
|
||||
|
||||
res, err := cr.db.Exec(q, clientCert, clientKey, caCert, thingID, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(owner, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := insertChannels(owner, channels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels during the update", tx, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateConnections(owner, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
}
|
||||
cr.rollback("Failed to update connections during the update", tx, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config update", tx, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(owner, id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
if _, err := cr.db.Exec(q, id, owner); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(owner, id string, state bootstrap.State) error {
|
||||
q := `UPDATE configs SET state = $1 WHERE mainflux_thing = $2 AND owner = $3;`
|
||||
|
||||
res, err := cr.db.Exec(q, state, id, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(owner string, ids []string) ([]bootstrap.Channel, error) {
|
||||
var channels []bootstrap.Channel
|
||||
if len(ids) == 0 {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
|
||||
rows, err := cr.db.Queryx(q, owner, pq.Array(ids))
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var dbch dbChannel
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) SaveUnknown(owner, id string) error {
|
||||
q := `INSERT INTO unknown_configs (external_id, external_key) VALUES ($1, $2)`
|
||||
|
||||
if _, err := cr.db.Exec(q, id, owner); err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errSaveUnknown, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
|
||||
q := `SELECT external_id, external_key FROM unknown_configs LIMIT $1 OFFSET $2`
|
||||
rows, err := cr.db.Query(q, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []bootstrap.Config{}
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{}
|
||||
if err := rows.Scan(&c.ExternalID, &c.ExternalKey); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM unknown_configs`)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count unknown configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: items,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveThing(id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1`
|
||||
_, err := cr.db.Exec(q, id)
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(c bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata WHERE mainflux_channel = :mainflux_channel`
|
||||
if _, err = cr.db.NamedExec(q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveChannel(id string) error {
|
||||
q := `DELETE FROM channels WHERE mainflux_channel = $1`
|
||||
if _, err := cr.db.Exec(q, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) DisconnectThing(channelID, thingID string) error {
|
||||
q := `UPDATE configs SET state = $1 WHERE EXISTS (
|
||||
SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
if _, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID); err != nil {
|
||||
return errors.Wrap(errDisconnectThing, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) retrieveAll(owner string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
template := `WHERE owner = $1 %s`
|
||||
params := []interface{}{owner}
|
||||
// One empty string so that strings Join works if only one filter is applied.
|
||||
queries := []string{""}
|
||||
// Since owner is the first param, start from 2.
|
||||
counter := 2
|
||||
for k, v := range filter.FullMatch {
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
params = append(params, v)
|
||||
counter++
|
||||
}
|
||||
for k, v := range filter.PartialMatch {
|
||||
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
|
||||
params = append(params, v)
|
||||
counter++
|
||||
}
|
||||
|
||||
f := strings.Join(queries, " AND ")
|
||||
|
||||
return fmt.Sprintf(template, f), params
|
||||
}
|
||||
|
||||
func (cr configRepository) rollback(content string, tx *sqlx.Tx, err error) {
|
||||
cr.log.Error(fmt.Sprintf("%s %s", content, err))
|
||||
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func insertChannels(owner string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chans []dbChannel
|
||||
for _, ch := range channels {
|
||||
dbch, err := toDBChannel(owner, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata)
|
||||
VALUES (:mainflux_channel, :owner, :name, :metadata)`
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = bootstrap.ErrConflict
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertConnections(cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: cfg.MFThing,
|
||||
Channel: conn,
|
||||
ConfigOwner: cfg.Owner,
|
||||
ChannelOwner: cfg.Owner,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
_, err := tx.NamedExec(q, conns)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(owner, id string, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `DELETE FROM connections
|
||||
WHERE config_id = $1 AND config_owner = $2 AND channel_owner = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
res, err := tx.Exec(q, id, owner, pq.Array(connections))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: id,
|
||||
Channel: conn,
|
||||
ConfigOwner: owner,
|
||||
ChannelOwner: owner,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(q, conns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(cleanupQuery)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
|
||||
return sql.NullString{
|
||||
String: s,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
MFThing string `db:"mainflux_thing"`
|
||||
Owner string `db:"owner"`
|
||||
Name sql.NullString `db:"name"`
|
||||
ClientCert sql.NullString `db:"client_cert"`
|
||||
ClientKey sql.NullString `db:"client_key"`
|
||||
CaCert sql.NullString `db:"ca_cert"`
|
||||
MFKey string `db:"mainflux_key"`
|
||||
ExternalID string `db:"external_id"`
|
||||
ExternalKey string `db:"external_key"`
|
||||
Content sql.NullString `db:"content"`
|
||||
State bootstrap.State `db:"state"`
|
||||
}
|
||||
|
||||
func toDBConfig(cfg bootstrap.Config) dbConfig {
|
||||
return dbConfig{
|
||||
MFThing: cfg.MFThing,
|
||||
Owner: cfg.Owner,
|
||||
Name: nullString(cfg.Name),
|
||||
ClientCert: nullString(cfg.ClientCert),
|
||||
ClientKey: nullString(cfg.ClientKey),
|
||||
CaCert: nullString(cfg.CACert),
|
||||
MFKey: cfg.MFKey,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Content: nullString(cfg.Content),
|
||||
State: cfg.State,
|
||||
}
|
||||
}
|
||||
|
||||
func toConfig(dbcfg dbConfig) bootstrap.Config {
|
||||
cfg := bootstrap.Config{
|
||||
MFThing: dbcfg.MFThing,
|
||||
Owner: dbcfg.Owner,
|
||||
MFKey: dbcfg.MFKey,
|
||||
ExternalID: dbcfg.ExternalID,
|
||||
ExternalKey: dbcfg.ExternalKey,
|
||||
State: dbcfg.State,
|
||||
}
|
||||
|
||||
if dbcfg.Name.Valid {
|
||||
cfg.Name = dbcfg.Name.String
|
||||
}
|
||||
|
||||
if dbcfg.Content.Valid {
|
||||
cfg.Content = dbcfg.Content.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientCert.Valid {
|
||||
cfg.ClientCert = dbcfg.ClientCert.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientKey.Valid {
|
||||
cfg.ClientKey = dbcfg.ClientKey.String
|
||||
}
|
||||
|
||||
if dbcfg.CaCert.Valid {
|
||||
cfg.CACert = dbcfg.CaCert.String
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type dbChannel struct {
|
||||
ID string `db:"mainflux_channel"`
|
||||
Name sql.NullString `db:"name"`
|
||||
Owner sql.NullString `db:"owner"`
|
||||
Metadata string `db:"metadata"`
|
||||
}
|
||||
|
||||
func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
dbch := dbChannel{
|
||||
ID: ch.ID,
|
||||
Name: nullString(ch.Name),
|
||||
Owner: nullString(owner),
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, errors.Wrap(errMarshalChannel, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
return dbch, nil
|
||||
}
|
||||
|
||||
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
ch := bootstrap.Channel{
|
||||
ID: dbch.ID,
|
||||
}
|
||||
|
||||
if dbch.Name.Valid {
|
||||
ch.Name = dbch.Name.String
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, errors.Wrap(errUnmarshalChannel, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type dbConnection struct {
|
||||
Config string `db:"config_id"`
|
||||
Channel string `db:"channel_id"`
|
||||
ConfigOwner string `db:"config_owner"`
|
||||
ChannelOwner string `db:"channel_owner"`
|
||||
}
|
||||
@@ -1,760 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const numConfigs = 10
|
||||
|
||||
var (
|
||||
config = bootstrap.Config{
|
||||
MFThing: "mf-thing",
|
||||
MFKey: "mf-key",
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
Owner: "user@email.com",
|
||||
MFChannels: []bootstrap.Channel{
|
||||
bootstrap.Channel{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
|
||||
bootstrap.Channel{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
}
|
||||
|
||||
channels = []string{"1", "2"}
|
||||
)
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
diff := "different"
|
||||
|
||||
duplicateThing := config
|
||||
duplicateThing.ExternalID = diff
|
||||
duplicateThing.MFKey = diff
|
||||
duplicateThing.MFChannels = []bootstrap.Channel{}
|
||||
|
||||
duplicateExternal := config
|
||||
duplicateExternal.MFThing = diff
|
||||
duplicateExternal.MFKey = diff
|
||||
duplicateExternal.MFChannels = []bootstrap.Channel{}
|
||||
|
||||
duplicateChannels := config
|
||||
duplicateChannels.ExternalID = diff
|
||||
duplicateChannels.MFKey = diff
|
||||
duplicateChannels.MFThing = diff
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a config",
|
||||
config: config,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Thing ID",
|
||||
config: duplicateThing,
|
||||
connections: nil,
|
||||
err: bootstrap.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
connections: nil,
|
||||
err: bootstrap.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Channels",
|
||||
config: duplicateChannels,
|
||||
connections: channels,
|
||||
err: bootstrap.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(tc.config, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
nonexistentConfID, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve config",
|
||||
owner: c.Owner,
|
||||
id: id,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve config with wrong owner",
|
||||
owner: "2",
|
||||
id: id,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing config",
|
||||
owner: c.Owner,
|
||||
id: nonexistentConfID.String(),
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a config with invalid ID",
|
||||
owner: c.Owner,
|
||||
id: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(tc.owner, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
c := config
|
||||
// Use UUID to prevent conflict errors.
|
||||
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = uid.String()
|
||||
c.Name = fmt.Sprintf("name %d", i)
|
||||
c.MFThing = uid.String()
|
||||
c.MFKey = uid.String()
|
||||
|
||||
if i%2 == 0 {
|
||||
c.State = bootstrap.Active
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
c.MFChannels = nil
|
||||
}
|
||||
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
offset uint64
|
||||
limit uint64
|
||||
filter bootstrap.Filter
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve subset",
|
||||
owner: config.Owner,
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
{
|
||||
desc: "retrieve wrong owner",
|
||||
owner: "2",
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all active",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(tc.owner, tc.filter, tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByExternalID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(tc.externalID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongOwner := c
|
||||
wrongOwner.Owner = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
config bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong owner",
|
||||
config: wrongOwner,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
config: c,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.Update(tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongOwner := c
|
||||
wrongOwner.Owner = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
thingID string
|
||||
owner string
|
||||
cert string
|
||||
certKey string
|
||||
ca string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong owner",
|
||||
thingID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
owner: "wrong",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
thingID: c.MFThing,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
owner: c.Owner,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateCert(tc.owner, tc.thingID, tc.cert, tc.certKey, tc.ca)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err = uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.MFChannels = []bootstrap.Channel{}
|
||||
c2, err := repo.Save(c, []string{channels[0]})
|
||||
require.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
owner: config.Owner,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
owner: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections with existing channels",
|
||||
owner: config.Owner,
|
||||
id: c2,
|
||||
channels: nil,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
owner: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateConnections(tc.owner, tc.id, tc.channels, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
// Removal works the same for both existing and non-existing
|
||||
// (removed) config
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.Remove(c.Owner, id)
|
||||
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(c.Owner, id)
|
||||
require.True(t, errors.Contains(err, bootstrap.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong owner",
|
||||
id: saved,
|
||||
owner: "2",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state with wrong id",
|
||||
id: "wrong",
|
||||
owner: c.Owner,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
id: saved,
|
||||
owner: c.Owner,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
id: saved,
|
||||
owner: c.Owner,
|
||||
state: bootstrap.Inactive,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeState(tc.owner, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListExisting(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var chs []bootstrap.Channel
|
||||
for _, ch := range config.MFChannels {
|
||||
chs = append(chs, ch)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
owner: c.Owner,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
owner: c.Owner,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
owner: c.Owner,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(tc.owner, tc.connections)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.existing, existing))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUnknown(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save unknown",
|
||||
externalID: "unknown",
|
||||
externalKey: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save invalid unknown",
|
||||
externalID: "unknown",
|
||||
externalKey: "",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.SaveUnknown(tc.externalKey, tc.externalID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveUnknown(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
id, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
repo.SaveUnknown(id.String(), id.String())
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all",
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset",
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveUnknown(tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveThing(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.RemoveThing(saved)
|
||||
assert.Nil(t, err, fmt.Sprintf("an unexpected error occured: %s\n", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
id := c.MFChannels[0].ID
|
||||
update := bootstrap.Channel{
|
||||
ID: id,
|
||||
Name: "update name",
|
||||
Metadata: map[string]interface{}{"update": "metadata update"},
|
||||
}
|
||||
err = repo.UpdateChannel(update)
|
||||
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
var retreved bootstrap.Channel
|
||||
for _, c := range cfg.MFChannels {
|
||||
if c.ID == id {
|
||||
retreved = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.RemoveChannel(c.MFChannels[0].ID)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.NotContains(t, cfg.MFChannels, c.MFChannels[0], fmt.Sprintf("expected to remove channel %s from %s", c.MFChannels[0], cfg.MFChannels))
|
||||
}
|
||||
|
||||
func TestDisconnectThing(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.MFKey = uid.String()
|
||||
c.MFThing = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.DisconnectThing(c.MFChannels[0].ID, saved)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected ti be inactive when a connection is removed from %s", cfg))
|
||||
}
|
||||
|
||||
func deleteChannels(repo bootstrap.ConfigRepository) error {
|
||||
for _, ch := range channels {
|
||||
if err := repo.RemoveChannel(ch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Config defines the options that are used when connecting to a PostgreSQL instance
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Name string
|
||||
SSLMode string
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
SSLRootCert string
|
||||
}
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance and applies any
|
||||
// unapplied database migrations. A non-nil error is returned to indicate
|
||||
// failure.
|
||||
func Connect(cfg Config) (*sqlx.DB, error) {
|
||||
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
|
||||
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sqlx.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "configs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
mainflux_thing TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
mainflux_key CHAR(36) UNIQUE NOT NULL,
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
content TEXT,
|
||||
client_cert TEXT,
|
||||
client_key TEXT,
|
||||
ca_cert TEXT,
|
||||
state BIGINT NOT NULL,
|
||||
PRIMARY KEY (mainflux_thing, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS unknown_configs (
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
PRIMARY KEY (external_id, external_key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS channels (
|
||||
mainflux_channel TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
metadata JSON,
|
||||
PRIMARY KEY (mainflux_channel, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS connections (
|
||||
channel_id TEXT,
|
||||
channel_owner VARCHAR(256),
|
||||
config_id TEXT,
|
||||
config_owner VARCHAR(256),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_thing, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE configs",
|
||||
"DROP TABLE channels",
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
wrongID = "0"
|
||||
wrongValue = "wrong-value"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = logger.New(os.Stdout, logger.Info.String())
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err = sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// bootstrapRes represent Mainflux Response to the Bootatrap request.
|
||||
// This is used as a response from ConfigReader and can easily be
|
||||
// replace with any other response format.
|
||||
type bootstrapRes struct {
|
||||
MFThing string `json:"mainflux_id"`
|
||||
MFKey string `json:"mainflux_key"`
|
||||
MFChannels []channelRes `json:"mainflux_channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
encKey []byte
|
||||
}
|
||||
|
||||
// NewConfigReader return new reader which is used to generate response
|
||||
// from the config.
|
||||
func NewConfigReader(encKey []byte) ConfigReader {
|
||||
return reader{encKey: encKey}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config, secure bool) (interface{}, error) {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
}
|
||||
|
||||
res := bootstrapRes{
|
||||
MFKey: cfg.MFKey,
|
||||
MFThing: cfg.MFThing,
|
||||
MFChannels: channels,
|
||||
Content: cfg.Content,
|
||||
ClientCert: cfg.ClientCert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
CACert: cfg.CACert,
|
||||
}
|
||||
if secure {
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.encrypt(b)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r reader) encrypt(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(r.encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(in))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
|
||||
return ciphertext, nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type readChan struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type readResp struct {
|
||||
MFThing string `json:"mainflux_id"`
|
||||
MFKey string `json:"mainflux_key"`
|
||||
MFChannels []readChan `json:"mainflux_channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func dec(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, bootstrap.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(in, in)
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
cfg := bootstrap.Config{
|
||||
MFThing: "mf_id",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
MFKey: "mf_key",
|
||||
MFChannels: []bootstrap.Channel{
|
||||
bootstrap.Channel{
|
||||
ID: "mf_id",
|
||||
Name: "mf_name",
|
||||
Metadata: map[string]interface{}{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
}
|
||||
ret := readResp{
|
||||
MFThing: "mf_id",
|
||||
MFKey: "mf_key",
|
||||
MFChannels: []readChan{
|
||||
{
|
||||
ID: "mf_id",
|
||||
Name: "mf_name",
|
||||
Metadata: map[string]interface{}{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
}
|
||||
|
||||
bin, err := json.Marshal(ret)
|
||||
require.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
|
||||
reader := bootstrap.NewConfigReader(encKey)
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
enc []byte
|
||||
secret bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "read a config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
desc: "read encrypted config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res, err := reader.ReadConfig(tc.config, tc.secret)
|
||||
require.Nil(t, err, fmt.Sprintf("Reading config to succeed: %s.\n", err))
|
||||
|
||||
if tc.secret {
|
||||
d, err := dec(res.([]byte))
|
||||
require.Nil(t, err, fmt.Sprintf("Decrypting expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, d, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, d))
|
||||
continue
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
require.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, b, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, b))
|
||||
resp, ok := res.(mainflux.Response)
|
||||
require.True(t, ok, fmt.Sprintf("If not encrypted, reader should return response."))
|
||||
assert.False(t, resp.Empty(), fmt.Sprintf("Response should not be empty %s.", err))
|
||||
assert.Equal(t, http.StatusOK, resp.Code(), fmt.Sprintf("Default config response code should be 200."))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package consumer contains events consumer for events
|
||||
// published by Things service.
|
||||
package consumer
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
type removeEvent struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
type disconnectEvent struct {
|
||||
thingID string
|
||||
channelID string
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
stream = "mainflux.things"
|
||||
group = "mainflux.bootstrap"
|
||||
|
||||
thingPrefix = "thing."
|
||||
thingRemove = thingPrefix + "remove"
|
||||
thingDisconnect = thingPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "channel."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
channelRemove = channelPrefix + "remove"
|
||||
|
||||
exists = "BUSYGROUP Consumer Group name already exists"
|
||||
)
|
||||
|
||||
// Subscriber represents event source for things and channels provisioning.
|
||||
type Subscriber interface {
|
||||
// Subscribes to given subject and receives events.
|
||||
Subscribe(string) error
|
||||
}
|
||||
|
||||
type eventStore struct {
|
||||
svc bootstrap.Service
|
||||
client *redis.Client
|
||||
consumer string
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewEventStore returns new event store instance.
|
||||
func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string, log logger.Logger) Subscriber {
|
||||
return eventStore{
|
||||
svc: svc,
|
||||
client: client,
|
||||
consumer: consumer,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Subscribe(subject string) error {
|
||||
err := es.client.XGroupCreateMkStream(stream, group, "$").Err()
|
||||
if err != nil && err.Error() != exists {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
streams, err := es.client.XReadGroup(&redis.XReadGroupArgs{
|
||||
Group: group,
|
||||
Consumer: es.consumer,
|
||||
Streams: []string{stream, ">"},
|
||||
Count: 100,
|
||||
}).Result()
|
||||
if err != nil || len(streams) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, msg := range streams[0].Messages {
|
||||
event := msg.Values
|
||||
|
||||
var err error
|
||||
switch event["operation"] {
|
||||
case thingRemove:
|
||||
rte := decodeRemoveThing(event)
|
||||
err = es.handleRemoveThing(rte)
|
||||
case thingDisconnect:
|
||||
dte := decodeDisconnectThing(event)
|
||||
err = es.handleDisconnectThing(dte)
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(event)
|
||||
err = es.handleUpdateChannel(uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(event)
|
||||
err = es.handleRemoveChannel(rce)
|
||||
}
|
||||
if err != nil {
|
||||
es.logger.Warn(fmt.Sprintf("Failed to handle event sourcing: %s", err.Error()))
|
||||
break
|
||||
}
|
||||
es.client.XAck(stream, group, msg.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveThing(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
|
||||
strmeta := read(event, "metadata", "{}")
|
||||
var metadata map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(strmeta), metadata); err != nil {
|
||||
metadata = map[string]interface{}{}
|
||||
}
|
||||
|
||||
return updateChannelEvent{
|
||||
id: read(event, "id", ""),
|
||||
name: read(event, "name", ""),
|
||||
metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
|
||||
return disconnectEvent{
|
||||
channelID: read(event, "chan_id", ""),
|
||||
thingID: read(event, "thing_id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) handleRemoveThing(rte removeEvent) error {
|
||||
return es.svc.RemoveConfigHandler(rte.id)
|
||||
}
|
||||
|
||||
func (es eventStore) handleUpdateChannel(uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
}
|
||||
return es.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (es eventStore) handleRemoveChannel(rce removeEvent) error {
|
||||
return es.svc.RemoveChannelHandler(rce.id)
|
||||
}
|
||||
|
||||
func (es eventStore) handleDisconnectThing(dte disconnectEvent) error {
|
||||
return es.svc.DisconnectThingHandler(dte.channelID, dte.thingID)
|
||||
}
|
||||
|
||||
func read(event map[string]interface{}, key, def string) string {
|
||||
val, ok := event[key].(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package producer contains the domain events needed to support
|
||||
// event sourcing of Bootstrap service actions.
|
||||
package producer
|
||||
@@ -1,134 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
const (
|
||||
configPrefix = "config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
|
||||
thingPrefix = "thing."
|
||||
thingBootstrap = thingPrefix + "bootstrap"
|
||||
thingStateChange = thingPrefix + "state_change"
|
||||
thingUpdateConnections = thingPrefix + "update_connections"
|
||||
)
|
||||
|
||||
type event interface {
|
||||
encode() map[string]interface{}
|
||||
}
|
||||
|
||||
var (
|
||||
_ event = (*createConfigEvent)(nil)
|
||||
_ event = (*updateConfigEvent)(nil)
|
||||
_ event = (*removeConfigEvent)(nil)
|
||||
_ event = (*bootstrapEvent)(nil)
|
||||
_ event = (*changeStateEvent)(nil)
|
||||
_ event = (*updateConnectionsEvent)(nil)
|
||||
)
|
||||
|
||||
type createConfigEvent struct {
|
||||
mfThing string
|
||||
owner string
|
||||
name string
|
||||
mfChannels []string
|
||||
externalID string
|
||||
content string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (cce createConfigEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": cce.mfThing,
|
||||
"owner": cce.owner,
|
||||
"name": cce.name,
|
||||
"channels": strings.Join(cce.mfChannels, ", "),
|
||||
"external_id": cce.externalID,
|
||||
"content": cce.content,
|
||||
"timestamp": cce.timestamp.Unix(),
|
||||
"operation": configCreate,
|
||||
}
|
||||
}
|
||||
|
||||
type updateConfigEvent struct {
|
||||
mfThing string
|
||||
name string
|
||||
content string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (uce updateConfigEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": uce.mfThing,
|
||||
"name": uce.name,
|
||||
"content": uce.content,
|
||||
"timestamp": uce.timestamp.Unix(),
|
||||
"operation": configUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
type removeConfigEvent struct {
|
||||
mfThing string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": rce.mfThing,
|
||||
"timestamp": rce.timestamp.Unix(),
|
||||
"operation": configRemove,
|
||||
}
|
||||
}
|
||||
|
||||
type bootstrapEvent struct {
|
||||
externalID string
|
||||
success bool
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (be bootstrapEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"timestamp": be.timestamp.Unix(),
|
||||
"operation": thingBootstrap,
|
||||
}
|
||||
}
|
||||
|
||||
type changeStateEvent struct {
|
||||
mfThing string
|
||||
state bootstrap.State
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": cse.mfThing,
|
||||
"state": cse.state.String(),
|
||||
"timestamp": cse.timestamp.Unix(),
|
||||
"operation": thingStateChange,
|
||||
}
|
||||
}
|
||||
|
||||
type updateConnectionsEvent struct {
|
||||
mfThing string
|
||||
mfChannels []string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": uce.mfThing,
|
||||
"channels": strings.Join(uce.mfChannels, ", "),
|
||||
"timestamp": uce.timestamp.Unix(),
|
||||
"operation": thingUpdateConnections,
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
wrongID = 0
|
||||
wrongValue = "wrong-value"
|
||||
)
|
||||
|
||||
var redisClient *redis.Client
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.Run("redis", "5.0-alpine", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
redisClient = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("localhost:%s", container.GetPort("6379/tcp")),
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
return redisClient.Ping().Err()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
const (
|
||||
streamID = "mainflux.bootstrap"
|
||||
streamLen = 1000
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*eventStore)(nil)
|
||||
|
||||
type eventStore struct {
|
||||
svc bootstrap.Service
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// NewEventStoreMiddleware returns wrapper around bootstrap service that sends
|
||||
// events to event store.
|
||||
func NewEventStoreMiddleware(svc bootstrap.Service, client *redis.Client) bootstrap.Service {
|
||||
return eventStore{
|
||||
svc: svc,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(token, cfg)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
var channels []string
|
||||
for _, ch := range saved.MFChannels {
|
||||
channels = append(channels, ch.ID)
|
||||
}
|
||||
|
||||
ev := createConfigEvent{
|
||||
mfThing: saved.MFThing,
|
||||
owner: saved.Owner,
|
||||
name: saved.Name,
|
||||
mfChannels: channels,
|
||||
externalID: saved.ExternalID,
|
||||
content: saved.Content,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es eventStore) View(token, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) Update(token string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(token, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConfigEvent{
|
||||
mfThing: cfg.MFThing,
|
||||
name: cfg.Name,
|
||||
content: cfg.Content,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateConnections(token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConnectionsEvent{
|
||||
mfThing: id,
|
||||
mfChannels: connections,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) List(token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (es eventStore) Remove(token, id string) error {
|
||||
if err := es.svc.Remove(token, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeConfigEvent{
|
||||
mfThing: id,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
externalID: externalID,
|
||||
timestamp: time.Now(),
|
||||
success: true,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ev.success = false
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es eventStore) ChangeState(token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := changeStateEvent{
|
||||
mfThing: id,
|
||||
state: state,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveConfigHandler(id string) error {
|
||||
return es.svc.RemoveConfigHandler(id)
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveChannelHandler(id string) error {
|
||||
return es.svc.RemoveChannelHandler(id)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateChannelHandler(channel bootstrap.Channel) error {
|
||||
return es.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (es eventStore) DisconnectThingHandler(channelID, thingID string) error {
|
||||
return es.svc.DisconnectThingHandler(channelID, thingID)
|
||||
}
|
||||
|
||||
func (es eventStore) add(ev event) error {
|
||||
record := &redis.XAddArgs{
|
||||
Stream: streamID,
|
||||
MaxLenApprox: streamLen,
|
||||
Values: ev.encode(),
|
||||
}
|
||||
|
||||
return es.client.XAdd(record).Err()
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/bootstrap/redis/producer"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
streamID = "mainflux.bootstrap"
|
||||
email = "user@example.com"
|
||||
validToken = "validToken"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
defaultTimout = 5
|
||||
|
||||
configPrefix = "config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
|
||||
thingPrefix = "thing."
|
||||
thingStateChange = thingPrefix + "state_change"
|
||||
thingBootstrap = thingPrefix + "bootstrap"
|
||||
thingUpdateConnections = thingPrefix + "update_connections"
|
||||
)
|
||||
|
||||
var (
|
||||
encKey = []byte("1234567891011121")
|
||||
|
||||
channel = bootstrap.Channel{
|
||||
ID: "1",
|
||||
Name: "name",
|
||||
Metadata: map[string]interface{}{"name": "value"},
|
||||
}
|
||||
|
||||
config = bootstrap.Config{
|
||||
ExternalID: "external_id",
|
||||
ExternalKey: "external_key",
|
||||
MFChannels: []bootstrap.Channel{channel},
|
||||
Content: "config",
|
||||
}
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(auth, configs, sdk, encKey)
|
||||
}
|
||||
|
||||
func newThingsService(auth mainflux.AuthNServiceClient) things.Service {
|
||||
channels := make(map[string]things.Channel, channelsNum)
|
||||
for i := 0; i < channelsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
channels[id] = things.Channel{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Metadata: map[string]interface{}{"meta": "data"},
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
func TestAdd(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
var channels []string
|
||||
for _, ch := range config.MFChannels {
|
||||
channels = append(channels, ch.ID)
|
||||
}
|
||||
|
||||
invalidConfig := config
|
||||
invalidConfig.MFChannels = []bootstrap.Channel{bootstrap.Channel{ID: "empty"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "create config successfully",
|
||||
config: config,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": "1",
|
||||
"owner": email,
|
||||
"name": config.Name,
|
||||
"channels": strings.Join(channels, ", "),
|
||||
"external_id": config.ExternalID,
|
||||
"content": config.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configCreate,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "create invalid config",
|
||||
config: invalidConfig,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
svcConfig, svcErr := svc.View(validToken, saved.MFThing)
|
||||
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
esConfig, esErr := svc.View(validToken, saved.MFThing)
|
||||
|
||||
assert.Equal(t, svcConfig, esConfig, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfig, esConfig))
|
||||
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
modified := saved
|
||||
modified.Content = "new-config"
|
||||
modified.Name = "new name"
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.MFThing = "unknown"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "update config successfully",
|
||||
config: modified,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": modified.MFThing,
|
||||
"name": modified.Name,
|
||||
"content": modified.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configUpdate,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "update non-existing config",
|
||||
config: nonExisting,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.token, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
connections []string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "update connections successfully",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": saved.MFThing,
|
||||
"channels": "2",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": thingUpdateConnections,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "update connections unsuccessfully",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
connections: []string{"256"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
}
|
||||
func TestList(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
offset := uint64(0)
|
||||
limit := uint64(10)
|
||||
svcConfigs, svcErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
|
||||
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
esConfigs, esErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
|
||||
|
||||
assert.Equal(t, svcConfigs, esConfigs, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfigs, esConfigs))
|
||||
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "remove config successfully",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": saved.MFThing,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configRemove,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "remove config with invalid credentials",
|
||||
id: saved.MFThing,
|
||||
token: "",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.token, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap successfully",
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: saved.ExternalKey,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"external_id": saved.ExternalID,
|
||||
"success": "1",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": thingBootstrap,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "bootstrap with an error",
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "external",
|
||||
err: bootstrap.ErrNotFound,
|
||||
event: map[string]interface{}{
|
||||
"external_id": saved.ExternalID,
|
||||
"success": "0",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": thingBootstrap,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Bootstrap(tc.externalKey, tc.externalID, false)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
state bootstrap.State
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "change state to active",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": saved.MFThing,
|
||||
"state": bootstrap.Active.String(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": thingStateChange,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "change state invalid credentials",
|
||||
id: saved.MFThing,
|
||||
token: "",
|
||||
state: bootstrap.Inactive,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.token, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func test(t *testing.T, expected, actual map[string]interface{}, description string) {
|
||||
if expected != nil && actual != nil {
|
||||
ts1 := expected["timestamp"].(int64)
|
||||
ts2, err := strconv.ParseInt(actual["timestamp"].(string), 10, 64)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid timestamp, got %s", description, err))
|
||||
val := ts1 == ts2 || ts2 <= ts1+defaultTimout
|
||||
assert.True(t, val, fmt.Sprintf("%s: timestamp is not in valid range", description))
|
||||
delete(expected, "timestamp")
|
||||
delete(actual, "timestamp")
|
||||
assert.Equal(t, expected, actual, fmt.Sprintf("%s: expected %v got %v\n", description, expected, actual))
|
||||
}
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound = errors.New("non-existent entity")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification.
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrConflict indicates that entity with the same ID or external ID already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
// ErrThings indicates failure to communicate with Mainflux Things service.
|
||||
// It can be due to networking error or invalid/unauthorized request.
|
||||
ErrThings = errors.New("failed to receive response from Things service")
|
||||
|
||||
// ErrExternalKeyNotFound indicates a non-existent bootstrap configuration for given external key
|
||||
ErrExternalKeyNotFound = errors.New("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrSecureBootstrap indicates erron in getting bootstrap configuration for given encrypted external key
|
||||
ErrSecureBootstrap = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
errAddBootstrap = errors.New("failed to add bootstrap configuration")
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
errRemoveChannel = errors.New("failed to remove channel")
|
||||
errCreateThing = errors.New("failed to create thing")
|
||||
errDisconnectThing = errors.New("failed to disconnect thing")
|
||||
errThingNotFound = errors.New("thing not found")
|
||||
errCheckChannels = errors.New("failed to check if channels exists")
|
||||
errConnectionChannels = errors.New("failed to check channels connections")
|
||||
errUpdateCert = errors.New("failed to update cert")
|
||||
)
|
||||
|
||||
var _ Service = (*bootstrapService)(nil)
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Add adds new Thing Config to the user identified by the provided token.
|
||||
Add(token string, cfg Config) (Config, error)
|
||||
|
||||
// View returns Thing Config with given ID belonging to the user identified by the given token.
|
||||
View(token, id string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(token string, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(token, thingID, clientCert, clientKey, caCert string) error
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(token, id string, connections []string) error
|
||||
|
||||
// List returns subset of Configs with given search params that belong to the
|
||||
// user identified by the given token.
|
||||
List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
Remove(token, id string) error
|
||||
|
||||
// Bootstrap returns Config to the Thing with provided external ID using external key.
|
||||
Bootstrap(externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Thing with given ID and owner.
|
||||
ChangeState(token, id string, state State) error
|
||||
|
||||
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
|
||||
// handlers for events. That's why these methods surpass ownership check.
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(channel Channel) error
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(id string) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
|
||||
DisconnectThingHandler(channelID, thingID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
// as a JSON and consumed from the client side. The purpose of this interface
|
||||
// is to provide convenient way to generate custom configuration response
|
||||
// based on the specific Config which will be consumed by the client.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config, bool) (interface{}, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
auth mainflux.AuthNServiceClient
|
||||
configs ConfigRepository
|
||||
sdk mfsdk.SDK
|
||||
encKey []byte
|
||||
reader ConfigReader
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(auth mainflux.AuthNServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
auth: auth,
|
||||
encKey: encKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
toConnect := bs.toIDList(cfg.MFChannels)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(owner, toConnect)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errCheckChannels, err)
|
||||
}
|
||||
|
||||
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), token)
|
||||
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errConnectionChannels, err)
|
||||
}
|
||||
|
||||
id := cfg.MFThing
|
||||
mfThing, err := bs.thing(token, id)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.MFThing = mfThing.ID
|
||||
cfg.Owner = owner
|
||||
cfg.State = Inactive
|
||||
cfg.MFKey = mfThing.Key
|
||||
|
||||
saved, err := bs.configs.Save(cfg, toConnect)
|
||||
if err != nil {
|
||||
if id == "" {
|
||||
if errT := bs.sdk.DeleteThing(cfg.MFThing, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
return Config{}, errors.Wrap(errAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.MFThing = saved
|
||||
cfg.MFChannels = append(cfg.MFChannels, existing...)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(token, id string) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveByID(owner, id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(token string, cfg Config) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Owner = owner
|
||||
|
||||
return bs.configs.Update(cfg)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caCert string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bs.configs.UpdateCert(owner, thingID, clientCert, clientKey, caCert); err != nil {
|
||||
return errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(token, id string, connections []string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(owner, connections)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
cfg.MFChannels = channels
|
||||
var connect, disconnect []string
|
||||
|
||||
if cfg.State == Active {
|
||||
connect = add
|
||||
disconnect = remove
|
||||
}
|
||||
|
||||
for _, c := range disconnect {
|
||||
if err := bs.sdk.DisconnectThing(id, c, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range connect {
|
||||
conIDs := mfsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{c},
|
||||
ThingIDs: []string{id},
|
||||
}
|
||||
if err := bs.sdk.Connect(conIDs, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedConnect) {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
|
||||
return bs.configs.UpdateConnections(owner, id, channels, connections)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return ConfigsPage{}, err
|
||||
}
|
||||
|
||||
if filter.Unknown {
|
||||
return bs.configs.RetrieveUnknown(offset, limit), nil
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Remove(token, id string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bs.configs.Remove(owner, id); err != nil {
|
||||
return errors.Wrap(errRemoveBootstrap, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalID)
|
||||
if err != nil {
|
||||
if errors.Contains(err, ErrNotFound) {
|
||||
bs.configs.SaveUnknown(externalKey, externalID)
|
||||
return Config{}, ErrNotFound
|
||||
}
|
||||
return cfg, errors.Wrap(errBootstrap, err)
|
||||
}
|
||||
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrSecureBootstrap, err)
|
||||
}
|
||||
externalKey = dec
|
||||
}
|
||||
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, errors.Wrap(ErrExternalKeyNotFound, ErrNotFound)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(token, id string, state State) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
|
||||
if cfg.State == state {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range cfg.MFChannels {
|
||||
conIDs := mfsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{c.ID},
|
||||
ThingIDs: []string{cfg.MFThing},
|
||||
}
|
||||
if err := bs.sdk.Connect(conIDs, token); err != nil {
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
case Inactive:
|
||||
for _, c := range cfg.MFChannels {
|
||||
if err := bs.sdk.DisconnectThing(cfg.MFThing, c.ID, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := bs.configs.ChangeState(owner, id, state); err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
|
||||
if err := bs.configs.UpdateChannel(channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(id string) error {
|
||||
if err := bs.configs.RemoveThing(id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(id string) error {
|
||||
if err := bs.configs.RemoveChannel(id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
|
||||
if err := bs.configs.DisconnectThing(channelID, thingID); err != nil {
|
||||
return errors.Wrap(errDisconnectThing, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) identify(token string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := bs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return res.GetValue(), nil
|
||||
}
|
||||
|
||||
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
|
||||
thingID := id
|
||||
var err error
|
||||
|
||||
if id == "" {
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, token)
|
||||
if err != nil {
|
||||
return mfsdk.Thing{}, errors.Wrap(errCreateThing, err)
|
||||
}
|
||||
}
|
||||
|
||||
thing, err := bs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedFetch) {
|
||||
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, ErrNotFound)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
if errT := bs.sdk.DeleteThing(thingID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
|
||||
return mfsdk.Thing{}, errors.Wrap(ErrThings, err)
|
||||
}
|
||||
|
||||
return thing, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(channels, existing []string, token string) ([]Channel, error) {
|
||||
add := make(map[string]bool, len(channels))
|
||||
for _, ch := range channels {
|
||||
add[ch] = true
|
||||
}
|
||||
|
||||
for _, ch := range existing {
|
||||
if add[ch] == true {
|
||||
delete(add, ch)
|
||||
}
|
||||
}
|
||||
|
||||
var ret []Channel
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(id, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Method updateList accepts config and channel IDs and returns three lists:
|
||||
// 1) IDs of Channels to be added
|
||||
// 2) IDs of Channels to be removed
|
||||
// 3) IDs of common Channels for these two configs
|
||||
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
|
||||
var disconnect map[string]bool
|
||||
disconnect = make(map[string]bool, len(cfg.MFChannels))
|
||||
for _, c := range cfg.MFChannels {
|
||||
disconnect[c.ID] = true
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
if disconnect[c] {
|
||||
// Don't disconnect common elements.
|
||||
delete(disconnect, c)
|
||||
continue
|
||||
}
|
||||
// Connect new elements.
|
||||
add = append(add, c)
|
||||
}
|
||||
|
||||
for v := range disconnect {
|
||||
remove = append(remove, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
var ret []string
|
||||
for _, ch := range channels {
|
||||
ret = append(ret, ch.ID)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (bs bootstrapService) dec(in string) (string, error) {
|
||||
ciphertext, err := hex.DecodeString(in)
|
||||
if err != nil {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", ErrMalformedEntity
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return string(ciphertext), nil
|
||||
}
|
||||
@@ -1,807 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
validToken = "validToken"
|
||||
invalidToken = "invalidToken"
|
||||
email = "test@example.com"
|
||||
unknown = "unknown"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
)
|
||||
|
||||
var (
|
||||
encKey = []byte("1234567891011121")
|
||||
|
||||
channel = bootstrap.Channel{
|
||||
ID: "1",
|
||||
Name: "name",
|
||||
Metadata: map[string]interface{}{"name": "value"},
|
||||
}
|
||||
|
||||
config = bootstrap.Config{
|
||||
ExternalID: "external_id",
|
||||
ExternalKey: "external_key",
|
||||
MFChannels: []bootstrap.Channel{channel},
|
||||
Content: "config",
|
||||
}
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(auth, things, sdk, encKey)
|
||||
}
|
||||
|
||||
func newThingsService(auth mainflux.AuthNServiceClient) things.Service {
|
||||
channels := make(map[string]things.Channel, channelsNum)
|
||||
for i := 0; i < channelsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
channels[id] = things.Channel{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Metadata: map[string]interface{}{"meta": "data"},
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func enc(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(in))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
neID := config
|
||||
neID.MFThing = "non-existent"
|
||||
|
||||
wrongChannels := config
|
||||
ch := channel
|
||||
ch.ID = "invalid"
|
||||
wrongChannels.MFChannels = append(wrongChannels.MFChannels, ch)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "add a new config",
|
||||
config: config,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add a config with an invalid ID",
|
||||
config: neID,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "add a config with wrong credentials",
|
||||
config: config,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "add a config with invalid list of channels",
|
||||
config: wrongChannels,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view an existing config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.View(tc.token, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
c := config
|
||||
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
modifiedCreated := saved
|
||||
modifiedCreated.Content = "new-config"
|
||||
modifiedCreated.Name = "new name"
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.MFThing = unknown
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update a config with state Created",
|
||||
config: modifiedCreated,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a non-existing config",
|
||||
config: nonExisting,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config with wrong credentials",
|
||||
config: saved,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
c := config
|
||||
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
thingKey string
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update certs for the valid config",
|
||||
thingKey: saved.MFKey,
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update cert for a non-existing config",
|
||||
thingKey: "empty",
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update config cert with wrong credentials",
|
||||
thingKey: saved.MFKey,
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateCert(tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
c := config
|
||||
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
created, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
externalID, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = externalID.String()
|
||||
active, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
err = svc.ChangeState(validToken, active.MFThing, bootstrap.Active)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.MFThing = unknown
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections for config with state Inactive",
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for config with state Active",
|
||||
token: validToken,
|
||||
id: active.MFThing,
|
||||
connections: []string{"3"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for non-existing config",
|
||||
token: validToken,
|
||||
id: "",
|
||||
connections: []string{"3"},
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections with invalid channels",
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"wrong"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update connections a config with wrong credentials",
|
||||
token: invalidToken,
|
||||
id: created.MFKey,
|
||||
connections: []string{"2", "3"},
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
numThings := 101
|
||||
var saved []bootstrap.Config
|
||||
for i := 0; i < numThings; i++ {
|
||||
c := config
|
||||
id, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = id.String()
|
||||
c.ExternalKey = id.String()
|
||||
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
|
||||
s, err := svc.Add(validToken, c)
|
||||
saved = append(saved, s)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
// Set one Thing to the different state
|
||||
err := svc.ChangeState(validToken, "42", bootstrap.Active)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
|
||||
saved[41].State = bootstrap.Active
|
||||
|
||||
unknownConfig := bootstrap.Config{
|
||||
ExternalID: unknownID,
|
||||
ExternalKey: unknownKey,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.ConfigsPage
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list configs",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs with specified name",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Configs: saved[95:96],
|
||||
},
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs unauthorized",
|
||||
config: bootstrap.ConfigsPage{},
|
||||
filter: bootstrap.Filter{},
|
||||
token: invalidToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "list last page",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 95,
|
||||
Limit: 10,
|
||||
Configs: saved[95:],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
token: validToken,
|
||||
offset: 95,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs with Active state",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 35,
|
||||
Limit: 20,
|
||||
Configs: []bootstrap.Config{saved[41]},
|
||||
},
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
token: validToken,
|
||||
offset: 35,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list unknown configs",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 20,
|
||||
Configs: []bootstrap.Config{unknownConfig},
|
||||
},
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
result, err := svc.List(tc.token, tc.filter, tc.offset, tc.limit)
|
||||
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
|
||||
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove removed config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existing config",
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.token, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
e, err := enc([]byte(saved.ExternalKey))
|
||||
require.Nil(t, err, fmt.Sprintf("Encrypting external key expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
externalKey string
|
||||
externalID string
|
||||
err error
|
||||
encrypted bool
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap using invalid external id",
|
||||
config: bootstrap.Config{},
|
||||
externalID: "invalid",
|
||||
externalKey: saved.ExternalKey,
|
||||
err: bootstrap.ErrNotFound,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap using invalid external key",
|
||||
config: bootstrap.Config{},
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap an existing config",
|
||||
config: saved,
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: saved.ExternalKey,
|
||||
err: nil,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap encrypted",
|
||||
config: saved,
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: hex.EncodeToString(e),
|
||||
err: nil,
|
||||
encrypted: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
config, err := svc.Bootstrap(tc.externalKey, tc.externalID, tc.encrypted)
|
||||
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
state bootstrap.State
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong credentials",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "change state of non-existing config",
|
||||
state: bootstrap.Active,
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to current state",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
state: bootstrap.Inactive,
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.token, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
ch := bootstrap.Channel{
|
||||
ID: channel.ID,
|
||||
Name: "new name",
|
||||
Metadata: map[string]interface{}{"meta": "new"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
channel bootstrap.Channel
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update an existing channel",
|
||||
channel: ch,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a non-existing channel",
|
||||
channel: bootstrap.Channel{ID: ""},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateChannelHandler(tc.channel)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannelHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing channel",
|
||||
id: channel.ID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove a non-existing channel",
|
||||
id: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveChannelHandler(tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove a non-existing channel",
|
||||
id: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveConfigHandler(tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectThingsHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
thingID string
|
||||
channelID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "disconnect",
|
||||
channelID: channel.ID,
|
||||
thingID: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect disconnected",
|
||||
channelID: channel.ID,
|
||||
thingID: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
// Inactive Thing is created, but not able to exchange messages using Mainflux.
|
||||
Inactive State = iota
|
||||
// Active Thing is created, configured, and whitelisted.
|
||||
Active
|
||||
)
|
||||
|
||||
// State represents corresponding Mainflux Thing state. The possible Config States
|
||||
// as well as description of what that State represents are given in the table:
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
|
||||
// | Active | Thing is able to communicate using Mainflux |
|
||||
type State int
|
||||
|
||||
// String returns string representation of State.
|
||||
func (s State) String() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform things configuration.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/things/configs:
|
||||
post:
|
||||
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: "#/parameters/Authorization"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the new config.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigReq"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /things/configs/{configId}).
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
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: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
- $ref: "#/parameters/State"
|
||||
- $ref: "#/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
description: |
|
||||
Data retrieved. Configs from this list don't contain channels.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/ConfigAuth"
|
||||
- $ref: "#/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/BootstrapRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config. Thing which attempted
|
||||
to bootstrap is saved as an unknown Thing and can be listed and
|
||||
added to the service later.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/EncConfigAuth"
|
||||
- $ref: "#/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
description: |
|
||||
Data retrieved. In this case, Bootstrap response is encrypted using
|
||||
the secret key, so an actual response is in the binary format.
|
||||
schema:
|
||||
$ref: "#/definitions/BootstrapRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config. Thing which attempted
|
||||
to bootstrap is saved as an unknown Thing and can be listed and
|
||||
added to the service later.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels)
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
put:
|
||||
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, Mainflux Thing ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
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 Mainflux channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateCertReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/connections/{configId}:
|
||||
put:
|
||||
summary: Updates channels the thing is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Thing is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: channels
|
||||
description: Array if IDs the thing is be connected to.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateConnReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/state/{configId}:
|
||||
put:
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding Mainflux Thing to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: state
|
||||
description: New state of the Config.
|
||||
in: body
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
type: integer
|
||||
enum:
|
||||
- inactive
|
||||
- active
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/unknown/configs:
|
||||
get:
|
||||
summary: Get a list of unsuccessfully bootstrapped Things
|
||||
description: |
|
||||
Retrieves a list of unknown 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: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
EncConfigAuth:
|
||||
name: configAuthorization
|
||||
description: |
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
type: integer
|
||||
enum:
|
||||
- inactive
|
||||
- active
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
|
||||
definitions:
|
||||
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: "#/definitions/ConfigRes"
|
||||
required:
|
||||
- configs
|
||||
State:
|
||||
type: integer
|
||||
enum:
|
||||
- active
|
||||
- inactive
|
||||
ConfigRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: ID of the Channel.
|
||||
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: '#/definitions/State'
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
BootstrapRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certrificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
|
||||
ConfigReq:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
thing_id:
|
||||
type: string
|
||||
description: ID of the corresponding Mainflux Thing.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigUpdateConnReq:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
ConfigUpdateCertReq:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
+722
@@ -0,0 +1,722 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Certs Service API
|
||||
description: |
|
||||
Certificate management service for issuing, renewing, revoking, and managing X.509 certificates.
|
||||
This service provides PKI functionality including certificate lifecycle management, OCSP responder,
|
||||
and CRL generation.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Abstract Machines
|
||||
license:
|
||||
name: Apache-2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9019
|
||||
description: Development server
|
||||
|
||||
tags:
|
||||
- name: certificates
|
||||
description: Certificate lifecycle management operations
|
||||
- name: pki
|
||||
description: PKI infrastructure operations (OCSP, CRL, CA)
|
||||
- name: health
|
||||
description: Service health and monitoring
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
paths:
|
||||
/{domainID}/certs/issue/{entityID}:
|
||||
post:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Issue a new certificate
|
||||
description: Issues a new X.509 certificate for the specified entity with custom subject options
|
||||
operationId: issueCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueCertRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Certificate successfully issued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CertificateResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{id}/renew:
|
||||
patch:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Renew a certificate
|
||||
description: Renews an existing certificate with extended TTL and new serial number
|
||||
operationId: renewCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/CertID'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate successfully renewed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenewCertResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{id}/revoke:
|
||||
patch:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Revoke a certificate
|
||||
description: Revokes a certificate by its serial number
|
||||
operationId: revokeCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/CertID'
|
||||
responses:
|
||||
'204':
|
||||
description: Certificate successfully revoked
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'422':
|
||||
$ref: '#/components/responses/UnprocessableEntity'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{entityID}/delete:
|
||||
delete:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Delete certificates for an entity
|
||||
description: Deletes all certificates associated with the specified entity
|
||||
operationId: deleteCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
responses:
|
||||
'204':
|
||||
description: Certificates successfully deleted
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'422':
|
||||
$ref: '#/components/responses/UnprocessableEntity'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs:
|
||||
get:
|
||||
tags:
|
||||
- certificates
|
||||
summary: List certificates
|
||||
description: Retrieves a paginated list of certificates with optional filtering by entity ID
|
||||
operationId: listCerts
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/EntityIDFilter'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificates successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CertificateListResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{id}:
|
||||
get:
|
||||
tags:
|
||||
- certificates
|
||||
summary: View certificate details
|
||||
description: Retrieves detailed information about a specific certificate by serial number
|
||||
operationId: viewCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/CertID'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate details successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/csrs/{entityID}:
|
||||
post:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Issue certificate from CSR
|
||||
description: Issues a certificate from a Certificate Signing Request (CSR)
|
||||
operationId: issueFromCSR
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
- $ref: '#/components/parameters/TTL'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate successfully issued from CSR
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/csrs/{entityID}:
|
||||
post:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Issue certificate from CSR (Internal)
|
||||
description: Issues a certificate from a CSR using internal agent authentication
|
||||
operationId: issueFromCSRInternal
|
||||
security:
|
||||
- AgentAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
- $ref: '#/components/parameters/TTL'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate successfully issued from CSR
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/ocsp:
|
||||
post:
|
||||
tags:
|
||||
- pki
|
||||
summary: OCSP responder
|
||||
description: |
|
||||
Online Certificate Status Protocol (OCSP) responder endpoint.
|
||||
Accepts both binary OCSP requests and JSON format requests.
|
||||
operationId: ocsp
|
||||
security: []
|
||||
parameters:
|
||||
- name: force_status
|
||||
in: query
|
||||
description: Force a specific OCSP status for testing
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/ocsp-request:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
description: DER-encoded OCSP request
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OCSPRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OCSP response
|
||||
content:
|
||||
application/ocsp-response:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
description: DER-encoded OCSP response
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/crl:
|
||||
get:
|
||||
tags:
|
||||
- pki
|
||||
summary: Generate Certificate Revocation List
|
||||
description: Generates and returns the current Certificate Revocation List (CRL)
|
||||
operationId: generateCRL
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: CRL successfully generated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CRLResponse'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/view-ca:
|
||||
get:
|
||||
tags:
|
||||
- pki
|
||||
summary: View CA certificate
|
||||
description: Retrieves the CA certificate chain (root and intermediate certificates)
|
||||
operationId: viewCA
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: CA certificate successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/download-ca:
|
||||
get:
|
||||
tags:
|
||||
- pki
|
||||
summary: Download CA certificate
|
||||
description: Downloads the CA certificate as a ZIP file
|
||||
operationId: downloadCA
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: CA certificate ZIP file
|
||||
content:
|
||||
application/zip:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/HealthRes'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/metrics:
|
||||
get:
|
||||
tags:
|
||||
- health
|
||||
summary: Prometheus metrics
|
||||
description: Returns Prometheus metrics for monitoring
|
||||
operationId: metrics
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: Metrics successfully retrieved
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: User authentication token
|
||||
AgentAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Agent authentication token for internal operations
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
in: path
|
||||
required: true
|
||||
description: Domain identifier
|
||||
schema:
|
||||
type: string
|
||||
EntityID:
|
||||
name: entityID
|
||||
in: path
|
||||
required: true
|
||||
description: Entity identifier for the certificate
|
||||
schema:
|
||||
type: string
|
||||
CertID:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Certificate serial number
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
in: query
|
||||
description: Number of items to skip
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
description: Maximum number of items to return
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 10
|
||||
EntityIDFilter:
|
||||
name: entity_id
|
||||
in: query
|
||||
description: Filter certificates by entity ID
|
||||
schema:
|
||||
type: string
|
||||
TTL:
|
||||
name: ttl
|
||||
in: query
|
||||
description: Time to live for the certificate (e.g., "8760h", "365d")
|
||||
schema:
|
||||
type: string
|
||||
|
||||
schemas:
|
||||
IssueCertRequest:
|
||||
type: object
|
||||
required:
|
||||
- options
|
||||
properties:
|
||||
ttl:
|
||||
type: string
|
||||
description: Time to live for the certificate (e.g., "8760h" for 1 year)
|
||||
example: "8760h"
|
||||
ip_addresses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IP addresses to include in the certificate
|
||||
example: ["192.168.1.1", "10.0.0.1"]
|
||||
options:
|
||||
$ref: '#/components/schemas/SubjectOptions'
|
||||
|
||||
SubjectOptions:
|
||||
type: object
|
||||
required:
|
||||
- common_name
|
||||
properties:
|
||||
common_name:
|
||||
type: string
|
||||
description: Common Name (CN) for the certificate subject
|
||||
example: "example.com"
|
||||
organization:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Organization (O)
|
||||
example: ["Abstract Machines"]
|
||||
organizational_unit:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Organizational Unit (OU)
|
||||
example: ["Engineering"]
|
||||
country:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Country (C)
|
||||
example: ["US"]
|
||||
province:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Province or State (ST)
|
||||
example: ["California"]
|
||||
locality:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Locality or City (L)
|
||||
example: ["San Francisco"]
|
||||
street_address:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Street Address
|
||||
example: ["123 Main St"]
|
||||
postal_code:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Postal Code
|
||||
example: ["94105"]
|
||||
dns_names:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: DNS names for Subject Alternative Names
|
||||
example: ["example.com", "www.example.com"]
|
||||
ip_addresses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IP addresses for Subject Alternative Names
|
||||
example: ["192.168.1.1"]
|
||||
|
||||
CertificateResponse:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Unique serial number of the certificate
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
key:
|
||||
type: string
|
||||
description: PEM-encoded private key
|
||||
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
||||
revoked:
|
||||
type: boolean
|
||||
description: Whether the certificate is revoked
|
||||
example: false
|
||||
expiry_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Certificate expiration time
|
||||
example: "2026-11-05T12:00:00Z"
|
||||
entity_id:
|
||||
type: string
|
||||
description: Entity identifier associated with the certificate
|
||||
example: "entity-123"
|
||||
|
||||
RenewCertResponse:
|
||||
type: object
|
||||
properties:
|
||||
certificate:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
|
||||
ViewCertResponse:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Certificate serial number
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
key:
|
||||
type: string
|
||||
description: PEM-encoded private key
|
||||
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
||||
revoked:
|
||||
type: boolean
|
||||
description: Revocation status
|
||||
example: false
|
||||
expiry_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Expiration timestamp
|
||||
example: "2026-11-05T12:00:00Z"
|
||||
entity_id:
|
||||
type: string
|
||||
description: Associated entity identifier
|
||||
example: "entity-123"
|
||||
|
||||
CertificateListResponse:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Total number of certificates
|
||||
example: 100
|
||||
offset:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Current offset
|
||||
example: 0
|
||||
limit:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Current limit
|
||||
example: 10
|
||||
certificates:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
|
||||
IssueFromCSRRequest:
|
||||
type: object
|
||||
required:
|
||||
- csr
|
||||
properties:
|
||||
csr:
|
||||
type: string
|
||||
format: byte
|
||||
description: PEM-encoded Certificate Signing Request
|
||||
example: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K..."
|
||||
|
||||
IssueFromCSRResponse:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Serial number of the issued certificate
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
revoked:
|
||||
type: boolean
|
||||
description: Revocation status
|
||||
example: false
|
||||
expiry_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Expiration timestamp
|
||||
example: "2026-11-05T12:00:00Z"
|
||||
entity_id:
|
||||
type: string
|
||||
description: Associated entity identifier
|
||||
example: "entity-123"
|
||||
|
||||
OCSPRequest:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Certificate serial number to check
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate to check
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
status:
|
||||
type: string
|
||||
description: Force a specific status (for testing)
|
||||
enum: [good, revoked, unknown]
|
||||
|
||||
CRLResponse:
|
||||
type: object
|
||||
properties:
|
||||
crl:
|
||||
type: string
|
||||
format: byte
|
||||
description: DER-encoded Certificate Revocation List
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
example: "invalid request"
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Bad request - invalid parameters or malformed request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
Unauthorized:
|
||||
description: Unauthorized - invalid or missing authentication token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
UnprocessableEntity:
|
||||
description: Unprocessable entity - request cannot be processed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
InternalServerError:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: './schemas/health_info.yaml'
|
||||
+1149
File diff suppressed because it is too large
Load Diff
-204
@@ -1,204 +0,0 @@
|
||||
# Mainflux CLI
|
||||
## Build
|
||||
From the project root:
|
||||
```bash
|
||||
make cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Service
|
||||
#### Get the version of Mainflux services
|
||||
```bash
|
||||
mainflux-cli version
|
||||
```
|
||||
|
||||
### Users management
|
||||
#### Create User
|
||||
```bash
|
||||
mainflux-cli users create <user_email> <user_password>
|
||||
```
|
||||
|
||||
#### Login User
|
||||
```bash
|
||||
mainflux-cli users token <user_email> <user_password>
|
||||
```
|
||||
|
||||
#### Retrieve User
|
||||
```bash
|
||||
mainflux-cli users get <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update User Metadata
|
||||
```bash
|
||||
mainflux-cli users update '{"key1":"value1", "key2":"value2"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update User Password
|
||||
```bash
|
||||
mainflux-cli users password <old_password> <password> <user_auth_token>
|
||||
```
|
||||
|
||||
### System Provisioning
|
||||
#### Create Thing
|
||||
```bash
|
||||
mainflux-cli things create '{"name":"myThing"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Create Thing with metadata
|
||||
```bash
|
||||
mainflux-cli things create '{"name":"myThing", "metadata": {\"key1\":\"value1\"}}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Bulk Provision Things
|
||||
```bash
|
||||
mainflux-cli provision things <file> <user_auth_token>
|
||||
```
|
||||
|
||||
* `file` - A CSV or JSON file containing things
|
||||
* `user_auth_token` - A valid user auth token for the current system
|
||||
|
||||
#### Update Thing
|
||||
```bash
|
||||
mainflux-cli things update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Thing
|
||||
```bash
|
||||
mainflux-cli things delete <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of provisioned Things
|
||||
```bash
|
||||
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve Thing By ID
|
||||
```bash
|
||||
mainflux-cli things get <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Create Channel
|
||||
```bash
|
||||
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Bulk Provision Channels
|
||||
```bash
|
||||
mainflux-cli provision channels <file> <user_auth_token>
|
||||
```
|
||||
|
||||
* `file` - A CSV or JSON file containing channels
|
||||
* `user_auth_token` - A valid user auth token for the current system
|
||||
|
||||
#### Update Channel
|
||||
```bash
|
||||
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Channel
|
||||
```bash
|
||||
mainflux-cli channels delete <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of provisioned Channels
|
||||
```bash
|
||||
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve Channel By ID
|
||||
```bash
|
||||
mainflux-cli channels get <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
### Access control
|
||||
#### Connect Thing to Channel
|
||||
```bash
|
||||
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Bulk Connect Things to Channels
|
||||
```bash
|
||||
mainflux-cli provision connect <file> <user_auth_token>
|
||||
```
|
||||
|
||||
* `file` - A CSV or JSON file containing thing and channel ids
|
||||
* `user_auth_token` - A valid user auth token for the current system
|
||||
|
||||
An example CSV file might be
|
||||
|
||||
```csv
|
||||
<thing_id>,<channel_id>
|
||||
<thing_id>,<channel_id>
|
||||
```
|
||||
|
||||
in which the first column is thing IDs and the second column is channel IDs. A connection will be created for each thing to each channel. This example would result in 4 connections being created.
|
||||
|
||||
A comparable JSON file would be
|
||||
|
||||
```json
|
||||
{
|
||||
"thing_ids": [
|
||||
"<thing_id>",
|
||||
"<thing_id>"
|
||||
],
|
||||
"channel_ids": [
|
||||
"<channel_id>",
|
||||
"<channel_id>"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Disconnect Thing from Channel
|
||||
```bash
|
||||
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
|
||||
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of Channels connected to Thing
|
||||
```bash
|
||||
mainflux-cli things connections <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of Things connected to Channel
|
||||
```bash
|
||||
mainflux-cli channels connections <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
|
||||
### Messaging
|
||||
#### Send a message over HTTP
|
||||
```bash
|
||||
mainflux-cli messages send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"hum","v":40}, {"bn":"Dev2", "n":"temp","v":20}, {"n":"hum","v":40}]' <thing_auth_token>
|
||||
```
|
||||
|
||||
#### Read messages over HTTP
|
||||
```bash
|
||||
mainflux-cli messages read <channel_id> <thing_auth_token>
|
||||
```
|
||||
|
||||
### Bootstrap
|
||||
|
||||
#### Add configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap add '{"external_id": "myExtID", "external_key": "myExtKey", "name": "myName", "content": "myContent"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### View configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap view <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap update '{"MFThing":"<thing_id>", "name": "newName", "content": "newContent"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap remove <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Bootstrap configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap bootstrap <external_id> <external_key>
|
||||
```
|
||||
@@ -1,137 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdBootstrap = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "add",
|
||||
Short: "add <JSON_config> <user_auth_token>",
|
||||
Long: `Adds new Thing Bootstrap Config to the user identified by the provided key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg mfxsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.AddBootstrap(args[1], cfg)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "view",
|
||||
Short: "view <thing_id> <user_auth_token>",
|
||||
Long: `Returns Thing Config with given ID belonging to the user identified by the given key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := sdk.ViewBootstrap(args[1], args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <JSON_config> <user_auth_token>",
|
||||
Long: `Updates editable fields of the provided Config`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg mfxsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateBootstrap(args[1], cfg); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "remove <thing_id> <user_auth_token>",
|
||||
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.RemoveBootstrap(args[1], args[0]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "bootstrap <external_id> <external_key>",
|
||||
Long: `Returns Config to the Thing with provided external ID using external key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := sdk.Bootstrap(args[1], args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewBootstrapCmd returns bootstrap command.
|
||||
func NewBootstrapCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "Bootstrap management",
|
||||
Long: `Bootstrap management: create, get, update or delete Bootstrap config`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("bootstrap [add | view | update | remove | bootstrap]")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdBootstrap {
|
||||
cmd.AddCommand(&cmdBootstrap[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdChannels = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create <JSON_channel> <user_auth_token>",
|
||||
Long: `Creates new channel and generates it's UUID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var channel mfxsdk.Channel
|
||||
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.CreateChannel(channel, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get [all | <channel_id>] <user_auth_token>",
|
||||
Long: `Gets list of all channels or gets channel by id`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if args[0] == "all" {
|
||||
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit), Name)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := sdk.Channel(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <JSON_string> <user_auth_token>",
|
||||
Long: `Updates channel record`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var channel mfxsdk.Channel
|
||||
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateChannel(channel, args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete <channel_id> <user_auth_token>",
|
||||
Long: `Delete channel by ID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.DeleteChannel(args[0], args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "connections",
|
||||
Short: "connections <channel_id> <user_auth_token>",
|
||||
Long: `List of Things connected to Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewChannelsCmd returns channels command.
|
||||
func NewChannelsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "channels",
|
||||
Short: "Channels management",
|
||||
Long: `Channels management: create, get, update or delete Channel and get list of Things connected to Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("channels [create | get | update | delete | connections]")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdChannels {
|
||||
cmd.AddCommand(&cmdChannels[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
const contentTypeSenml = "application/senml+json"
|
||||
|
||||
var cmdMessages = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "send",
|
||||
Short: "send <channel_id>[.<subtopic>...] <JSON_string> <thing_key>",
|
||||
Long: `Sends message on the channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.SendMessage(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "read",
|
||||
Short: "read <channel_id>[.<subtopic>...] <thing_key>",
|
||||
Long: `Reads all channel messages`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := sdk.ReadMessages(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(m)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewMessagesCmd returns messages command.
|
||||
func NewMessagesCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "messages",
|
||||
Short: "Send or read messages",
|
||||
Long: `Send or read messages using the http-adapter and the configured database reader`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("messages [send | read]")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdMessages {
|
||||
cmd.AddCommand(&cmdMessages[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var errMalformedCSV = errors.New("malformed CSV")
|
||||
|
||||
const jsonExt = ".json"
|
||||
const csvExt = ".csv"
|
||||
|
||||
var cmdProvision = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "things",
|
||||
Short: "things <things_file> <user_token>",
|
||||
Long: `Bulk create things`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
things, err := thingsFromFile(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
things, err = sdk.CreateThings(things, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(things)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "channels",
|
||||
Short: "channels <channels_file> <user_token>",
|
||||
Long: `Bulk create channels`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
channels, err := channelsFromFile(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
channels, err = sdk.CreateChannels(channels, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(channels)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "connect",
|
||||
Short: "connect <connections_file> <user_token>",
|
||||
Long: `Bulk connect things to channels`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
connIDs, err := connectionsFromFile(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = sdk.Connect(connIDs, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "test",
|
||||
Short: "test",
|
||||
Long: `Provisions test setup: one test user, two things and two channels. \
|
||||
Connect both things to one of the channels, \
|
||||
and only on thing to other channel.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
numThings := 2
|
||||
numChan := 2
|
||||
things := []mfxsdk.Thing{}
|
||||
channels := []mfxsdk.Channel{}
|
||||
|
||||
if len(args) != 0 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
un := fmt.Sprintf("%s@email.com", namesgenerator.GetRandomName(0))
|
||||
// Create test user
|
||||
user := mfxsdk.User{
|
||||
Email: un,
|
||||
Password: "12345678",
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ut, err := sdk.CreateToken(user)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create things
|
||||
for i := 0; i < numThings; i++ {
|
||||
n := fmt.Sprintf("d%d", i)
|
||||
|
||||
t := mfxsdk.Thing{
|
||||
Name: n,
|
||||
}
|
||||
|
||||
things = append(things, t)
|
||||
}
|
||||
things, err = sdk.CreateThings(things, ut)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create channels
|
||||
for i := 0; i < numChan; i++ {
|
||||
n := fmt.Sprintf("c%d", i)
|
||||
|
||||
c := mfxsdk.Channel{
|
||||
Name: n,
|
||||
}
|
||||
|
||||
channels = append(channels, c)
|
||||
}
|
||||
channels, err = sdk.CreateChannels(channels, ut)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Connect things to channels - first thing to both channels, second only to first
|
||||
conIDs := mfxsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{channels[0].ID, channels[1].ID},
|
||||
ThingIDs: []string{things[0].ID},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
conIDs = mfxsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{channels[0].ID},
|
||||
ThingIDs: []string{things[1].ID},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(user, ut, things, channels)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewProvisionCmd returns provision command.
|
||||
func NewProvisionCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "provision",
|
||||
Short: "Provision things and channels from a config file",
|
||||
Long: `Provision things and channels: use json or csv file to bulk provision things and channels`,
|
||||
}
|
||||
|
||||
for i := range cmdProvision {
|
||||
cmd.AddCommand(&cmdProvision[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
|
||||
func thingsFromFile(path string) ([]mfxsdk.Thing, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return []mfxsdk.Thing{}, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return []mfxsdk.Thing{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
things := []mfxsdk.Thing{}
|
||||
switch filepath.Ext(path) {
|
||||
case csvExt:
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return []mfxsdk.Thing{}, err
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
return []mfxsdk.Thing{}, errors.New("empty line found in file")
|
||||
}
|
||||
|
||||
thing := mfxsdk.Thing{
|
||||
Name: l[0],
|
||||
}
|
||||
|
||||
things = append(things, thing)
|
||||
}
|
||||
case jsonExt:
|
||||
err := json.NewDecoder(file).Decode(&things)
|
||||
if err != nil {
|
||||
return []mfxsdk.Thing{}, err
|
||||
}
|
||||
default:
|
||||
return []mfxsdk.Thing{}, err
|
||||
}
|
||||
|
||||
return things, nil
|
||||
}
|
||||
|
||||
func channelsFromFile(path string) ([]mfxsdk.Channel, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return []mfxsdk.Channel{}, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return []mfxsdk.Channel{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
channels := []mfxsdk.Channel{}
|
||||
switch filepath.Ext(path) {
|
||||
case csvExt:
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return []mfxsdk.Channel{}, err
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
return []mfxsdk.Channel{}, errors.New("empty line found in file")
|
||||
}
|
||||
|
||||
channel := mfxsdk.Channel{
|
||||
Name: l[0],
|
||||
}
|
||||
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
case jsonExt:
|
||||
err := json.NewDecoder(file).Decode(&channels)
|
||||
if err != nil {
|
||||
return []mfxsdk.Channel{}, err
|
||||
}
|
||||
default:
|
||||
return []mfxsdk.Channel{}, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func connectionsFromFile(path string) (mfxsdk.ConnectionIDs, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return mfxsdk.ConnectionIDs{}, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return mfxsdk.ConnectionIDs{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
connections := mfxsdk.ConnectionIDs{}
|
||||
switch filepath.Ext(path) {
|
||||
case csvExt:
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return mfxsdk.ConnectionIDs{}, err
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
return mfxsdk.ConnectionIDs{}, errors.New("empty line found in file")
|
||||
}
|
||||
|
||||
connections.ThingIDs = append(connections.ThingIDs, l[0])
|
||||
connections.ChannelIDs = append(connections.ChannelIDs, l[1])
|
||||
}
|
||||
case jsonExt:
|
||||
err := json.NewDecoder(file).Decode(&connections)
|
||||
if err != nil {
|
||||
return mfxsdk.ConnectionIDs{}, err
|
||||
}
|
||||
default:
|
||||
return mfxsdk.ConnectionIDs{}, err
|
||||
}
|
||||
|
||||
return connections, nil
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
|
||||
// Keep SDK handle in global var
|
||||
var sdk mfxsdk.SDK
|
||||
|
||||
// SetSDK sets mainflux SDK instance.
|
||||
func SetSDK(s mfxsdk.SDK) {
|
||||
sdk = s
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdThings = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create <JSON_thing> <user_auth_token>",
|
||||
Long: `Create new thing, generate his UUID and store it`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var thing mfxsdk.Thing
|
||||
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.CreateThing(thing, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get [all | <thing_id>] <user_auth_token>",
|
||||
Long: `Get all things or thing by id`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if args[0] == "all" {
|
||||
l, err := sdk.Things(args[1], uint64(Offset), uint64(Limit), Name)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
|
||||
t, err := sdk.Thing(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(t)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete <thing_id> <user_auth_token>",
|
||||
Long: `Removes thing from database`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.DeleteThing(args[0], args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <JSON_string> <user_auth_token>",
|
||||
Long: `Update thing record`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var thing mfxsdk.Thing
|
||||
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateThing(thing, args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "connect",
|
||||
Short: "connect <thing_id> <channel_id> <user_auth_token>",
|
||||
Long: `Connect thing to the channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
connIDs := mfxsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{args[1]},
|
||||
ThingIDs: []string{args[0]},
|
||||
}
|
||||
if err := sdk.Connect(connIDs, args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "disconnect",
|
||||
Short: "disconnect <thing_id> <channel_id> <user_auth_token>",
|
||||
Long: `Disconnect thing to the channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.DisconnectThing(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "connections",
|
||||
Short: "connections <thing_id> <user_auth_token>",
|
||||
Long: `List of Channels connected to Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewThingsCmd returns things command.
|
||||
func NewThingsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "things",
|
||||
Short: "Things management",
|
||||
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected to Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("things [create | get | update | delete | connect | disconnect | connections]")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdThings {
|
||||
cmd.AddCommand(&cmdThings[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdUsers = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create <username> <password>",
|
||||
Long: `Creates new user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
user := mfxsdk.User{
|
||||
Email: args[0],
|
||||
Password: args[1],
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get <user_auth_token>",
|
||||
Long: `Returns user object`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := sdk.User(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(u)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "token",
|
||||
Short: "token <username> <password>",
|
||||
Long: `Creates new token`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
user := mfxsdk.User{
|
||||
Email: args[0],
|
||||
Password: args[1],
|
||||
}
|
||||
token, err := sdk.CreateToken(user)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(token)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <JSON_string> <user_auth_token>",
|
||||
Long: `Update user metadata`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var user mfxsdk.User
|
||||
if err := json.Unmarshal([]byte(args[0]), &user.Metadata); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateUser(user, args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "password",
|
||||
Short: "password <old_password> <password> <user_auth_token>",
|
||||
Long: `Update user password`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdatePassword(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewUsersCmd returns users command.
|
||||
func NewUsersCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Users management",
|
||||
Long: `Users management: create accounts and tokens"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("Usage: users [create | get | update | token | password]")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdUsers {
|
||||
cmd.AddCommand(&cmdUsers[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user