mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 436db8877d | |||
| 4da66aecbf | |||
| 3c794d4a03 | |||
| 8dd59f8557 | |||
| bc41c32cf5 | |||
| 42e4e054c2 | |||
| 9ed5f8334f | |||
| 8ea26c5ab7 | |||
| 043d1e0aac | |||
| f10e49e6b5 | |||
| f18f2c1c98 | |||
| dfa6d8ba0d | |||
| e272d9f7ad | |||
| 26c944b5c3 | |||
| 3694a82de6 | |||
| 930f8beb29 | |||
| 9334568ba2 | |||
| b91024465c | |||
| be1361311d | |||
| c1088b9315 | |||
| 2453cd75ed | |||
| 7dc6b0d879 | |||
| 381ebb1e51 | |||
| 09d09c6ef5 | |||
| a7eee53dfb | |||
| e6b7fd818b | |||
| ecff066421 | |||
| 8d33285489 | |||
| ee5c76395a | |||
| 4a9c7403d8 | |||
| 381a15a695 | |||
| a7b81dc37a | |||
| a1643b1e7e | |||
| 340e685d70 | |||
| 97f8d65885 | |||
| 7f3e2c1b21 | |||
| 8b004b3daf | |||
| a7c3cfcf1c | |||
| f1ee9d0665 | |||
| ea3a891c91 | |||
| c26c7f34cd | |||
| a5fb55c328 | |||
| 8906943d1d | |||
| a57fb03c01 | |||
| 877005538c | |||
| b4c80132e6 | |||
| d7670e7adb | |||
| df6f5adff8 | |||
| 5089fccf36 | |||
| b13a3135dd | |||
| 4d9694c027 | |||
| b8be18129c | |||
| f9432c6525 | |||
| f482df9033 | |||
| 16ac44f377 | |||
| 2b4cf8a990 | |||
| 0c016cd7ce | |||
| f872546925 | |||
| 9d5202c46b | |||
| bf189fbd23 | |||
| c3019fffb6 | |||
| 36d00d8b9e | |||
| 9b4eb7056f | |||
| cb3b70d567 | |||
| c7a8e75353 | |||
| 9d430fa02c | |||
| 7a18d3fa56 | |||
| ccbfe20607 | |||
| eb9092494e | |||
| 88fa724fc3 | |||
| 7910d64ad4 | |||
| 01aa4190c5 | |||
| f982b8ccc6 | |||
| 7d839b7b61 | |||
| 6bd74575ff | |||
| 4c970a8079 | |||
| b8bd011f9d | |||
| fbbf2c07a5 | |||
| 5b5229975b | |||
| d8dca28072 | |||
| 537d73d0c6 | |||
| 880e193b0a | |||
| e438be4250 | |||
| 57f7ee2595 | |||
| 6c39c85cc8 | |||
| 338c55e554 | |||
| 382bc89161 | |||
| 8325c1caf6 | |||
| 1d78233fe6 | |||
| fff492bd50 | |||
| 51ec256664 | |||
| c20b43ed41 | |||
| 46aadcfd9d | |||
| d2153a8846 | |||
| 70955d1937 | |||
| c1df2cb21c | |||
| b8818c4dd2 | |||
| a2d70c8907 | |||
| 6ffa916ed2 | |||
| 19503742a6 | |||
| c91fe0d453 | |||
| 3e4a9eb16a | |||
| b3f91f5c9e | |||
| ba97e865a4 | |||
| cc5e0288df | |||
| 6e4e5b351a | |||
| d587921eda | |||
| 7df2ababc4 | |||
| 63dc1f69b6 | |||
| 1c6f124062 | |||
| cc90f568b6 | |||
| 5e145858f1 | |||
| 9e2bb4933a | |||
| 3f77b87a91 | |||
| dcba41e8c4 | |||
| f6c813ea8c | |||
| 42b3682352 | |||
| 3b5d51276f | |||
| a30a3b9063 | |||
| e22d1fbbb4 | |||
| 51cd0524a1 | |||
| 1c2c86b030 | |||
| a1078e6999 | |||
| b8138fac32 | |||
| 2b7637fd75 | |||
| f785116a6f | |||
| 3f6a0cd14b | |||
| 269fe89a42 | |||
| bdf1326933 | |||
| a96db05cc0 | |||
| 7b62f1ce8a | |||
| 575d1d6bbf | |||
| 0d361f3df1 | |||
| 5714a6dfcb | |||
| decfae9c9d | |||
| c4fa27fd7e | |||
| 8475e87fe7 | |||
| 0d3adfdc5c | |||
| 160fda4d37 | |||
| 4ba1717582 | |||
| b3991b8497 | |||
| 5834d364ad | |||
| f42f2095ba | |||
| 60a90d84d5 | |||
| 887bc2d9c0 | |||
| 2080ee8afa | |||
| 9c1743c080 | |||
| 9f37927dec | |||
| bdeb7711ce | |||
| e960004b44 | |||
| 76b68e10a8 | |||
| 5120a71595 | |||
| 8c4da85039 | |||
| 237514ee33 | |||
| 263108cc85 | |||
| 277342b8ca | |||
| 87dd91e328 | |||
| 6da650645f | |||
| 8602185b74 | |||
| b8ac6ca2bf | |||
| 76908d7c8b | |||
| 0ab627730f | |||
| 1b427f4396 | |||
| 52c4d4a824 | |||
| ced90b21a4 | |||
| ac3ff5221a | |||
| 5e35cbe06b | |||
| 894e1b83ec | |||
| afeec81a5e | |||
| 67d518821a | |||
| 5446d88abf | |||
| 9e8dd4b149 | |||
| e735e5c01b | |||
| 841b943718 | |||
| 3bfdcfe83c | |||
| 66f2332674 | |||
| f50335ab33 | |||
| d7dc836714 | |||
| 887542aab3 | |||
| 1d2c8b25a1 | |||
| 1a31ecd044 | |||
| 28a176ac1d | |||
| 8be2516321 | |||
| 380af878d2 | |||
| da4fc62440 | |||
| 426813cec4 | |||
| 8f3dff837b | |||
| 5f5d7e4cfa | |||
| fec058c614 | |||
| 613e75f7ee | |||
| 7499f8deea | |||
| 02fd492105 | |||
| d309973a23 | |||
| e45e0ab307 | |||
| dea7c67ddb | |||
| 2e664ffe10 | |||
| cdf9347b01 | |||
| e16a025fba | |||
| 8ab0b08c39 | |||
| f4f938a730 | |||
| 6687a738ce | |||
| 1da48afc4c | |||
| ddbf172ed5 | |||
| df2cb80f12 | |||
| b3ce48aca6 | |||
| 351b04cd2e | |||
| 23b421bdc5 | |||
| 93d939ea52 | |||
| 66487eda42 | |||
| 23dff53a08 | |||
| 19f8922a9e | |||
| ecc15b64b6 | |||
| 92a640f6fc | |||
| 873ef4c96f | |||
| 5e3bb270a3 | |||
| c8cb2655c0 | |||
| b83439fcc2 | |||
| 08105205e1 | |||
| d20dfa84bc | |||
| a6cf76709d | |||
| 5ef6aa84de | |||
| 3b19292966 | |||
| 4870119270 | |||
| 97327ab05f | |||
| 115d94bd1b | |||
| 640dfb7e19 | |||
| e17a3de1d0 | |||
| ec4b46b669 | |||
| 38d85ae03b | |||
| 655f421ca9 | |||
| 9ccc37c4b1 | |||
| a8836d67e0 | |||
| 2d1347eee3 | |||
| 1a4844cee1 | |||
| f090573567 | |||
| bfd12304ac | |||
| e7891ec6ab | |||
| 5c924bd5a3 | |||
| 19834dfc51 | |||
| 9a78b1111f | |||
| 9c52c3816a | |||
| f03949a003 | |||
| 68e8eda92d | |||
| c751d0e656 | |||
| 55d03ecdda | |||
| 2996193a50 | |||
| 6d2aa71ccc | |||
| 54823bed25 | |||
| 649986b19f | |||
| 99ced38229 | |||
| 8efcaee350 | |||
| a3de6953f8 | |||
| 748b7c1ee8 |
@@ -1,10 +1,6 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
ui
|
||||
docker
|
||||
docs
|
||||
k8s
|
||||
load-test
|
||||
metrics
|
||||
scripts
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# Docker: Environment variables in Compose
|
||||
|
||||
## NGINX
|
||||
## NginX
|
||||
MF_NGINX_HTTP_PORT=80
|
||||
MF_NGINX_SSL_PORT=443
|
||||
MF_NGINX_MQTT_PORT=8883
|
||||
MF_NGINX_MQTT_PORT=1883
|
||||
MF_NGINX_MQTTS_PORT=8883
|
||||
|
||||
## NATS
|
||||
MF_NATS_URL=nats://nats:4222
|
||||
|
||||
## REDIS
|
||||
## Redis
|
||||
MF_REDIS_TCP_PORT=6379
|
||||
|
||||
## UI
|
||||
MF_UI_PORT=3000
|
||||
|
||||
## Grafana
|
||||
MF_GRAFANA_PORT=3000
|
||||
|
||||
@@ -25,61 +23,158 @@ MF_JAEGER_CONFIGS=5778
|
||||
MF_JAEGER_URL=jaeger:6831
|
||||
|
||||
## Core Services
|
||||
|
||||
### AuthN
|
||||
MF_AUTHN_LOG_LEVEL=debug
|
||||
MF_AUTHN_HTTP_PORT=8189
|
||||
MF_AUTHN_GRPC_PORT=8181
|
||||
MF_AUTHN_GRPC_URL=authn:8181
|
||||
MF_AUTHN_GRPC_TIMEOUT=1s
|
||||
MF_AUTHN_DB_PORT=5432
|
||||
MF_AUTHN_DB_USER=mainflux
|
||||
MF_AUTHN_DB_PASS=mainflux
|
||||
MF_AUTHN_DB=authn
|
||||
MF_AUTHN_SECRET=secret
|
||||
|
||||
### Users
|
||||
MF_USERS_LOG_LEVEL=debug
|
||||
MF_USERS_HTTP_PORT=8180
|
||||
MF_USERS_GRPC_PORT=8181
|
||||
MF_USERS_DB_PORT=5432
|
||||
MF_USERS_DB_USER=mainflux
|
||||
MF_USERS_DB_PASS=mainflux
|
||||
MF_USERS_DB=users
|
||||
MF_USERS_SECRET=secret
|
||||
MF_USERS_ADMIN_EMAIL=admin@example.com
|
||||
MF_USERS_ADMIN_PASSWORD=12345678
|
||||
|
||||
### Email utility
|
||||
MF_EMAIL_DRIVER=smtp
|
||||
MF_EMAIL_HOST=smtp.mailtrap.io
|
||||
MF_EMAIL_PORT=2525
|
||||
MF_EMAIL_USERNAME=18bf7f70705139
|
||||
MF_EMAIL_PASSWORD=2b0d302e775b1e
|
||||
MF_EMAIL_FROM_ADDRESS=from@example.com
|
||||
MF_EMAIL_FROM_NAME=Example
|
||||
MF_EMAIL_TEMPLATE=email.tmpl
|
||||
|
||||
### Token utility
|
||||
MF_TOKEN_RESET_ENDPOINT=/reset-request
|
||||
|
||||
### Things
|
||||
MF_THINGS_LOG_LEVEL=debug
|
||||
MF_THINGS_HTTP_PORT=8182
|
||||
MF_THINGS_AUTH_HTTP_PORT=8989
|
||||
MF_THINGS_AUTH_GRPC_PORT=8183
|
||||
MF_THINGS_AUTH_GRPC_URL=things:8183
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT=1s
|
||||
MF_THINGS_DB_PORT=5432
|
||||
MF_THINGS_DB_USER=mainflux
|
||||
MF_THINGS_DB_PASS=mainflux
|
||||
MF_THINGS_DB=things
|
||||
MF_THINGS_SECRET=secret
|
||||
|
||||
### Normalizer
|
||||
MF_NORMALIZER_LOG_LEVEL=debug
|
||||
MF_NORMALIZER_PORT=8184
|
||||
|
||||
### WS
|
||||
MF_WS_ADAPTER_LOG_LEVEL=debug
|
||||
MF_WS_ADAPTER_PORT=8186
|
||||
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_PORT=1883
|
||||
MF_MQTT_ADAPTER_WS_PORT=8880
|
||||
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=
|
||||
|
||||
### COAP
|
||||
### 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=8200
|
||||
MF_BOOTSTRAP_PORT=8202
|
||||
MF_BOOTSTRAP_DB_PORT=5432
|
||||
MF_BOOTSTRAP_DB_USER=mainflux
|
||||
MF_BOOTSTRAP_DB_PASS=mainflux
|
||||
MF_BOOTSTRAP_DB=bootstrap
|
||||
MF_BOOTSTRAP_DB_SSL_MODE=disable
|
||||
|
||||
### Provision
|
||||
MF_PROVISION_CONFIG_FILE=/configs/config.toml
|
||||
MF_PROVISION_LOG_LEVEL=debug
|
||||
MF_PROVISION_HTTP_PORT=8190
|
||||
MF_PROVISION_ENV_CLIENTS_TLS=false
|
||||
MF_PROVISION_SERVER_CERT=
|
||||
MF_PROVISION_SERVER_KEY=
|
||||
MF_PROVISION_MQTT_URL=tcp://localhost
|
||||
MF_PROVISION_USERS_LOCATION=http://localhost:8180
|
||||
MF_PROVISION_THINGS_LOCATION=http://things:8182
|
||||
MF_PROVISION_USER=
|
||||
MF_PROVISION_PASS=
|
||||
MF_PROVISION_API_KEY=
|
||||
MF_PROVISION_CERTS_SVC_URL=http://certs:8204
|
||||
MF_PROVISION_X509_PROVISIONING=true
|
||||
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things
|
||||
MF_PROVISION_BS_SVC_WHITELIST_URL=http://bootstrap:8202/things/state
|
||||
MF_PROVISION_BS_CONFIG_PROVISIONING=true
|
||||
MF_PROVISION_BS_AUTO_WHITELIST=true
|
||||
MF_PROVISION_BS_CONTENT=
|
||||
MF_PROVISION_CERTS_RSA_BITS=4096
|
||||
MF_PROVISION_CERTS_HOURS_VALID=2400h
|
||||
|
||||
# Certs
|
||||
MF_CERTS_LOG_LEVEL=debug
|
||||
MF_CERTS_HTTP_PORT=8204
|
||||
MF_CERTS_DB_HOST=certs-db
|
||||
MF_CERTS_DB_PORT=5432
|
||||
MF_CERTS_DB_USER=mainflux
|
||||
MF_CERTS_DB_PASS=mainflux
|
||||
MF_CERTS_DB=certs
|
||||
MF_CERTS_DB_SSL_MODE=
|
||||
MF_CERTS_DB_SSL_CERT=
|
||||
MF_CERTS_DB_SSL_KEY=
|
||||
MF_CERTS_DB_SSL_ROOT_CERT=
|
||||
MF_CERTS_ENCRYPT_KEY=
|
||||
MF_CERTS_CLIENT_TLS=
|
||||
MF_CERTS_CA_CERTS=
|
||||
MF_CERTS_SERVER_CERT=
|
||||
MF_CERTS_SERVER_KEY=
|
||||
MF_SDK_BASE_URL=http://172.17.0.1
|
||||
MF_SDK_THINGS_PREFIX=
|
||||
MF_CERTS_SIGN_CA_PATH=/etc/ssl/certs/ca.crt
|
||||
MF_CERTS_SIGN_CA_KEY_PATH=/etc/ssl/certs/ca.key
|
||||
MF_CERTS_SIGN_HOURS_VALID=2048h
|
||||
MF_CERTS_SIGN_RSA_BITS=2048
|
||||
MF_CERTS_VAULT_HOST=
|
||||
MF_CERTS_VAULT_PKI_PATH=pki_int
|
||||
MF_CERTS_VAULT_ROLE=agent
|
||||
MF_CERTS_VAULT_TOKEN=s.nArgw6xn3uIOfA7nfKk8LFaW
|
||||
|
||||
|
||||
### LoRa
|
||||
MF_LORA_ADAPTER_LOG_LEVEL=debug
|
||||
MF_LORA_ADAPTER_MESSAGES_URL=tcp://lora.mqtt.mainflux.io:1883
|
||||
MF_LORA_ADAPTER_HTTP_PORT=8187
|
||||
MF_LORA_ADAPTER_ROUTE_MAP_URL=localhost:6379
|
||||
MF_LORA_ADAPTER_ROUTE_MAP_PASS=
|
||||
MF_LORA_ADAPTER_ROUTE_MAP_DB=0
|
||||
|
||||
### OPC-UA
|
||||
MF_OPCUA_ADAPTER_HTTP_PORT=8188
|
||||
MF_OPCUA_ADAPTER_LOG_LEVEL=debug
|
||||
MF_OPCUA_ADAPTER_POLICY=
|
||||
MF_OPCUA_ADAPTER_MODE=
|
||||
MF_OPCUA_ADAPTER_CERT_FILE=
|
||||
MF_OPCUA_ADAPTER_KEY_FILE=
|
||||
MF_OPCUA_ADAPTER_ROUTE_MAP_URL=localhost:6379
|
||||
MF_OPCUA_ADAPTER_ROUTE_MAP_PASS=
|
||||
MF_OPCUA_ADAPTER_ROUTE_MAP_DB=0
|
||||
MF_OPCUA_ADAPTER_EVENT_CONSUMER=opcua
|
||||
|
||||
### Cassandra Writer
|
||||
MF_CASSANDRA_WRITER_LOG_LEVEL=debug
|
||||
@@ -87,6 +182,7 @@ 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
|
||||
@@ -94,6 +190,8 @@ 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
|
||||
@@ -101,30 +199,36 @@ 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_NAME=mainflux
|
||||
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_NAME=mainflux
|
||||
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_NAME=mainflux
|
||||
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_NAME=mainflux
|
||||
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
|
||||
@@ -132,11 +236,12 @@ 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_NAME=messages
|
||||
MF_POSTGRES_WRITER_DB=mainflux
|
||||
MF_POSTGRES_WRITER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_WRITER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_KEY=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_ROOT_CERT=""
|
||||
MF_POSTGRES_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### Postgres Reader
|
||||
MF_POSTGRES_READER_LOG_LEVEL=debug
|
||||
@@ -146,8 +251,25 @@ 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_NAME=messages
|
||||
MF_POSTGRES_READER_DB=mainflux
|
||||
MF_POSTGRES_READER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_READER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_READER_DB_SSL_KEY=""
|
||||
MF_POSTGRES_READER_DB_SSL_ROOT_CERT=""
|
||||
|
||||
# Twins
|
||||
MF_TWINS_LOG_LEVEL=debug
|
||||
MF_TWINS_HTTP_PORT=9021
|
||||
MF_TWINS_SERVER_CERT=""
|
||||
MF_TWINS_SERVER_KEY=""
|
||||
MF_TWINS_DB=mainflux-twins
|
||||
MF_TWINS_DB_HOST=twins-db
|
||||
MF_TWINS_DB_PORT=27018
|
||||
MF_TWINS_SINGLE_USER_EMAIL=""
|
||||
MF_TWINS_SINGLE_USER_TOKEN=""
|
||||
MF_TWINS_CLIENT_TLS=""
|
||||
MF_TWINS_CA_CERTS=""
|
||||
MF_TWINS_CHANNEL_ID=
|
||||
MF_TWINS_CACHE_URL=es-redis:6379
|
||||
MF_TWINS_CACHE_PASS=
|
||||
MF_TWINS_CACHE_DB=0
|
||||
|
||||
+6
-1
@@ -1,3 +1,8 @@
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Set your private global .gitignore:
|
||||
# https://digitalfortress.tech/tricks/creating-a-global-gitignore/
|
||||
|
||||
build
|
||||
|
||||
site/
|
||||
|
||||
+209
@@ -11,6 +11,215 @@ 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)
|
||||
|
||||
Generated
-850
@@ -1,850 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6da51e5ec493ad2b44cb04129e2d0a068c8fb9bd6cb5739d199573558696bb94"
|
||||
name = "github.com/Azure/go-ansiterm"
|
||||
packages = [
|
||||
".",
|
||||
"winterm",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
|
||||
version = "v0.3.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bf42be3cb1519bf8018dfd99720b1005ee028d947124cab3ccf965da59381df6"
|
||||
name = "github.com/Microsoft/go-winio"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f"
|
||||
version = "v0.4.7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3721a10686511b80c052323423f0de17a8c06d417dbdd3b392b1578432a33aae"
|
||||
name = "github.com/Nvveen/Gotty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
pruneopts = "UT"
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2209584c0f7c9b68c23374e659357ab546e1b70eec2761f03280f69a8fd23d77"
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bb300c7f4b52deb1fcc65f6be624ae3dc289453824c2f1f184963dc41d941a68"
|
||||
name = "github.com/cisco/senml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "910a55054e168c1122b905e3f8acdc5ff97cbec1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fc8dbcc2a5de7c093e167828ebbdf551641761d2ad75431d3a167d467a264115"
|
||||
name = "github.com/containerd/continuity"
|
||||
packages = ["pathdriver"]
|
||||
pruneopts = "UT"
|
||||
revision = "c6cef34830231743494fe2969284df7b82cc0ad0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = "UT"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a5036bc703c3994f70ced9b34e9941381aa6bd03e69a0ce538f43bf14dc8dc98"
|
||||
name = "github.com/docker/docker"
|
||||
packages = [
|
||||
"pkg/namesgenerator",
|
||||
"pkg/random",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "092cba3727bb9b4a2f0e922cd6c0f93ea270e363"
|
||||
version = "v1.13.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:87dcb59127512b84097086504c16595cf8fef35b9e0bfca565dfc06e198158d7"
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
pruneopts = "UT"
|
||||
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6f82cacd0af5921e99bf3f46748705239b36489464f4529a1589bc895764fb18"
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
|
||||
version = "v0.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f77ee3cd73cbf60434b4f72ec46a66773c555d46764399735b5f9a28505f3ece"
|
||||
name = "github.com/dustin/go-coap"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:184008c955d6a3226b700c13ed0e875a7a051a759618f9bccba9b5c99f17faa5"
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
packages = [
|
||||
".",
|
||||
"packets",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "36d01c2b4cbeb3d2a12063e4880ce30800af9560"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:80abda6d164916248f3297f714a17b1a33e54bb5694ff283409b4d0320357b3a"
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
"endpoint",
|
||||
"log",
|
||||
"metrics",
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"tracing/opentracing",
|
||||
"transport/grpc",
|
||||
"transport/http",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "ca4112baa34cb55091301bdc13b1420a122b1b9e"
|
||||
version = "v0.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:31a18dae27a29aa074515e43a443abfd2ba6deb6d69309d8d7ce789c45f34659"
|
||||
name = "github.com/go-logfmt/logfmt"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:424f6593024cdf0f6f90cba81bc69ca98df3758525e6fb248198ef15ead603a9"
|
||||
name = "github.com/go-redis/redis"
|
||||
packages = [
|
||||
".",
|
||||
"internal",
|
||||
"internal/consistenthash",
|
||||
"internal/hashtag",
|
||||
"internal/pool",
|
||||
"internal/proto",
|
||||
"internal/singleflight",
|
||||
"internal/util",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7f89fbac80bcc62ce920b6dbc6ca60238d7725d1"
|
||||
version = "v6.15.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c4a2528ccbcabf90f9f3c464a5fc9e302d592861bbfd0b7135a7de8a943d0406"
|
||||
name = "github.com/go-stack/stack"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8dfa0b1b7c4b7398d46340aef0f2a58aad5a75959eeeb79e023ab826f0c7f7f0"
|
||||
name = "github.com/go-zoo/bone"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
|
||||
version = "1.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:348bbd07b4a983b2f811f81948c78142de09bb9fcf25284bee5e2ab3bdd0f6f9"
|
||||
name = "github.com/gocql/gocql"
|
||||
packages = [
|
||||
".",
|
||||
"internal/lru",
|
||||
"internal/murmur",
|
||||
"internal/streams",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "68ae1e384be4d7cd7df0f5b77f331759d806308e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bed9d72d596f94e65fff37f4d6c01398074a6bb1c3f3ceff963516bd01db6ff5"
|
||||
name = "github.com/gofrs/uuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "6b08a5c5172ba18946672b49749cde22873dd7c2"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bbadccf3d3317ea03c0dac0b45b673b4b397c8f91a1d2eff550a3c51c4ad770e"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
pruneopts = "UT"
|
||||
revision = "636bf0302bc95575d69441b25a2603156ffdddf1"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5ac24c0ad5dc117fb2b6d90d205c876ec599c09cccb702a36586680e09cb20b5"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/empty",
|
||||
"ptypes/timestamp",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009"
|
||||
name = "github.com/golang/snappy"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:364b908b9b27b97ab838f2f6f1b1f46281fa29b978a037d72a9b1d4f6d940190"
|
||||
name = "github.com/hailocab/go-hostpool"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6727359c2ab1efe714cc87acb36be6cf15a3aae078d8ae736a17fec787dab307"
|
||||
name = "github.com/hokaccha/go-prettyjson"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "f579f869bbfea48a61fbea09207cadd1aaeceb83"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:65b0a07f85f7b5cc019c26775efc278a155a5dea0a8aa617c980e8308d16bc55"
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = [
|
||||
"client/v2",
|
||||
"models",
|
||||
"pkg/escape",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "a2ba6e7654fb078f8a3f5add1f8d935df38161bd"
|
||||
version = "v1.6.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:965881afde38cce93606988d7ef9cd187b394a8a73b11a7dde379206f5e2fba3"
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
packages = [
|
||||
".",
|
||||
"reflectx",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "cdf62fdf55f66fb2484fe214f89c059f0bd3f567"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a64e323dc06b73892e5bb5d040ced475c4645d456038333883f58934abbf6f72"
|
||||
name = "github.com/kr/logfmt"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1e537dfb7b4d543e25efa2c812084dd65736f8010979c1b7e167a5b876563199"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
pruneopts = "UT"
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4ce94f3d2074662c04677db471456feb56078244ebcfe3789e6e95901c085bc5"
|
||||
name = "github.com/nats-io/go-nats"
|
||||
packages = [
|
||||
".",
|
||||
"encoders/builtin",
|
||||
"util",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "fb0396ee0bdb8018b0fef30d6d1de798ce99cd05"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c3cd663f2f30b92536b9f290ac85c6310dae36a14cb8961553ae9ccf0d85ae41"
|
||||
name = "github.com/nats-io/nuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ee4d4af67d93cc7644157882329023ce9a7bcfce956a079069a9405521c7cc8d"
|
||||
name = "github.com/opencontainers/go-digest"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
|
||||
version = "v1.0.0-rc1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11db38d694c130c800d0aefb502fb02519e514dc53d9804ce51d1ad25ec27db6"
|
||||
name = "github.com/opencontainers/image-spec"
|
||||
packages = [
|
||||
"specs-go",
|
||||
"specs-go/v1",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d60099175f88c47cd379c4738d158884749ed235"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1869683e323ebff2bdf8adcb560f82bf6f8d94019d35099e3403f7df12e9c07e"
|
||||
name = "github.com/opencontainers/runc"
|
||||
packages = [
|
||||
"libcontainer/system",
|
||||
"libcontainer/user",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c2024d8533dc702a6ef5c92d5cf1494858eff4484d8abbf794f21acd29e7ef70"
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
".",
|
||||
"ext",
|
||||
"log",
|
||||
"mocktracer",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "659c90643e714681897ec2521c60567dd21da733"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3b830e06971be8448e7c9e57c9cc15ce53ab7e8846e7b7c39110a177c7fae466"
|
||||
name = "github.com/ory/dockertest"
|
||||
packages = [
|
||||
"docker",
|
||||
"docker/opts",
|
||||
"docker/pkg/archive",
|
||||
"docker/pkg/fileutils",
|
||||
"docker/pkg/homedir",
|
||||
"docker/pkg/idtools",
|
||||
"docker/pkg/ioutils",
|
||||
"docker/pkg/jsonmessage",
|
||||
"docker/pkg/longpath",
|
||||
"docker/pkg/mount",
|
||||
"docker/pkg/pools",
|
||||
"docker/pkg/stdcopy",
|
||||
"docker/pkg/system",
|
||||
"docker/pkg/term",
|
||||
"docker/pkg/term/windows",
|
||||
"docker/types",
|
||||
"docker/types/blkiodev",
|
||||
"docker/types/container",
|
||||
"docker/types/filters",
|
||||
"docker/types/mount",
|
||||
"docker/types/network",
|
||||
"docker/types/registry",
|
||||
"docker/types/strslice",
|
||||
"docker/types/versions",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "UT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:26663fafdea73a38075b07e8e9d82fc0056379d2be8bb4e13899e8fda7c7dd23"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/internal",
|
||||
"prometheus/promhttp",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "abad2d1bd44235a26707c172eab6bca5bf2dbad3"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
pruneopts = "UT"
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e469cd65badf7694aeb44874518606d93c1d59e7735d3754ad442782437d3cc3"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:570784e0ddbf67f14087c6d8cfb7b999b9c55e75518a755e88cb202566ea8a17"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:89cf776c0a621a34017c819b66b9e0f0bb076d3c5b9ca74763fb89570e140038"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
packages = [
|
||||
".",
|
||||
"sqlparse",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "ba2c6a7295c59448dbc195cef2f41df5163b3892"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d3ffbc45dfe6929a4ba313464c2d761d96325d306eda29512da56c034db2a4dd"
|
||||
name = "github.com/sony/gobreaker"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b2a34562d02c4d5fd6645399c69e00141fb24e5a"
|
||||
version = "0.4.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9424f440bba8f7508b69414634aef3b2b3a877e522d8a4624692412805407bb7"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c40d65817cdd41fac9aa7af8bed56927bb2d6d47e4fea566a74880f5c2b1c41e"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"require",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3b0ac1ac6f703e6d43f4e29c97da2c9e34465c66e2e19f9828e9841d1c9279a4"
|
||||
name = "github.com/uber/jaeger-client-go"
|
||||
packages = [
|
||||
".",
|
||||
"config",
|
||||
"internal/baggage",
|
||||
"internal/baggage/remote",
|
||||
"internal/spanlog",
|
||||
"internal/throttler",
|
||||
"internal/throttler/remote",
|
||||
"log",
|
||||
"rpcmetrics",
|
||||
"thrift",
|
||||
"thrift-gen/agent",
|
||||
"thrift-gen/baggage",
|
||||
"thrift-gen/jaeger",
|
||||
"thrift-gen/sampling",
|
||||
"thrift-gen/zipkincore",
|
||||
"transport",
|
||||
"utils",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2f47546e3facd43297739439600bcf43f44cce5d"
|
||||
version = "v2.16.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c9d69a04f7fa171f50360bbcc32196b4de8ab8837ef772f6302d0140a1e3e7f6"
|
||||
name = "github.com/uber/jaeger-lib"
|
||||
packages = ["metrics"]
|
||||
pruneopts = "UT"
|
||||
revision = "0e30338a695636fe5bcf7301e8030ce8dd2a8530"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:03aa6e485e528acb119fb32901cf99582c380225fc7d5a02758e08b180cb56c3"
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
pruneopts = "UT"
|
||||
revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:40fdfd6ab85ca32b6935853bbba35935dcb1d796c8135efd85947566c76e662e"
|
||||
name = "github.com/xdg/scram"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "7eeb5667e42c09cb51bf7b7c28aea8c56767da90"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f5c1d04bc09c644c592b45b9f0bad4030521b1a7d11c7dadbb272d9439fa6e8e"
|
||||
name = "github.com/xdg/stringprep"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "73f8eece6fdcd902c185bf651de50f3828bed5ed"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:21f9cb6f1337c4776ed1d2d7b7ed6ffba3269ca12274c065364a5c0edee1f28b"
|
||||
name = "go.mongodb.org/mongo-driver"
|
||||
packages = [
|
||||
"bson",
|
||||
"bson/bsoncodec",
|
||||
"bson/bsonrw",
|
||||
"bson/bsontype",
|
||||
"bson/primitive",
|
||||
"event",
|
||||
"internal",
|
||||
"mongo",
|
||||
"mongo/options",
|
||||
"mongo/readconcern",
|
||||
"mongo/readpref",
|
||||
"mongo/writeconcern",
|
||||
"tag",
|
||||
"version",
|
||||
"x/bsonx",
|
||||
"x/bsonx/bsoncore",
|
||||
"x/mongo/driver",
|
||||
"x/mongo/driver/auth",
|
||||
"x/mongo/driver/auth/internal/gssapi",
|
||||
"x/mongo/driver/session",
|
||||
"x/mongo/driver/topology",
|
||||
"x/mongo/driver/uuid",
|
||||
"x/network/address",
|
||||
"x/network/command",
|
||||
"x/network/compressor",
|
||||
"x/network/connection",
|
||||
"x/network/connstring",
|
||||
"x/network/description",
|
||||
"x/network/result",
|
||||
"x/network/wiremessage",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "1c3b9b9a41eecdf056560e07b68e6407b8d598c3"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:7310f5459b88177d24e26c5ec7deb100b78ec03c0437d7a7aaec3c7eac333a55"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"pbkdf2",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:70e155783b8dfd4f7697243384e2f361930ff0751459580769ce44d511f38b05"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/socks",
|
||||
"internal/timeseries",
|
||||
"proxy",
|
||||
"trace",
|
||||
"websocket",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e0140c0c868c6e0f01c0380865194592c011fe521d6e12d78bfd33e756fe018a"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["semaphore"]
|
||||
pruneopts = "UT"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:66e34aa65b83061616603ba83517a4523c7261e18afd01c769dd215614bf7d70"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7dfd1290c7917b7ba22824b9d24954ab3002fe24"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cd018653a358d4b743a9d3bee89e825521f2ab2f2ec0770164bf7632d8d73ab7"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
pruneopts = "UT"
|
||||
revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c3ad9841823db6da420a5625b367913b4ff54bbe60e8e3c98bd20e243e62e2d2"
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"internal/backoff",
|
||||
"internal/channelz",
|
||||
"internal/envconfig",
|
||||
"internal/grpcrand",
|
||||
"internal/transport",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2e463a05d100327ca47ac218281906921038fd95"
|
||||
version = "v1.16.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fa9a7c0ef59217bd22f32eb7cc027894d73f340a5633258cc079dec025db6a7f"
|
||||
name = "gopkg.in/gorp.v1"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
|
||||
name = "gopkg.in/inf.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:de4bc452b049bde272d99e944aabcf218dc22df0a478a42a7107932f82430ae2"
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "9f1141b78507d23603f6be530587664ebb63a8d2"
|
||||
version = "v3.3.2"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/BurntSushi/toml",
|
||||
"github.com/cisco/senml",
|
||||
"github.com/dgrijalva/jwt-go",
|
||||
"github.com/docker/docker/pkg/namesgenerator",
|
||||
"github.com/dustin/go-coap",
|
||||
"github.com/eclipse/paho.mqtt.golang",
|
||||
"github.com/fatih/color",
|
||||
"github.com/go-kit/kit/endpoint",
|
||||
"github.com/go-kit/kit/log",
|
||||
"github.com/go-kit/kit/metrics",
|
||||
"github.com/go-kit/kit/metrics/prometheus",
|
||||
"github.com/go-kit/kit/tracing/opentracing",
|
||||
"github.com/go-kit/kit/transport/grpc",
|
||||
"github.com/go-kit/kit/transport/http",
|
||||
"github.com/go-redis/redis",
|
||||
"github.com/go-zoo/bone",
|
||||
"github.com/gocql/gocql",
|
||||
"github.com/gofrs/uuid",
|
||||
"github.com/gogo/protobuf/proto",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/golang/protobuf/ptypes/empty",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/hokaccha/go-prettyjson",
|
||||
"github.com/influxdata/influxdb/client/v2",
|
||||
"github.com/jmoiron/sqlx",
|
||||
"github.com/lib/pq",
|
||||
"github.com/nats-io/go-nats",
|
||||
"github.com/opentracing/opentracing-go",
|
||||
"github.com/opentracing/opentracing-go/mocktracer",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rubenv/sql-migrate",
|
||||
"github.com/sony/gobreaker",
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"github.com/uber/jaeger-client-go/config",
|
||||
"go.mongodb.org/mongo-driver/bson",
|
||||
"go.mongodb.org/mongo-driver/mongo",
|
||||
"go.mongodb.org/mongo-driver/mongo/options",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/net/context",
|
||||
"google.golang.org/grpc",
|
||||
"google.golang.org/grpc/codes",
|
||||
"google.golang.org/grpc/credentials",
|
||||
"google.golang.org/grpc/status",
|
||||
"gopkg.in/ory-am/dockertest.v3",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
[[constraint]]
|
||||
name = "github.com/cisco/senml"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
version = "1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/color"
|
||||
version = "1.7.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
version = "0.7.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-zoo/bone"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/hokaccha/go-prettyjson"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/lib/pq"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
version = "1.6.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.9.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
version = "0.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
version = "0.0.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.3.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
version = "1.6.4"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gocql/gocql"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dustin/go-coap"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1.16.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-redis/redis"
|
||||
version = "v6.15.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "go.mongodb.org/mongo-driver"
|
||||
version = "~1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
version = "~v0.3.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gofrs/uuid"
|
||||
version = "~v3.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
version = "~v1.1.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2019 Mainflux
|
||||
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.
|
||||
|
||||
@@ -1,72 +1,67 @@
|
||||
## Copyright (c) 2015-2019
|
||||
## Mainflux
|
||||
##
|
||||
## SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http normalizer ws coap lora influxdb-writer influxdb-reader mongodb-writer mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli bootstrap
|
||||
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
|
||||
bootstrap opcua authn twins mqtt provision certs
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
|
||||
define compile_service
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -mod=vendor -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
$(eval svc=$(subst docker_,,$(1)))
|
||||
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(subst docker_,,$(1)) \
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--tag=mainflux/$(subst docker_,,$(1))-$(2) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
define make_docker_dev
|
||||
$(eval svc=$(subst docker_dev_,,$(1)))
|
||||
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(subst docker_dev_,,$(1)) \
|
||||
--tag=mainflux/$(subst docker_dev_,,$(1)) \
|
||||
--build-arg SVC=$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
all: $(SERVICES) mqtt
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt ui latest_manifest
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
rm -rf mqtt/node_modules
|
||||
|
||||
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
|
||||
# Stops containers and removes containers, networks, volumes, and images created by up
|
||||
docker-compose -f docker/docker-compose.yml down --rmi all -v --remove-orphans
|
||||
|
||||
ifdef pv
|
||||
# Remove unused volumes
|
||||
docker volume ls -f name=mainflux -f dangling=true -q | xargs -r docker volume rm
|
||||
docker volume ls -f name=$(MF_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
endif
|
||||
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
test:
|
||||
go test -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
|
||||
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:. pkg/messaging/*.proto
|
||||
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
@@ -77,69 +72,29 @@ $(DOCKERS):
|
||||
$(DOCKERS_DEV):
|
||||
$(call make_docker_dev,$(@))
|
||||
|
||||
docker_ui:
|
||||
$(MAKE) -C ui docker
|
||||
|
||||
docker_mqtt:
|
||||
# MQTT Docker build must be done from root dir because it copies .proto files
|
||||
ifeq ($(GOARCH), arm)
|
||||
docker build --tag=mainflux/mqtt-arm -f mqtt/Dockerfile.arm .
|
||||
else
|
||||
docker build --tag=mainflux/mqtt-amd64 -f mqtt/Dockerfile .
|
||||
endif
|
||||
|
||||
dockers: $(DOCKERS) docker_ui docker_mqtt
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
ui:
|
||||
$(MAKE) -C ui
|
||||
|
||||
mqtt:
|
||||
cd mqtt && npm install
|
||||
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc-$(1):$(2); \
|
||||
docker push $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
done
|
||||
docker push mainflux/ui-$(1):$(2)
|
||||
docker push mainflux/mqtt-$(1):$(2)
|
||||
endef
|
||||
|
||||
changelog:
|
||||
git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s"
|
||||
|
||||
define docker_manifest
|
||||
for svc in $(SERVICES); do \
|
||||
docker manifest create mainflux/$$svc:$(1) mainflux/$$svc-amd64:$(1) mainflux/$$svc-arm:$(1); \
|
||||
docker manifest annotate mainflux/$$svc:$(1) mainflux/$$svc-arm:$(1) --arch arm --variant v7; \
|
||||
docker manifest push mainflux/$$svc:$(1); \
|
||||
done
|
||||
docker manifest create mainflux/ui:$(1) mainflux/ui-amd64:$(1) mainflux/ui-arm:$(1)
|
||||
docker manifest annotate mainflux/ui:$(1) mainflux/ui-arm:$(1) --arch arm --variant v7
|
||||
docker manifest push mainflux/ui:$(1)
|
||||
docker manifest create mainflux/mqtt:$(1) mainflux/mqtt-amd64:$(1) mainflux/mqtt-arm:$(1)
|
||||
docker manifest annotate mainflux/mqtt:$(1) mainflux/mqtt-arm:$(1) --arch arm --variant v7
|
||||
docker manifest push mainflux/mqtt:$(1)
|
||||
endef
|
||||
|
||||
latest: dockers
|
||||
$(call docker_push,$(GOARCH),latest)
|
||||
|
||||
latest_manifest:
|
||||
$(call docker_manifest,latest)
|
||||
$(call docker_push,latest)
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
GOARCH=$(GOARCH) GOARM=$(GOARM) $(MAKE) dockers
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc-$(GOARCH) mainflux/$$svc-$(GOARCH):$(version); \
|
||||
docker tag $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/ui mainflux/ui-$(GOARCH):$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt-$(GOARCH):$(version)
|
||||
$(call docker_push,$(GOARCH),$(version))
|
||||
$(call docker_manifest,$(version))
|
||||
$(call docker_push,$(version))
|
||||
|
||||
rundev:
|
||||
cd scripts && ./run.sh
|
||||
@@ -147,10 +102,12 @@ rundev:
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
runui:
|
||||
$(MAKE) -C ui run
|
||||
|
||||
runlora:
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up -d
|
||||
docker-compose -f docker/addons/lora-adapter/docker-compose.yml up
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
![banner][banner]
|
||||
|
||||
Mainflux is modern, scalable, secure open source and patent-free IoT cloud platform written in Go.
|
||||
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
|
||||
@@ -17,15 +17,17 @@ 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 company][company],
|
||||
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 protocol bridging (HTTP, MQTT, WebSocket and CoAP)
|
||||
- Device management and provisioning
|
||||
|
||||
- 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
|
||||
- Storage support (Cassandra, InfluxDB and MongoDB)
|
||||
- Platform logging and instrumentation support
|
||||
- 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
|
||||
@@ -34,78 +36,99 @@ which maintains the project and offers professional services around it.
|
||||
- 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
|
||||
Before proceeding, install the following prerequisites:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
Once everything is installed, execute the following commands from project root:
|
||||
Once the prerequisites are installed, execute the following commands from the project's root:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
```
|
||||
|
||||
This will bring up all Mainflux dockers and inter-connect them in the composition.
|
||||
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
|
||||
Best way to quickstart using Mainflux is via CLI:
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
> Mainflux CLI can also be downloaded as a tarball from [offical release page][rel]
|
||||
|
||||
If this works, head to [official documentation][docs] to understand Mainflux provisioning and messaging.
|
||||
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 from Markdown files in `./docs` directory.
|
||||
If you spot an error or need for corrections, please let us know - or even better: send us a PR.
|
||||
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 continiusly work on the project evolutions.
|
||||
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] and a large set of contributors.
|
||||
[@gsaleh][george], [@blokovi][iva], [@chombium][kole], [@mteodor][mirko] and a large set of contributors.
|
||||
|
||||
Maintainers are listed in [MAINTAINERS](MAINTAINERS) file.
|
||||
|
||||
Mainflux team would like to give special thanks to [@mijicd][dejan] for his monumental work
|
||||
on designing and implementing highly improved and optimized version of the platform,
|
||||
and [@malidukica][dusanm] for his effort on implementing initial user interface.
|
||||
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 wish to contribute!
|
||||
|
||||
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues).
|
||||
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>.
|
||||
|
||||
Note that the best way to grab our attention is by sending PRs :sunglasses:.
|
||||
>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)
|
||||
|
||||
[banner]: https://github.com/mainflux/mainflux/blob/master/docs/img/gopherBanner.jpg
|
||||
[](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
|
||||
@@ -140,3 +163,4 @@ Note that the best way to grab our attention is by sending PRs :sunglasses:.
|
||||
[iva]: https://github.com/blokovi
|
||||
[kole]: https://github.com/chombium
|
||||
[dusanm]: https://github.com/malidukica
|
||||
[mirko]: https://github.com/mteodor
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mainflux
|
||||
|
||||
|
||||
+1666
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package mainflux;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service ThingsService {
|
||||
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
|
||||
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
|
||||
rpc Identify(Token) returns (ThingID) {}
|
||||
}
|
||||
|
||||
service AuthNService {
|
||||
rpc Issue(IssueReq) returns (Token) {}
|
||||
rpc Identify(Token) returns (UserID) {}
|
||||
}
|
||||
|
||||
message AccessByKeyReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ThingID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message AccessByIDReq {
|
||||
string thingID = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
// If a token is not carrying any information itself, the type
|
||||
// field can be used to determine how to validate the token.
|
||||
// Also, different tokens can be encoded in different ways.
|
||||
message Token {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message UserID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message IssueReq {
|
||||
string issuer = 1;
|
||||
uint32 type = 2;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# Authentication service
|
||||
|
||||
Authentication service provides an API for managing authentication keys.
|
||||
|
||||
There are *three types of authentication keys*:
|
||||
|
||||
- user key - keys issued to the user upon login request
|
||||
- API key - keys issued upon the user request
|
||||
- recovery key - password recovery key
|
||||
|
||||
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user. API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. Recovery key is the password recovery key. It's short-lived token used for password recovery process.
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
The following actions are supported:
|
||||
|
||||
- create (all key types)
|
||||
- verify (all key types)
|
||||
- obtain (API keys only; secret is never obtained)
|
||||
- revoke (API keys only)
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------|
|
||||
| MF_AUTHN_LOG_LEVEL | Service level (debug, info, warn, error) | error |
|
||||
| MF_AUTHN_DB_HOST | Database host address | localhost |
|
||||
| MF_AUTHN_DB_PORT | Database host port | 5432 |
|
||||
| MF_AUTHN_DB_USER | Database user | mainflux |
|
||||
| MF_AUTHN_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_AUTHN_DB | Name of the database used by the service | auth |
|
||||
| MF_AUTHN_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_AUTHN_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_AUTHN_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_AUTHN_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_AUTHN_HTTP_PORT | Authn service HTTP port | 8180 |
|
||||
| MF_AUTHN_GRPC_PORT | Authn service gRPC port | 8181 |
|
||||
| MF_AUTHN_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTHN_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTHN_SECRET | String used for signing tokens | auth |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
authn:
|
||||
image: mainflux/authn:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_AUTHN_LOG_LEVEL: [Service log level]
|
||||
MF_AUTHN_DB_HOST: [Database host address]
|
||||
MF_AUTHN_DB_PORT: [Database host port]
|
||||
MF_AUTHN_DB_USER: [Database user]
|
||||
MF_AUTHN_DB_PASS: [Database password]
|
||||
MF_AUTHN_DB: [Name of the database used by the service]
|
||||
MF_AUTHN_DB_SSL_MODE: [SSL mode to connect to the database with]
|
||||
MF_AUTHN_DB_SSL_CERT: [Path to the PEM encoded certificate file]
|
||||
MF_AUTHN_DB_SSL_KEY: [Path to the PEM encoded key file]
|
||||
MF_AUTHN_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
|
||||
MF_AUTHN_HTTP_PORT: [Service HTTP port]
|
||||
MF_AUTHN_GRPC_PORT: [Service gRPC port]
|
||||
MF_AUTHN_SECRET: [String used for signing tokens]
|
||||
MF_AUTHN_SERVER_CERT: [String path to server certificate in pem format]
|
||||
MF_AUTHN_SERVER_KEY: [String path to server key in pem format]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the service
|
||||
make authn
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_AUTHN_LOG_LEVEL=[Service log level] MF_AUTHN_DB_HOST=[Database host address] MF_AUTHN_DB_PORT=[Database host port] MF_AUTHN_DB_USER=[Database user] MF_AUTHN_DB_PASS=[Database password] MF_AUTHN_DB=[Name of the database used by the service] MF_AUTHN_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTHN_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTHN_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTHN_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTHN_HTTP_PORT=[Service HTTP port] MF_AUTHN_GRPC_PORT=[Service gRPC port] MF_AUTHN_SECRET=[String used for signing tokens] MF_AUTHN_SERVER_CERT=[Path to server certificate] MF_AUTHN_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-authn
|
||||
```
|
||||
|
||||
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of AuthN service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthNServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthNServiceClient {
|
||||
return &grpcClient{
|
||||
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.AuthNService",
|
||||
"Issue",
|
||||
encodeIssueRequest,
|
||||
decodeIssueResponse,
|
||||
mainflux.UserID{},
|
||||
).Endpoint()),
|
||||
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.AuthNService",
|
||||
"Identify",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
mainflux.UserID{},
|
||||
).Endpoint()),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.issue(ctx, issueReq{issuer: req.GetIssuer(), keyType: req.Type})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.Token{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &mainflux.IssueReq{Issuer: req.issuer, Type: req.keyType}, nil
|
||||
}
|
||||
|
||||
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserID)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserID, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.UserID{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(identityReq)
|
||||
return &mainflux.Token{Value: req.token}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserID)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of AuthN service gRPC API.
|
||||
package grpc
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
key := authn.Key{
|
||||
Type: req.keyType,
|
||||
IssuedAt: now,
|
||||
}
|
||||
|
||||
k, err := svc.Issue(ctx, req.issuer, key)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
return identityRes{k.Secret, nil}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identifyEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
return identityRes{id, nil}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
)
|
||||
|
||||
var svc authn.Service
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc authn.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterAuthNServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
kind uint32
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
id: email,
|
||||
kind: authn.UserKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
id: email,
|
||||
kind: authn.RecoveryKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
id: userKey.Secret,
|
||||
kind: authn.APIKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
id: email,
|
||||
kind: 32,
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
kind: authn.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Issuer: tc.id, Type: tc.kind})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
|
||||
|
||||
apiKey, err := svc.Issue(context.Background(), userKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "identify user with recovery token",
|
||||
token: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with API token",
|
||||
token: apiKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
id: "",
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user that doesn't exist",
|
||||
token: "",
|
||||
id: "",
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.id, id.GetValue()))
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import "github.com/mainflux/mainflux/authn"
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
kind uint32
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
if req.kind != authn.UserKey &&
|
||||
req.kind != authn.APIKey &&
|
||||
req.kind != authn.RecoveryKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type issueReq struct {
|
||||
issuer string
|
||||
keyType uint32
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.issuer == "" {
|
||||
return authn.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.keyType != authn.UserKey &&
|
||||
req.keyType != authn.APIKey &&
|
||||
req.keyType != authn.RecoveryKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
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/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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/mainflux/mainflux/pkg/uuid"
|
||||
"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()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, uuidProvider, 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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"`
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// 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/pkg/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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/pkg/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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// 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/pkg/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
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
|
||||
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
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyRequest"
|
||||
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: "#/components/responses/ServiceError"
|
||||
/keys/{id}:
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
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
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ID:
|
||||
name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
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.
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains Key repository implementations using
|
||||
// PostgreSQL as the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/pkg/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
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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/pkg/errors"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"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)
|
||||
id, _ := uuidProvider.New().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)
|
||||
id, _ := uuidProvider.New().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)
|
||||
id, _ := uuidProvider.New().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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/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
|
||||
uuidProvider mainflux.UUIDProvider
|
||||
tokenizer Tokenizer
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, up mainflux.UUIDProvider, tokenizer Tokenizer) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
uuidProvider: up,
|
||||
}
|
||||
}
|
||||
|
||||
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.uuidProvider.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
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// 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/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
)
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, uuidProvider, 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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)
|
||||
}
|
||||
+58
-35
@@ -35,35 +35,36 @@ Thing configuration also contains the so-called `external ID` and `external key`
|
||||
|
||||
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_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_USERS_URL | Users service URL | localhost:8181 |
|
||||
| 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_INSTANCE_NAME | Bootstrap service instance name | bootstrap |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_BOOTSTRAP_THINGS_TIMEOUT | Things gRPC request timeout in seconds | 1 |
|
||||
| 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 | 1s |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -92,6 +93,7 @@ version: "2"
|
||||
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
|
||||
@@ -99,25 +101,25 @@ version: "2"
|
||||
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_USERS_URL: [Users service URL]
|
||||
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_INSTANCE_NAME: [Bootstrap service instance name]
|
||||
MF_BOOTSTRAP_EVENT_CONSUMER: [Bootstrap service event source consumer name]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
MF_BOOTSTRAP_THINGS_TIMEOUT: [Things gRPC request timeout in seconds]
|
||||
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
|
||||
go get github.com/mainflux/mainflux
|
||||
git clone https://github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
cd mainflux
|
||||
|
||||
# compile the service
|
||||
make bootstrap
|
||||
@@ -126,7 +128,28 @@ make bootstrap
|
||||
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_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_USERS_URL=[Users service URL] MF_JAEGER_URL=[Jaeger server URL] MF_BOOTSTRAP_THINGS_TIMEOUT=[Things gRPC request timeout in seconds] $GOBIN/mainflux-bootstrap
|
||||
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.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package api contains implementation of bootstrap service HTTP API.
|
||||
package api
|
||||
|
||||
+33
-49
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
@@ -38,7 +34,7 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(req.key, config)
|
||||
saved, err := svc.Add(req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,7 +55,7 @@ func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.UpdateCert(req.key, req.thingKey, req.ClientCert, req.ClientKey, req.CACert); err != nil {
|
||||
if err := svc.UpdateCert(req.key, req.thingID, req.ClientCert, req.ClientKey, req.CACert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -166,49 +162,37 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,19 +212,19 @@ func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader) endpoint.Endpoint {
|
||||
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)
|
||||
cfg, err := svc.Bootstrap(req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader.ReadConfig(cfg)
|
||||
return reader.ReadConfig(cfg, secure)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+142
-161
@@ -1,13 +1,13 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
bsapi "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
@@ -45,6 +45,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
encKey = []byte("1234567891011121")
|
||||
addChannels = []string{"1"}
|
||||
metadata = map[string]interface{}{"meta": "data"}
|
||||
addReq = struct {
|
||||
@@ -77,6 +78,12 @@ var (
|
||||
ClientKey: "newkey",
|
||||
CACert: "newca",
|
||||
}
|
||||
|
||||
bsErrorRes = toJSON(errorRes{bootstrap.ErrBootstrap.Error()})
|
||||
unauthRes = toJSON(errorRes{bootstrap.ErrUnauthorizedAccess.Error()})
|
||||
malformedRes = toJSON(errorRes{bootstrap.ErrMalformedEntity.Error()})
|
||||
extKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrExternalKeyNotFound.Error()})
|
||||
extSecKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrSecureBootstrap.Error()})
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -95,6 +102,9 @@ func newConfig(channels []bootstrap.Channel) bootstrap.Config {
|
||||
MFChannels: channels,
|
||||
Name: addName,
|
||||
Content: addContent,
|
||||
ClientCert: "newcert",
|
||||
ClientKey: "newkey",
|
||||
CACert: "newca",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +126,44 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService(users mainflux.UsersServiceClient, unknown map[string]string, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(unknown)
|
||||
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 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 newService(authn mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(users, things, sdk)
|
||||
return bootstrap.New(authn, things, sdk, encKey)
|
||||
}
|
||||
|
||||
func generateChannels() map[string]things.Channel {
|
||||
@@ -139,8 +179,8 @@ func generateChannels() map[string]things.Channel {
|
||||
return channels
|
||||
}
|
||||
|
||||
func newThingsService(users mainflux.UsersServiceClient) things.Service {
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), users)
|
||||
func newThingsService(authn mainflux.AuthNServiceClient) things.Service {
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), authn)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
@@ -149,7 +189,7 @@ func newThingsServer(svc things.Service) *httptest.Server {
|
||||
}
|
||||
|
||||
func newBootstrapServer(svc bootstrap.Service) *httptest.Server {
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader())
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader(encKey))
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -162,7 +202,7 @@ func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
data := toJSON(addReq)
|
||||
@@ -286,7 +326,7 @@ func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
c := newConfig([]bootstrap.Channel{})
|
||||
|
||||
@@ -368,7 +408,7 @@ func TestView(t *testing.T) {
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
var view config
|
||||
if err := json.NewDecoder(res.Body).Decode(&view); err != io.EOF {
|
||||
assert.Nil(t, err, fmt.Sprintf("Decoding expeceted to succeed %s: %s", tc.desc, err))
|
||||
assert.Nil(t, err, fmt.Sprintf("Decoding expected to succeed %s: %s", tc.desc, err))
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, tc.res.Channels, view.Channels, fmt.Sprintf("%s: expected response '%s' got '%s'", tc.desc, tc.res.Channels, view.Channels))
|
||||
@@ -383,7 +423,7 @@ func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -477,7 +517,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -490,7 +530,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
key string
|
||||
id string
|
||||
auth string
|
||||
contentType string
|
||||
status int
|
||||
@@ -498,7 +538,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
{
|
||||
desc: "update unauthorized",
|
||||
req: data,
|
||||
key: saved.MFKey,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
@@ -506,7 +546,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
{
|
||||
desc: "update with an empty token",
|
||||
req: data,
|
||||
key: saved.MFKey,
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
@@ -514,7 +554,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
{
|
||||
desc: "update a valid config",
|
||||
req: data,
|
||||
key: saved.MFKey,
|
||||
id: saved.MFThing,
|
||||
auth: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusOK,
|
||||
@@ -522,7 +562,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
{
|
||||
desc: "update a config with wrong content type",
|
||||
req: data,
|
||||
key: saved.MFKey,
|
||||
id: saved.MFThing,
|
||||
auth: validToken,
|
||||
contentType: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
@@ -530,7 +570,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
{
|
||||
desc: "update a non-existing config",
|
||||
req: data,
|
||||
key: wrongID,
|
||||
id: wrongID,
|
||||
auth: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusNotFound,
|
||||
@@ -538,14 +578,14 @@ func TestUpdateCert(t *testing.T) {
|
||||
{
|
||||
desc: "update a config with invalid request format",
|
||||
req: "}",
|
||||
key: saved.MFKey,
|
||||
id: saved.MFKey,
|
||||
auth: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "update a config with an empty request",
|
||||
key: saved.MFKey,
|
||||
id: saved.MFThing,
|
||||
req: "",
|
||||
auth: validToken,
|
||||
contentType: contentType,
|
||||
@@ -556,8 +596,8 @@ func TestUpdateCert(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: bs.Client(),
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/things/configs/certs/%s", bs.URL, tc.key),
|
||||
method: http.MethodPatch,
|
||||
url: fmt.Sprintf("%s/things/configs/certs/%s", bs.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
@@ -572,7 +612,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -684,7 +724,7 @@ func TestList(t *testing.T) {
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/configs")
|
||||
|
||||
@@ -933,7 +973,7 @@ func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -991,108 +1031,11 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUnknown(t *testing.T) {
|
||||
unknownNum := 10
|
||||
unknown := make([]config, unknownNum)
|
||||
unknownConfigs := make(map[string]string, unknownNum)
|
||||
// Save some unknown elements.
|
||||
for i := 0; i < unknownNum; i++ {
|
||||
u := config{
|
||||
ExternalID: fmt.Sprintf("key-%s", strconv.Itoa(i)),
|
||||
ExternalKey: fmt.Sprintf("%s%s", addExternalKey, strconv.Itoa(i)),
|
||||
}
|
||||
unknownConfigs[u.ExternalID] = u.ExternalKey
|
||||
unknown[i] = u
|
||||
}
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, unknownConfigs, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/unknown/configs")
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
url string
|
||||
status int
|
||||
res []config
|
||||
}{
|
||||
{
|
||||
desc: "view unknown unauthorized",
|
||||
auth: invalidToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusForbidden,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with an empty token",
|
||||
auth: "",
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusForbidden,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with limit < 0",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, -5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with offset < 0",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, -3, 5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with invalid query params",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&key=%%", path, 0, -5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a list of unknown",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusOK,
|
||||
res: unknown[:5],
|
||||
},
|
||||
{
|
||||
desc: "view unknown with no page paremeters",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s", path),
|
||||
status: http.StatusOK,
|
||||
res: unknown[:10],
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
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))
|
||||
var body map[string][]config
|
||||
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.res, body["configs"], fmt.Sprintf("%s: expected response '%s' got '%s'", tc.desc, tc.res, body["configs"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, map[string]string{}, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1100,6 +1043,9 @@ func TestBootstrap(t *testing.T) {
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
encExternKey, err := enc([]byte(c.ExternalKey))
|
||||
require.Nil(t, err, fmt.Sprintf("Encrypting config expected to succeed: %s.\n", err))
|
||||
|
||||
var channels []channel
|
||||
for _, ch := range saved.MFChannels {
|
||||
channels = append(channels, channel{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
@@ -1110,56 +1056,84 @@ func TestBootstrap(t *testing.T) {
|
||||
MFKey string `json:"mainflux_key"`
|
||||
MFChannels []channel `json:"mainflux_channels"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}{
|
||||
MFThing: saved.MFThing,
|
||||
MFKey: saved.MFKey,
|
||||
MFChannels: channels,
|
||||
Content: saved.Content,
|
||||
ClientCert: saved.ClientCert,
|
||||
ClientKey: saved.ClientKey,
|
||||
CACert: saved.CACert,
|
||||
}
|
||||
|
||||
data := toJSON(s)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
external_id string
|
||||
external_key string
|
||||
status int
|
||||
res string
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
status int
|
||||
res string
|
||||
secure bool
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap a Thing with unknown ID",
|
||||
external_id: unknown,
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: "",
|
||||
desc: "bootstrap a Thing with unknown ID",
|
||||
externalID: unknown,
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: bsErrorRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with an empty ID",
|
||||
external_id: "",
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusBadRequest,
|
||||
res: "",
|
||||
desc: "bootstrap a Thing with an empty ID",
|
||||
externalID: "",
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusBadRequest,
|
||||
res: malformedRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with unknown key",
|
||||
external_id: c.ExternalID,
|
||||
external_key: unknown,
|
||||
status: http.StatusNotFound,
|
||||
res: "",
|
||||
desc: "bootstrap a Thing with unknown key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: unknown,
|
||||
status: http.StatusNotFound,
|
||||
res: extKeyNotFoundRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with an empty key",
|
||||
external_id: c.ExternalID,
|
||||
external_key: "",
|
||||
status: http.StatusForbidden,
|
||||
res: "",
|
||||
desc: "bootstrap a Thing with an empty key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "",
|
||||
status: http.StatusForbidden,
|
||||
res: unauthRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap known Thing",
|
||||
external_id: c.ExternalID,
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
desc: "bootstrap known Thing",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap secure",
|
||||
externalID: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
externalKey: hex.EncodeToString(encExternKey),
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
secure: true,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap secure with unencrypted key",
|
||||
externalID: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: extSecKeyNotFoundRes,
|
||||
secure: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1167,8 +1141,8 @@ func TestBootstrap(t *testing.T) {
|
||||
req := testRequest{
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/things/bootstrap/%s", bs.URL, tc.external_id),
|
||||
token: tc.external_key,
|
||||
url: fmt.Sprintf("%s/things/bootstrap/%s", bs.URL, tc.externalID),
|
||||
token: tc.externalKey,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
@@ -1176,6 +1150,9 @@ func TestBootstrap(t *testing.T) {
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
if tc.secure && tc.status == http.StatusOK {
|
||||
body, err = dec(body)
|
||||
}
|
||||
|
||||
data := strings.Trim(string(body), "\n")
|
||||
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected response '%s' got '%s'", tc.desc, tc.res, data))
|
||||
@@ -1186,7 +1163,7 @@ func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1309,3 +1286,7 @@ type configPage struct {
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []config `json:"configs"`
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+27
-31
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// +build !test
|
||||
|
||||
@@ -29,9 +25,9 @@ func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Se
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Add(key string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
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 key %s and thing %s took %s to complete", key, saved.MFThing, time.Since(begin))
|
||||
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
|
||||
@@ -39,12 +35,12 @@ func (lm *loggingMiddleware) Add(key string, cfg bootstrap.Config) (saved bootst
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(key, cfg)
|
||||
return lm.svc.Add(token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) View(key, id string) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view for key %s and thing %s took %s to complete", key, saved.MFThing, time.Since(begin))
|
||||
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
|
||||
@@ -52,12 +48,12 @@ func (lm *loggingMiddleware) View(key, id string) (saved bootstrap.Config, err e
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(key, id)
|
||||
return lm.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Update(key string, cfg bootstrap.Config) (err error) {
|
||||
func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update for key %s and thing %s took %s to complete", key, cfg.MFThing, time.Since(begin))
|
||||
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
|
||||
@@ -65,12 +61,12 @@ func (lm *loggingMiddleware) Update(key string, cfg bootstrap.Config) (err error
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(key, cfg)
|
||||
return lm.svc.Update(token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
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 key %s took %s to complete", thingKey, time.Since(begin))
|
||||
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
|
||||
@@ -78,12 +74,12 @@ func (lm *loggingMiddleware) UpdateCert(key, thingKey, clientCert, clientKey, ca
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(key, thingKey, clientCert, clientKey, caCert)
|
||||
return lm.svc.UpdateCert(token, thingID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_connections for key %s and thing %s took %s to complete", key, id, time.Since(begin))
|
||||
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
|
||||
@@ -91,12 +87,12 @@ func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []str
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(key, id, connections)
|
||||
return lm.svc.UpdateConnections(token, id, connections)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
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 key %s and offset %d and limit %d took %s to complete", key, offset, limit, time.Since(begin))
|
||||
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
|
||||
@@ -104,12 +100,12 @@ func (lm *loggingMiddleware) List(key string, filter bootstrap.Filter, offset, l
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(key, filter, offset, limit)
|
||||
return lm.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Remove(key, id string) (err error) {
|
||||
func (lm *loggingMiddleware) Remove(token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove for key %s and thing %s took %s to complete", key, id, time.Since(begin))
|
||||
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
|
||||
@@ -117,10 +113,10 @@ func (lm *loggingMiddleware) Remove(key, id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(key, id)
|
||||
return lm.svc.Remove(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string) (cfg bootstrap.Config, err error) {
|
||||
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 {
|
||||
@@ -130,12 +126,12 @@ func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string) (cfg boot
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(externalKey, externalID)
|
||||
return lm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(key, id string, state bootstrap.State) (err error) {
|
||||
func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method change_state for key %s and thing %s took %s to complete", key, id, time.Since(begin))
|
||||
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
|
||||
@@ -143,7 +139,7 @@ func (lm *loggingMiddleware) ChangeState(key, id string, state bootstrap.State)
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(key, id, state)
|
||||
return lm.svc.ChangeState(token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
|
||||
+19
-23
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// +build !test
|
||||
|
||||
@@ -34,85 +30,85 @@ func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency m
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Add(key string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
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(key, cfg)
|
||||
return mm.svc.Add(token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) View(id, key string) (saved bootstrap.Config, err error) {
|
||||
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(id, key)
|
||||
return mm.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Update(key string, cfg bootstrap.Config) (err error) {
|
||||
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(key, cfg)
|
||||
return mm.svc.Update(token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
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(key, thingKey, clientCert, clientKey, caCert)
|
||||
return mm.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
|
||||
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(key, id, connections)
|
||||
return mm.svc.UpdateConnections(token, id, connections)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
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(key, filter, offset, limit)
|
||||
return mm.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Remove(id, key string) (err error) {
|
||||
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(id, key)
|
||||
return mm.svc.Remove(token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string) (cfg bootstrap.Config, err error) {
|
||||
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)
|
||||
return mm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ChangeState(id, key string, state bootstrap.State) (err error) {
|
||||
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(id, key, state)
|
||||
return mm.svc.ChangeState(token, id, state)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
@@ -14,7 +10,7 @@ type apiReq interface {
|
||||
}
|
||||
|
||||
type addReq struct {
|
||||
key string
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
@@ -27,7 +23,7 @@ type addReq struct {
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.key == "" {
|
||||
if req.token == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
@@ -76,7 +72,7 @@ func (req updateReq) validate() error {
|
||||
|
||||
type updateCertReq struct {
|
||||
key string
|
||||
thingKey string
|
||||
thingID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
@@ -87,7 +83,7 @@ func (req updateCertReq) validate() error {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.thingKey == "" {
|
||||
if req.thingID == "" {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -11,28 +11,28 @@ import (
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
key: "key",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
key: "key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
@@ -41,7 +41,7 @@ func TestAddReqValidation(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
}
|
||||
@@ -116,29 +116,29 @@ func TestUpdateReqValidation(t *testing.T) {
|
||||
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
thingKey string
|
||||
err error
|
||||
desc string
|
||||
key string
|
||||
thingID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
thingKey: "thingKey",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
thingID: "thingID",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty thing key",
|
||||
key: "key",
|
||||
thingKey: "",
|
||||
err: bootstrap.ErrNotFound,
|
||||
desc: "empty thing key",
|
||||
key: "key",
|
||||
thingID: "",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
key: tc.key,
|
||||
thingKey: tc.thingKey,
|
||||
key: tc.key,
|
||||
thingID: tc.thingID,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
@@ -93,27 +89,6 @@ 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"`
|
||||
@@ -146,3 +121,7 @@ func (res stateRes) Headers() map[string]string {
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+61
-64
@@ -1,16 +1,11 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -21,6 +16,7 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@@ -33,6 +29,8 @@ const (
|
||||
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"}
|
||||
)
|
||||
@@ -62,7 +60,7 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/configs/certs/:key", kithttp.NewServer(
|
||||
r.Patch("/things/configs/certs/:id", kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
encodeResponse,
|
||||
@@ -80,16 +78,16 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/unknown/configs", kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeUnknownRequest,
|
||||
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader),
|
||||
r.Get("/things/bootstrap/secure/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, true),
|
||||
decodeBootstrapRequest,
|
||||
encodeResponse,
|
||||
encodeSecureRes,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/state/:id", kithttp.NewServer(
|
||||
@@ -115,9 +113,9 @@ func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{key: r.Header.Get("Authorization")}
|
||||
req := addReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -131,7 +129,7 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
|
||||
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, err
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -142,10 +140,13 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{key: r.Header.Get("Authorization")}
|
||||
req.thingKey = bone.GetValue(r, "key")
|
||||
req := updateCertReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
thingID: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -159,28 +160,7 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
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, 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 nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -226,7 +206,7 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
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, err
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -258,33 +238,50 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
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
|
||||
}
|
||||
|
||||
switch err {
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errInvalidQueryParams, bootstrap.ErrMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case bootstrap.ErrNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case bootstrap.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case bootstrap.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case bootstrap.ErrThings:
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
case io.EOF:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
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 *json.UnmarshalTypeError:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,13 +302,13 @@ func parsePagePrams(q url.Values) (uint64, uint64, error) {
|
||||
offset, err := parseUint(q.Get("offset"))
|
||||
q.Del("offset")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, errors.Wrap(errInvalidOffsetParam, err)
|
||||
}
|
||||
|
||||
limit, err := parseUint(q.Get("limit"))
|
||||
q.Del("limit")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, errors.Wrap(errInvalidLimitParam, err)
|
||||
}
|
||||
|
||||
if limit > maxLimit {
|
||||
|
||||
+16
-27
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
@@ -36,7 +32,6 @@ type Channel struct {
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
Unknown bool
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
@@ -54,60 +49,54 @@ type ConfigsPage struct {
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(Config, []string) (string, error)
|
||||
Save(cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(string, string) (Config, error)
|
||||
RetrieveByID(owner, id string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(string, Filter, uint64, uint64) ConfigsPage
|
||||
RetrieveAll(owner string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(string, string) (Config, error)
|
||||
RetrieveByExternalID(externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(Config) error
|
||||
Update(cfg Config) error
|
||||
|
||||
// UpdateCerts updates an existing Config certificate and key.
|
||||
// UpdateCerts updates an existing Config certificate and owner.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(string, string, string, string, string) error
|
||||
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(string, string, []Channel, []string) error
|
||||
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(string, string) error
|
||||
Remove(owner, id string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(string, string, State) error
|
||||
|
||||
// SaveUnknown saves Thing which unsuccessfully bootstrapped.
|
||||
SaveUnknown(string, string) error
|
||||
|
||||
// RetrieveUnknown returns a subset of unsuccessfully bootstrapped Things.
|
||||
RetrieveUnknown(uint64, uint64) ConfigsPage
|
||||
ChangeState(owner, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(string, []string) ([]Channel, error)
|
||||
ListExisting(owner string, ids []string) ([]Channel, error)
|
||||
|
||||
// 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(string) error
|
||||
RemoveThing(id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(Channel) error
|
||||
UpdateChannel(c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(string) error
|
||||
RemoveChannel(id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when the corresponding Thing is
|
||||
// disconnected from the Channel.
|
||||
DisconnectThing(string, string) error
|
||||
DisconnectThing(channelID, thingID string) error
|
||||
}
|
||||
|
||||
+1
-5
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// Mainflux bootstrap service functionality.
|
||||
|
||||
+16
-76
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mocks
|
||||
|
||||
@@ -28,15 +24,13 @@ type configRepositoryMock struct {
|
||||
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 {
|
||||
func NewConfigsRepository() bootstrap.ConfigRepository {
|
||||
return &configRepositoryMock{
|
||||
configs: make(map[string]bootstrap.Config),
|
||||
channels: make(map[string]bootstrap.Channel),
|
||||
unknown: unknown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +59,11 @@ func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []str
|
||||
}
|
||||
|
||||
crm.configs[config.MFThing] = config
|
||||
delete(crm.unknown, config.ExternalID)
|
||||
|
||||
return config.MFThing, nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config, error) {
|
||||
func (crm *configRepositoryMock) RetrieveByID(token, id string) (bootstrap.Config, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -78,7 +71,7 @@ func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config,
|
||||
if !ok {
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
}
|
||||
if c.Owner != key {
|
||||
if c.Owner != token {
|
||||
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
@@ -86,7 +79,7 @@ func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config,
|
||||
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
func (crm *configRepositoryMock) RetrieveAll(token string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -114,7 +107,7 @@ func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter
|
||||
id, _ := strconv.ParseUint(v.MFThing, 10, 64)
|
||||
if (state == emptyState || v.State == state) &&
|
||||
(name == "" || strings.Index(strings.ToLower(v.Name), name) != notFoundIdx) &&
|
||||
v.Owner == key {
|
||||
v.Owner == token {
|
||||
if id >= first && id < last {
|
||||
configs = append(configs, v)
|
||||
}
|
||||
@@ -134,12 +127,12 @@ func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
|
||||
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 && cfg.ExternalKey == externalKey {
|
||||
if cfg.ExternalID == externalID {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
@@ -163,12 +156,12 @@ func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateCert(owner, thingKey, clientCert, clientKey, caCert string) error {
|
||||
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.MFKey == thingKey && v.Owner == owner {
|
||||
if v.MFThing == thingID && v.Owner == owner {
|
||||
forUpdate = v
|
||||
break
|
||||
}
|
||||
@@ -184,7 +177,7 @@ func (crm *configRepositoryMock) UpdateCert(owner, thingKey, clientCert, clientK
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -210,12 +203,12 @@ func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Remove(key, id string) error {
|
||||
func (crm *configRepositoryMock) Remove(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.configs {
|
||||
if v.Owner == key && k == id {
|
||||
if v.Owner == token && k == id {
|
||||
delete(crm.configs, k)
|
||||
break
|
||||
}
|
||||
@@ -224,7 +217,7 @@ func (crm *configRepositoryMock) Remove(key, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.State) error {
|
||||
func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.State) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -232,7 +225,7 @@ func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.Sta
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
if config.Owner != key {
|
||||
if config.Owner != token {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
@@ -241,60 +234,7 @@ func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.Sta
|
||||
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(key, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.unknown {
|
||||
if k == id && v == key {
|
||||
delete(crm.unknown, k)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) SaveUnknown(key, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.unknown[id] = key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ListExisting(key string, connections []string) ([]bootstrap.Channel, error) {
|
||||
func (crm *configRepositoryMock) ListExisting(token string, connections []string) ([]bootstrap.Channel, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
|
||||
+53
-38
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mocks
|
||||
|
||||
@@ -23,43 +19,45 @@ type mainfluxThings struct {
|
||||
counter uint64
|
||||
things map[string]things.Thing
|
||||
channels map[string]things.Channel
|
||||
users mainflux.UsersServiceClient
|
||||
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, users mainflux.UsersServiceClient) things.Service {
|
||||
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, authn mainflux.AuthNServiceClient) things.Service {
|
||||
return &mainfluxThings{
|
||||
things: things,
|
||||
channels: channels,
|
||||
users: users,
|
||||
auth: authn,
|
||||
connections: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) AddThing(_ context.Context, owner string, thing things.Thing) (things.Thing, error) {
|
||||
func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...things.Thing) ([]things.Thing, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, things.ErrUnauthorizedAccess
|
||||
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]
|
||||
}
|
||||
|
||||
svc.counter++
|
||||
thing.Owner = userID.Value
|
||||
thing.ID = strconv.FormatUint(svc.counter, 10)
|
||||
thing.Key = thing.ID
|
||||
svc.things[thing.ID] = thing
|
||||
return thing, nil
|
||||
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.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, things.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -72,20 +70,23 @@ func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (thing
|
||||
return things.Thing{}, things.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Connect(_ context.Context, owner, chanID, thingID string) error {
|
||||
func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs []string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if svc.channels[chanID].Owner != userID.Value {
|
||||
return things.ErrNotFound
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
svc.connections[chanID] = append(svc.connections[chanID], thingID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ func (svc *mainfluxThings) Disconnect(_ context.Context, owner, chanID, thingID
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil || svc.channels[chanID].Owner != userID.Value {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -125,7 +126,7 @@ func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) erro
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -137,13 +138,13 @@ func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) erro
|
||||
delete(svc.things, id)
|
||||
conns := make(map[string][]string)
|
||||
for k, v := range svc.connections {
|
||||
idx := findIndex(v, id)
|
||||
if idx != -1 {
|
||||
i := findIndex(v, id)
|
||||
if i != -1 {
|
||||
var tmp []string
|
||||
if idx != len(v)-2 {
|
||||
tmp = v[idx+1:]
|
||||
if i != len(v)-2 {
|
||||
tmp = v[i+1:]
|
||||
}
|
||||
conns[k] = append(v[:idx], tmp...)
|
||||
conns[k] = append(v[:i], tmp...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,27 +167,41 @@ func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) er
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string) (things.ThingsPage, error) {
|
||||
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) {
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64, bool) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64) (things.ThingsPage, error) {
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64, bool) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateChannel(context.Context, string, things.Channel) (things.Channel, 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.ChannelsPage, error) {
|
||||
func (svc *mainfluxThings) ListChannels(context.Context, string, uint64, uint64, string, things.Metadata) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -194,7 +209,7 @@ func (svc *mainfluxThings) RemoveChannel(context.Context, string, string) error
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CanAccess(context.Context, string, string) (string, error) {
|
||||
func (svc *mainfluxThings) CanAccessByKey(context.Context, string, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mocks
|
||||
|
||||
@@ -15,20 +11,30 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceClient = (*usersServiceMock)(nil)
|
||||
var _ mainflux.AuthNServiceClient = (*serviceMock)(nil)
|
||||
|
||||
type usersServiceMock struct {
|
||||
type serviceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.UsersServiceClient {
|
||||
return &usersServiceMock{users}
|
||||
func NewUsersService(users map[string]string) mainflux.AuthNServiceClient {
|
||||
return &serviceMock{users}
|
||||
}
|
||||
|
||||
func (svc usersServiceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserID, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform things configuration.
|
||||
version: "1.0.0"
|
||||
|
||||
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: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
201:
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
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: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/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: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
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: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
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: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/EncConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
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: "#/components/schemas/State"
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Config"
|
||||
required:
|
||||
- configs
|
||||
BootstrapConfig:
|
||||
type: object
|
||||
properties:
|
||||
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 certificate.
|
||||
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
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
schema:
|
||||
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
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/State"
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ConfigCreateReq:
|
||||
description: JSON-formatted document describing the new config.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
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:
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigCertUpdateReq:
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the thing is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
ConfigStateUpdateReq:
|
||||
description: Update the state of the Config.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
|
||||
responses:
|
||||
ConfigCreateRes:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /things/configs/{configId}).
|
||||
ConfigListRes:
|
||||
description: Data retrieved. Configs from this list don't contain channels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigList"
|
||||
ConfigRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
BootstrapConfigRes:
|
||||
description: |
|
||||
Data retrieved. If secure, a response is encrypted using
|
||||
the secret key, so the response is in the binary form.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
+93
-135
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres
|
||||
|
||||
@@ -17,6 +13,7 @@ import (
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,6 +28,20 @@ const (
|
||||
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")
|
||||
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 {
|
||||
@@ -44,13 +55,13 @@ func NewConfigRepository(db *sqlx.DB, log logger.Logger) bootstrap.ConfigReposit
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (string, error) {
|
||||
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 "", err
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
}
|
||||
|
||||
dbcfg := toDBConfig(cfg)
|
||||
@@ -63,27 +74,19 @@ func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (str
|
||||
|
||||
cr.rollback("Failed to insert a Config", tx, err)
|
||||
|
||||
return "", e
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels", tx, err)
|
||||
|
||||
return "", err
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(cfg, connections, tx); err != nil {
|
||||
if err := insertConnections(cfg, chsConnIDs, tx); err != nil {
|
||||
cr.rollback("Failed to insert connections", tx, err)
|
||||
|
||||
return "", 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 "", err
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -93,23 +96,23 @@ func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (str
|
||||
return cfg.MFThing, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
|
||||
FROM configs
|
||||
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: key,
|
||||
Owner: owner,
|
||||
}
|
||||
|
||||
if err := cr.db.QueryRowx(q, id, key).StructScan(&dbcfg); err != nil {
|
||||
if err := cr.db.QueryRowx(q, id, owner).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return empty, err
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
@@ -120,7 +123,7 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
||||
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{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -129,13 +132,13 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
||||
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{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
dbch.Owner = nullString(dbcfg.Owner)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
@@ -146,8 +149,8 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := cr.retrieveAll(key, filter)
|
||||
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
|
||||
@@ -165,7 +168,7 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{Owner: key}
|
||||
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{}
|
||||
@@ -192,21 +195,20 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, owner, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
WHERE external_key = $1 AND external_id = $2`
|
||||
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,
|
||||
ExternalKey: externalKey,
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
if err := cr.db.QueryRowx(q, externalKey, externalID).StructScan(&dbcfg); err != nil {
|
||||
if err := cr.db.QueryRowx(q, externalID).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
}
|
||||
return empty, err
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
@@ -217,7 +219,7 @@ func (cr configRepository) RetrieveByExternalID(externalKey, externalID string)
|
||||
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{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -226,13 +228,13 @@ func (cr configRepository) RetrieveByExternalID(externalKey, externalID string)
|
||||
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{}, 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{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
@@ -252,12 +254,12 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
|
||||
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdate, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdate, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
@@ -267,10 +269,10 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) error {
|
||||
q := `UPDATE configs SET client_cert = $1, client_key = $2, ca_cert = $3 WHERE mainflux_key = $4 AND owner = $5`
|
||||
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, thingKey, key)
|
||||
res, err := cr.db.Exec(q, clientCert, clientKey, caCert, thingID, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -287,19 +289,19 @@ func (cr configRepository) UpdateCert(key, thingKey, clientCert, clientKey, caCe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
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(key, channels, tx); err != nil {
|
||||
if err := insertChannels(owner, channels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels during the update", tx, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateConnections(key, id, connections, tx); err != nil {
|
||||
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
|
||||
@@ -317,10 +319,10 @@ func (cr configRepository) UpdateConnections(key, id string, channels []bootstra
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(key, id string) error {
|
||||
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, key); err != nil {
|
||||
return err
|
||||
if _, err := cr.db.Exec(q, id, owner); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
@@ -330,10 +332,10 @@ func (cr configRepository) Remove(key, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(key, id string, state bootstrap.State) error {
|
||||
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, key)
|
||||
res, err := cr.db.Exec(q, state, id, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -350,14 +352,14 @@ func (cr configRepository) ChangeState(key, id string, state bootstrap.State) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(key string, ids []string) ([]bootstrap.Channel, error) {
|
||||
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, key, pq.Array(ids))
|
||||
rows, err := cr.db.Queryx(q, owner, pq.Array(ids))
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
@@ -381,55 +383,6 @@ func (cr configRepository) ListExisting(key string, ids []string) ([]bootstrap.C
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) SaveUnknown(key, id string) error {
|
||||
q := `INSERT INTO unknown_configs (external_id, external_key) VALUES ($1, $2)`
|
||||
|
||||
if _, err := cr.db.Exec(q, id, key); err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
return nil
|
||||
}
|
||||
return 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)
|
||||
@@ -437,43 +390,48 @@ func (cr configRepository) RemoveThing(id string) error {
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return err
|
||||
if err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(channel bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", channel)
|
||||
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`
|
||||
_, err = cr.db.NamedExec(q, dbch)
|
||||
|
||||
return err
|
||||
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`
|
||||
_, err := cr.db.Exec(q, id)
|
||||
|
||||
return err
|
||||
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)`
|
||||
_, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID)
|
||||
|
||||
return err
|
||||
if _, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID); err != nil {
|
||||
return errors.Wrap(errDisconnectThing, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) retrieveAll(key string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
func (cr configRepository) retrieveAll(owner string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
template := `WHERE owner = $1 %s`
|
||||
params := []interface{}{key}
|
||||
params := []interface{}{owner}
|
||||
// One empty string so that strings Join works if only one filter is applied.
|
||||
queries := []string{""}
|
||||
// Since key is the first param, start from 2.
|
||||
// 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))
|
||||
@@ -499,21 +457,21 @@ func (cr configRepository) rollback(content string, tx *sqlx.Tx, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func insertChannels(key string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
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(key, ch)
|
||||
dbch, err := toDBChannel(owner, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata)
|
||||
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
|
||||
@@ -531,7 +489,7 @@ func insertConnections(cfg bootstrap.Config, connections []string, tx *sqlx.Tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
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 {
|
||||
@@ -548,7 +506,7 @@ func insertConnections(cfg bootstrap.Config, connections []string, tx *sqlx.Tx)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error {
|
||||
func updateConnections(owner, id string, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -557,7 +515,7 @@ func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error
|
||||
WHERE config_id = $1 AND config_owner = $2 AND channel_owner = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
res, err := tx.Exec(q, id, key, pq.Array(connections))
|
||||
res, err := tx.Exec(q, id, owner, pq.Array(connections))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -567,7 +525,7 @@ func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
@@ -575,8 +533,8 @@ func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error
|
||||
dbconn := dbConnection{
|
||||
Config: id,
|
||||
Channel: conn,
|
||||
ConfigOwner: key,
|
||||
ChannelOwner: key,
|
||||
ConfigOwner: owner,
|
||||
ChannelOwner: owner,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
@@ -683,7 +641,7 @@ func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, err
|
||||
return dbChannel{}, errors.Wrap(errMarshalChannel, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
@@ -700,7 +658,7 @@ func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, err
|
||||
return bootstrap.Channel{}, errors.Wrap(errUnmarshalChannel, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres_test
|
||||
|
||||
@@ -15,6 +11,7 @@ import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -94,7 +91,7 @@ func TestSave(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(tc.config, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +147,7 @@ func TestRetrieveByID(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(tc.owner, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,33 +249,24 @@ func TestRetrieveByExternalID(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
desc string
|
||||
externalID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
externalKey: config.ExternalKey,
|
||||
err: bootstrap.ErrNotFound,
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with invalid external key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: c.ExternalKey,
|
||||
err: nil,
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(tc.externalKey, tc.externalID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
_, 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +311,7 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.Update(tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +339,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
thingID string
|
||||
owner string
|
||||
cert string
|
||||
certKey string
|
||||
@@ -360,7 +348,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong owner",
|
||||
key: "",
|
||||
thingID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
@@ -369,7 +357,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
key: c.MFKey,
|
||||
thingID: c.MFThing,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
@@ -378,8 +366,8 @@ func TestUpdateCert(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateCert(tc.owner, tc.key, tc.cert, tc.key, tc.ca)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +399,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
owner string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
@@ -419,7 +407,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
@@ -427,7 +415,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
@@ -435,7 +423,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update connections with existing channels",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: c2,
|
||||
channels: nil,
|
||||
connections: channels,
|
||||
@@ -443,7 +431,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
@@ -451,8 +439,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateConnections(tc.key, tc.id, tc.channels, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +467,7 @@ func TestRemove(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(c.Owner, id)
|
||||
require.Equal(t, bootstrap.ErrNotFound, err, fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
|
||||
require.True(t, errors.Contains(err, bootstrap.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +523,7 @@ func TestChangeState(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeState(tc.owner, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,99 +550,36 @@ func TestListExisting(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
owner string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
key: c.Owner,
|
||||
owner: c.Owner,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
key: c.Owner,
|
||||
owner: c.Owner,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
key: c.Owner,
|
||||
owner: c.Owner,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(tc.key, tc.connections)
|
||||
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.Equal(t, tc.err, 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)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres
|
||||
|
||||
@@ -95,6 +91,15 @@ func migrateDB(db *sqlx.DB) error {
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_2",
|
||||
Up: []string{
|
||||
"DROP TABLE IF EXISTS unknown_configs",
|
||||
},
|
||||
Down: []string{
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres_test
|
||||
|
||||
@@ -16,7 +12,7 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -73,10 +69,11 @@ func TestMain(m *testing.M) {
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
+38
-15
@@ -1,16 +1,15 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
// bootstrapRes represent Mainflux Response to the Bootatrap request.
|
||||
@@ -20,10 +19,10 @@ 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"`
|
||||
Content string `json:"content,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
@@ -44,15 +43,17 @@ func (res bootstrapRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type reader struct{}
|
||||
type reader struct {
|
||||
encKey []byte
|
||||
}
|
||||
|
||||
// NewConfigReader return new reader which is used to generate response
|
||||
// from the config.
|
||||
func NewConfigReader() ConfigReader {
|
||||
return reader{}
|
||||
func NewConfigReader(encKey []byte) ConfigReader {
|
||||
return reader{encKey: encKey}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config) (mainflux.Response, error) {
|
||||
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})
|
||||
@@ -62,11 +63,33 @@ func (r reader) ReadConfig(cfg Config) (mainflux.Response, error) {
|
||||
MFKey: cfg.MFKey,
|
||||
MFThing: cfg.MFThing,
|
||||
MFChannels: channels,
|
||||
Content: cfg.Content,
|
||||
ClientCert: cfg.ClientCert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
CaCert: cfg.CACert,
|
||||
Content: cfg.Content,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package consumer contains events consumer for events
|
||||
// published by Things service.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package consumer
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package consumer
|
||||
|
||||
@@ -31,8 +27,8 @@ const (
|
||||
exists = "BUSYGROUP Consumer Group name already exists"
|
||||
)
|
||||
|
||||
// EventStore represents event source for things and channels provisioning.
|
||||
type EventStore interface {
|
||||
// Subscriber represents event source for things and channels provisioning.
|
||||
type Subscriber interface {
|
||||
// Subscribes to given subject and receives events.
|
||||
Subscribe(string) error
|
||||
}
|
||||
@@ -45,7 +41,7 @@ type eventStore struct {
|
||||
}
|
||||
|
||||
// NewEventStore returns new event store instance.
|
||||
func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string, log logger.Logger) EventStore {
|
||||
func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string, log logger.Logger) Subscriber {
|
||||
return eventStore{
|
||||
svc: svc,
|
||||
client: client,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package producer contains the domain events needed to support
|
||||
// event sourcing of Bootstrap service actions.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package producer
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package producer_test
|
||||
|
||||
@@ -14,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package producer
|
||||
|
||||
@@ -35,8 +31,8 @@ func NewEventStoreMiddleware(svc bootstrap.Service, client *redis.Client) bootst
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Add(key string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(key, cfg)
|
||||
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
|
||||
}
|
||||
@@ -61,12 +57,12 @@ func (es eventStore) Add(key string, cfg bootstrap.Config) (bootstrap.Config, er
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es eventStore) View(key, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(key, id)
|
||||
func (es eventStore) View(token, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) Update(key string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(key, cfg); err != nil {
|
||||
func (es eventStore) Update(token string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(token, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -82,12 +78,12 @@ func (es eventStore) Update(key string, cfg bootstrap.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(key, thingKey, clientCert, clientKey, caCert)
|
||||
func (es eventStore) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateConnections(key, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(key, id, connections); err != nil {
|
||||
func (es eventStore) UpdateConnections(token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -102,12 +98,12 @@ func (es eventStore) UpdateConnections(key, id string, connections []string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) List(key string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(key, filter, offset, limit)
|
||||
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(key, id string) error {
|
||||
if err := es.svc.Remove(key, id); err != nil {
|
||||
func (es eventStore) Remove(token, id string) error {
|
||||
if err := es.svc.Remove(token, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -121,8 +117,8 @@ func (es eventStore) Remove(key, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) Bootstrap(externalKey, externalID string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(externalKey, externalID)
|
||||
func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
externalID: externalID,
|
||||
@@ -139,8 +135,8 @@ func (es eventStore) Bootstrap(externalKey, externalID string) (bootstrap.Config
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es eventStore) ChangeState(key, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(key, id, state); err != nil {
|
||||
func (es eventStore) ChangeState(token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package producer_test
|
||||
|
||||
@@ -17,12 +13,13 @@ import (
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/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"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -33,8 +30,6 @@ const (
|
||||
streamID = "mainflux.bootstrap"
|
||||
email = "user@example.com"
|
||||
validToken = "validToken"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
defaultTimout = 5
|
||||
|
||||
@@ -50,6 +45,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
encKey = []byte("1234567891011121")
|
||||
|
||||
channel = bootstrap.Channel{
|
||||
ID: "1",
|
||||
Name: "name",
|
||||
@@ -64,17 +61,17 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func newService(users mainflux.UsersServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(users, configs, sdk)
|
||||
return bootstrap.New(auth, configs, sdk, encKey)
|
||||
}
|
||||
|
||||
func newThingsService(users mainflux.UsersServiceClient) things.Service {
|
||||
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)
|
||||
@@ -85,7 +82,7 @@ func newThingsService(users mainflux.UsersServiceClient) things.Service {
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, users)
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
@@ -111,14 +108,14 @@ func TestAdd(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "create config successfully",
|
||||
config: config,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": "1",
|
||||
@@ -134,7 +131,7 @@ func TestAdd(t *testing.T) {
|
||||
{
|
||||
desc: "create invalid config",
|
||||
config: invalidConfig,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
event: nil,
|
||||
},
|
||||
@@ -142,8 +139,8 @@ func TestAdd(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.key, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
_, 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},
|
||||
@@ -206,14 +203,14 @@ func TestUpdate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "update config successfully",
|
||||
config: modified,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": modified.MFThing,
|
||||
@@ -226,7 +223,7 @@ func TestUpdate(t *testing.T) {
|
||||
{
|
||||
desc: "update non-existing config",
|
||||
config: nonExisting,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
@@ -234,7 +231,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.key, tc.config)
|
||||
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{
|
||||
@@ -269,7 +266,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
connections []string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
@@ -277,7 +274,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
{
|
||||
desc: "update connections successfully",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
@@ -290,7 +287,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
{
|
||||
desc: "update connections unsuccessfully",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
connections: []string{"256"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
event: nil,
|
||||
@@ -299,8 +296,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.key, tc.id, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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},
|
||||
@@ -354,15 +351,15 @@ func TestRemove(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "remove config successfully",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove config successfully",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": saved.MFThing,
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -372,7 +369,7 @@ func TestRemove(t *testing.T) {
|
||||
{
|
||||
desc: "remove config with invalid credentials",
|
||||
id: saved.MFThing,
|
||||
key: "",
|
||||
token: "",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
event: nil,
|
||||
},
|
||||
@@ -380,7 +377,7 @@ func TestRemove(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.key, tc.id)
|
||||
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{
|
||||
@@ -449,8 +446,8 @@ func TestBootstrap(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Bootstrap(tc.externalKey, tc.externalID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
_, 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},
|
||||
@@ -486,7 +483,7 @@ func TestChangeState(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
state bootstrap.State
|
||||
err error
|
||||
event map[string]interface{}
|
||||
@@ -494,7 +491,7 @@ func TestChangeState(t *testing.T) {
|
||||
{
|
||||
desc: "change state to active",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
@@ -507,7 +504,7 @@ func TestChangeState(t *testing.T) {
|
||||
{
|
||||
desc: "change state invalid credentials",
|
||||
id: saved.MFThing,
|
||||
key: "",
|
||||
token: "",
|
||||
state: bootstrap.Inactive,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
event: nil,
|
||||
@@ -516,7 +513,7 @@ func TestChangeState(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.key, tc.id, tc.state)
|
||||
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{
|
||||
|
||||
+178
-99
@@ -1,19 +1,18 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -32,7 +31,30 @@ var (
|
||||
|
||||
// ErrThings indicates failure to communicate with Mainflux Things service.
|
||||
// It can be due to networking error or invalid/unauthorized request.
|
||||
ErrThings = errors.New("error receiving response from Things service")
|
||||
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 error in getting bootstrap configuration for given encrypted external key
|
||||
ErrSecureBootstrap = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
|
||||
errAddBootstrap = errors.New("failed to add bootstrap configuration")
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
errRemoveChannel = errors.New("failed to remove channel")
|
||||
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)
|
||||
@@ -40,49 +62,49 @@ 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 key.
|
||||
Add(string, Config) (Config, error)
|
||||
// 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 key.
|
||||
View(string, string) (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(string, Config) error
|
||||
Update(token string, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and key.
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(string, string, string, string, string) error
|
||||
UpdateCert(token, thingID, clientCert, clientKey, caCert string) error
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(string, string, []string) error
|
||||
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 key.
|
||||
List(string, Filter, uint64, uint64) (ConfigsPage, error)
|
||||
// user identified by the given token.
|
||||
List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified key that belongs to the user identified by the given key.
|
||||
Remove(string, string) 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(string, string) (Config, error)
|
||||
Bootstrap(externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Thing with given ID and owner.
|
||||
ChangeState(string, string, State) error
|
||||
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.
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(string) error
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(Channel) error
|
||||
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(string) error
|
||||
RemoveChannelHandler(id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
|
||||
DisconnectThingHandler(string, string) error
|
||||
DisconnectThingHandler(channelID, thingID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
@@ -90,26 +112,29 @@ type Service 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) (mainflux.Response, error)
|
||||
ReadConfig(Config, bool) (interface{}, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
users mainflux.UsersServiceClient
|
||||
auth mainflux.AuthNServiceClient
|
||||
configs ConfigRepository
|
||||
sdk mfsdk.SDK
|
||||
encKey []byte
|
||||
reader ConfigReader
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(users mainflux.UsersServiceClient, configs ConfigRepository, sdk mfsdk.SDK) Service {
|
||||
func New(auth mainflux.AuthNServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
users: users,
|
||||
auth: auth,
|
||||
encKey: encKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
@@ -119,33 +144,34 @@ func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
// 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{}, err
|
||||
return Config{}, errors.Wrap(errCheckChannels, err)
|
||||
}
|
||||
|
||||
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), key)
|
||||
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), token)
|
||||
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(errConnectionChannels, err)
|
||||
}
|
||||
|
||||
id := cfg.MFThing
|
||||
mfThing, err := bs.thing(key, id)
|
||||
mfThing, err := bs.thing(token, id)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
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)
|
||||
|
||||
saved, err := bs.configs.Save(cfg, toConnect)
|
||||
if err != nil {
|
||||
if id == "" {
|
||||
// Fail silently.
|
||||
bs.sdk.DeleteThing(cfg.MFThing, key)
|
||||
if errT := bs.sdk.DeleteThing(cfg.MFThing, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(errAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.MFThing = saved
|
||||
@@ -154,8 +180,8 @@ func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(key, id string) (Config, error) {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) View(token, id string) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
@@ -163,8 +189,8 @@ func (bs bootstrapService) View(key, id string) (Config, error) {
|
||||
return bs.configs.RetrieveByID(owner, id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(key string, cfg Config) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) Update(token string, cfg Config) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -174,23 +200,26 @@ func (bs bootstrapService) Update(key string, cfg Config) error {
|
||||
return bs.configs.Update(cfg)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caCert string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.configs.UpdateCert(owner, thingKey, clientCert, clientKey, caCert)
|
||||
if err := bs.configs.UpdateCert(owner, thingID, clientCert, clientKey, caCert); err != nil {
|
||||
return errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(key, id string, connections []string) error {
|
||||
owner, err := bs.identify(key)
|
||||
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 err
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
@@ -198,12 +227,12 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
// 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 err
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), key)
|
||||
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), token)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
cfg.MFChannels = channels
|
||||
@@ -215,8 +244,8 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
}
|
||||
|
||||
for _, c := range disconnect {
|
||||
if err := bs.sdk.DisconnectThing(id, c, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
if err := bs.sdk.DisconnectThing(id, c, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
@@ -224,8 +253,12 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
}
|
||||
|
||||
for _, c := range connect {
|
||||
if err := bs.sdk.ConnectThing(id, c, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
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
|
||||
@@ -235,49 +268,56 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
return bs.configs.UpdateConnections(owner, id, channels, connections)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(key string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
owner, err := bs.identify(key)
|
||||
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(key, id string) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) Remove(token, id string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bs.configs.Remove(owner, id)
|
||||
if err := bs.configs.Remove(owner, id); err != nil {
|
||||
return errors.Wrap(errRemoveBootstrap, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalKey, externalID)
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalID)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
bs.configs.SaveUnknown(externalKey, externalID)
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrSecureBootstrap, err)
|
||||
}
|
||||
return Config{}, ErrNotFound
|
||||
externalKey = dec
|
||||
}
|
||||
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, errors.Wrap(ErrExternalKeyNotFound, ErrNotFound)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(key, id string, state State) error {
|
||||
owner, err := bs.identify(key)
|
||||
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 err
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
|
||||
if cfg.State == state {
|
||||
@@ -287,45 +327,63 @@ func (bs bootstrapService) ChangeState(key, id string, state State) error {
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range cfg.MFChannels {
|
||||
if err := bs.sdk.ConnectThing(cfg.MFThing, c.ID, key); err != nil {
|
||||
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, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
if err := bs.sdk.DisconnectThing(cfg.MFThing, c.ID, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bs.configs.ChangeState(owner, id, state)
|
||||
if err := bs.configs.ChangeState(owner, id, state); err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
|
||||
return bs.configs.UpdateChannel(channel)
|
||||
if err := bs.configs.UpdateChannel(channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(id string) error {
|
||||
return bs.configs.RemoveThing(id)
|
||||
if err := bs.configs.RemoveThing(id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(id string) error {
|
||||
return bs.configs.RemoveChannel(id)
|
||||
if err := bs.configs.RemoveChannel(id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
|
||||
return bs.configs.DisconnectThing(channelID, thingID)
|
||||
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.users.Identify(ctx, &mainflux.Token{Value: token})
|
||||
res, err := bs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -334,34 +392,36 @@ func (bs bootstrapService) identify(token string) (string, error) {
|
||||
}
|
||||
|
||||
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) thing(key, id string) (mfsdk.Thing, error) {
|
||||
func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
|
||||
thingID := id
|
||||
var err error
|
||||
|
||||
if id == "" {
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, key)
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, token)
|
||||
if err != nil {
|
||||
return mfsdk.Thing{}, err
|
||||
return mfsdk.Thing{}, errors.Wrap(errCreateThing, err)
|
||||
}
|
||||
}
|
||||
|
||||
thing, err := bs.sdk.Thing(thingID, key)
|
||||
thing, err := bs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
return mfsdk.Thing{}, ErrNotFound
|
||||
if errors.Contains(err, mfsdk.ErrFailedFetch) {
|
||||
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, ErrNotFound)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
bs.sdk.DeleteThing(thingID, key)
|
||||
if errT := bs.sdk.DeleteThing(thingID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
|
||||
return mfsdk.Thing{}, ErrThings
|
||||
return mfsdk.Thing{}, errors.Wrap(ErrThings, err)
|
||||
}
|
||||
|
||||
return thing, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(channels, existing []string, key string) ([]Channel, error) {
|
||||
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
|
||||
@@ -375,9 +435,9 @@ func (bs bootstrapService) connectionChannels(channels, existing []string, key s
|
||||
|
||||
var ret []Channel
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(id, key)
|
||||
ch, err := bs.sdk.Channel(id, token)
|
||||
if err != nil {
|
||||
return nil, ErrMalformedEntity
|
||||
return nil, errors.Wrap(ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
@@ -426,3 +486,22 @@ func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+137
-124
@@ -1,14 +1,15 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// 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"
|
||||
@@ -19,7 +20,8 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -31,12 +33,12 @@ const (
|
||||
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",
|
||||
@@ -51,17 +53,17 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func newService(users mainflux.UsersServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(users, things, sdk)
|
||||
return bootstrap.New(auth, things, sdk, encKey)
|
||||
}
|
||||
|
||||
func newThingsService(users mainflux.UsersServiceClient) things.Service {
|
||||
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)
|
||||
@@ -72,7 +74,7 @@ func newThingsService(users mainflux.UsersServiceClient) things.Service {
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, users)
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
@@ -80,6 +82,21 @@ func newThingsServer(svc things.Service) *httptest.Server {
|
||||
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})
|
||||
|
||||
@@ -97,38 +114,38 @@ func TestAdd(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "add a new config",
|
||||
config: config,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add a config with an invalid ID",
|
||||
config: neID,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "add a config with wrong credentials",
|
||||
config: config,
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "add a config with invalid list of channels",
|
||||
config: wrongChannels,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.key, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
_, 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,34 +159,34 @@ func TestView(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view an existing config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "view an existing config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.View(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
_, 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,32 +213,32 @@ func TestUpdate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update a config with state Created",
|
||||
config: modifiedCreated,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a non-existing config",
|
||||
config: nonExisting,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config with wrong credentials",
|
||||
config: saved,
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.key, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +257,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
thingKey string
|
||||
clientCert string
|
||||
clientKey string
|
||||
@@ -253,7 +270,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
@@ -263,8 +280,8 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update config cert with wrong credentials",
|
||||
@@ -272,14 +289,14 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateCert(tc.key, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,42 +326,42 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections for config with state Inactive",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for config with state Active",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: active.MFThing,
|
||||
connections: []string{"3"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for non-existing config",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: "",
|
||||
connections: []string{"3"},
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections with invalid channels",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"wrong"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update connections a config with wrong credentials",
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
id: created.MFKey,
|
||||
connections: []string{"2", "3"},
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
@@ -352,8 +369,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.key, tc.id, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,18 +398,13 @@ func TestList(t *testing.T) {
|
||||
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
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
@@ -404,7 +416,7 @@ func TestList(t *testing.T) {
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
@@ -418,7 +430,7 @@ func TestList(t *testing.T) {
|
||||
Configs: saved[95:96],
|
||||
},
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
@@ -427,7 +439,7 @@ func TestList(t *testing.T) {
|
||||
desc: "list configs unauthorized",
|
||||
config: bootstrap.ConfigsPage{},
|
||||
filter: bootstrap.Filter{},
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
@@ -441,7 +453,7 @@ func TestList(t *testing.T) {
|
||||
Configs: saved[95:],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
offset: 95,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
@@ -455,32 +467,18 @@ func TestList(t *testing.T) {
|
||||
Configs: []bootstrap.Config{saved[41]},
|
||||
},
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
key: validToken,
|
||||
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},
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
result, err := svc.List(tc.key, tc.filter, tc.offset, tc.limit)
|
||||
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.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,40 +492,40 @@ func TestRemove(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove removed config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove removed config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existing config",
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove non-existing config",
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,12 +538,16 @@ func TestBootstrap(t *testing.T) {
|
||||
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",
|
||||
@@ -553,6 +555,7 @@ func TestBootstrap(t *testing.T) {
|
||||
externalID: "invalid",
|
||||
externalKey: saved.ExternalKey,
|
||||
err: bootstrap.ErrNotFound,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap using invalid external key",
|
||||
@@ -560,6 +563,7 @@ func TestBootstrap(t *testing.T) {
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap an existing config",
|
||||
@@ -567,13 +571,22 @@ func TestBootstrap(t *testing.T) {
|
||||
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)
|
||||
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.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,49 +603,49 @@ func TestChangeState(t *testing.T) {
|
||||
desc string
|
||||
state bootstrap.State
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong credentials",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "change state of non-existing config",
|
||||
state: bootstrap.Active,
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to current state",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
state: bootstrap.Inactive,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.key, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,7 +682,7 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateChannelHandler(tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +714,7 @@ func TestRemoveChannelHandler(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveChannelHandler(tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,7 +733,7 @@ func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing conifg",
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
@@ -733,7 +746,7 @@ func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveConfigHandler(tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,7 +772,7 @@ func TestDisconnectThingsHandler(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect dicsonnected",
|
||||
desc: "disconnect disconnected",
|
||||
channelID: channel.ID,
|
||||
thingID: saved.MFThing,
|
||||
err: nil,
|
||||
@@ -768,6 +781,6 @@ func TestDisconnectThingsHandler(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
@@ -18,9 +14,9 @@ const (
|
||||
|
||||
// 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 |
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
|
||||
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
|
||||
// | Active | Thing is able to communicate using Mainflux |
|
||||
type State int
|
||||
|
||||
|
||||
@@ -1,483 +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 config'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/ConfigAuthorization"
|
||||
- $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/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 successfull removal the service will ensure
|
||||
that the removed config is disconnected from all of the Maifnlux channels.
|
||||
tags:
|
||||
- confgis
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
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/configs/certs/{thingKey}:
|
||||
put:
|
||||
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/ThingKey"
|
||||
- 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 unsucessfully 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
|
||||
ConfigAuthorization:
|
||||
name: configAuthorization
|
||||
description: Configuration external 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
|
||||
ThingKey:
|
||||
name: thingKey
|
||||
description: Unique Thing key.
|
||||
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 occured.
|
||||
|
||||
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:
|
||||
- confgis
|
||||
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 uinque 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.
|
||||
required:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
ConfigReq:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some uinque 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
|
||||
@@ -0,0 +1,53 @@
|
||||
# Certs Service
|
||||
Issues certificates for things. `Certs` service can create certificates to be used when `Mainflux` is deployed to support mTLS.
|
||||
Certificate service can create certificates in two modes:
|
||||
1. Development mode - to be used when no PKI is deployed, this works similar to the [make thing_cert](../docker/ssl/Makefile)
|
||||
2. PKI mode - certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
|
||||
|
||||
## Development mode
|
||||
If `MF_CERTS_VAULT_HOST` is empty than Development mode is on.
|
||||
|
||||
To issue a certificate:
|
||||
```bash
|
||||
|
||||
TOK=`curl -s --insecure -S -X POST http://localhost/tokens -H 'Content-Type: application/json' -d '{"email":"edge@email.com","password":"12345678"}' | jq -r '.token'`
|
||||
|
||||
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "rsa_bits":2048, "key_type":"rsa"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ThingID": "",
|
||||
"ClientCert": "-----BEGIN CERTIFICATE-----\nMIIDmTCCAoGgAwIBAgIRANmkAPbTR1UYeYO0Id/4+8gwDQYJKoZIhvcNAQELBQAw\nVzESMBAGA1UEAwwJbG9jYWxob3N0MREwDwYDVQQKDAhNYWluZmx1eDEMMAoGA1UE\nCwwDSW9UMSAwHgYJKoZIhvcNAQkBFhFpbmZvQG1haW5mbHV4LmNvbTAeFw0yMDA2\nMzAxNDIxMDlaFw0yMDA5MjMyMjIxMDlaMFUxETAPBgNVBAoTCE1haW5mbHV4MREw\nDwYDVQQLEwhtYWluZmx1eDEtMCsGA1UEAxMkYjAwZDBhNzktYjQ2YS00NTk3LTli\nNGYtMjhkZGJhNTBjYTYyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\ntgS2fLUWG3CCQz/l6VRQRJfRvWmdxK0mW6zIXGeeOILYZeaLiuiUnohwMJ4RiMqT\nuJbInAIuO/Tt5osfrCFFzPEOLYJ5nZBBaJfTIAxqf84Ou1oeMRll4wpzgeKx0rJO\nXMAARwn1bT9n3uky5QQGSLy4PyyILzSXH/1yCQQctdQB/Ar/UI1TaYoYlGzh7dHT\nWpcxq1HYgCyAtcrQrGD0rEwUn82UBCrnya+bygNqu0oDzIFQwa1G8jxSgXk0mFS1\nWrk7rBipsvp8HQhdnvbEVz4k4AAKcQxesH4DkRx/EXmU2UvN3XysvcJ2bL+UzMNI\njNhAe0pgPbB82F6zkYZ/XQIDAQABo2IwYDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0l\nBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQHBAUBAgMEBjAfBgNVHSME\nGDAWgBRs4xR91qEjNRGmw391xS7x6Tc+8jANBgkqhkiG9w0BAQsFAAOCAQEAW/dS\nV4vNLTZwBnPVHUX35pRFxPKvscY+vnnpgyDtITgZHYe0KL+Bs3IHuywtqaezU5x1\nkZo+frE1OcpRvp7HJtDiT06yz+18qOYZMappCWCeAFWtZkMhlvnm3TqTkgui6Xgl\nGj5xnPb15AOlsDE2dkv5S6kEwJGHdVX6AOWfB4ubUq5S9e4ABYzXGUty6Hw/ZUmJ\nhCTRVJ7cQJVTJsl1o7CYT8JBvUUG75LirtoFE4M4JwsfsKZXzrQffTf1ynqI3dN/\nHWySEbvTSWcRcA3MSmOTxGt5/zwCglHDlWPKMrXtjTW7NPuGL5/P9HSB9HGVVeET\nDUMdvYwgj0cUCEu3LA==\n-----END CERTIFICATE-----\n",
|
||||
"IssuingCA": "",
|
||||
"CAChain": null,
|
||||
"ClientKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtgS2fLUWG3CCQz/l6VRQRJfRvWmdxK0mW6zIXGeeOILYZeaL\niuiUnohwMJ4RiMqTuJbInAIuO/Tt5osfrCFFzPEOLYJ5nZBBaJfTIAxqf84Ou1oe\nMRll4wpzgeKx0rJOXMAARwn1bT9n3uky5QQGSLy4PyyILzSXH/1yCQQctdQB/Ar/\nUI1TaYoYlGzh7dHTWpcxq1HYgCyAtcrQrGD0rEwUn82UBCrnya+bygNqu0oDzIFQ\nwa1G8jxSgXk0mFS1Wrk7rBipsvp8HQhdnvbEVz4k4AAKcQxesH4DkRx/EXmU2UvN\n3XysvcJ2bL+UzMNIjNhAe0pgPbB82F6zkYZ/XQIDAQABAoIBAALoal3tqq+/iWU3\npR2oKiweXMxw3oNg3McEKKNJSH7QoFJob3xFoPIzbc9pBxCvY9LEHepYIpL0o8RW\nHqhqU6olg7t4ZSb+Qf1Ax6+wYxctnJCjrO3N4RHSfevqSjr6fEQBEUARSal4JNmr\n0hNUkCEjWrIvrPFMHsn1C5hXR3okJQpGsad4oCGZDp2eZ/NDyvmLBLci9/5CJdRv\n6roOF5ShWweKcz1+pfy666Q8RiUI7H1zXjPaL4yqkv8eg/WPOO0dYF2Ri2Grk9OY\n1qTM0W1vi9zfncinZ0DpgtwMTFQezGwhUyJHSYHmjVBA4AaYIyOQAI/2dl5fXM+O\n9JfXpOUCgYEA10xAtMc/8KOLbHCprpc4pbtOqfchq/M04qPKxQNAjqvLodrWZZgF\nexa+B3eWWn5MxmQMx18AjBCPwbNDK8Rkd9VqzdWempaSblgZ7y1a0rRNTXzN5DFP\noiuRQV4wszCuj5XSdPn+lxApaI/4+TQ0oweIZCpGW39XKePPoB5WZiMCgYEA2G3W\niJncRpmxWwrRPi1W26E9tWOT5s9wYgXWMc+PAVUd/qdDRuMBHpu861Qoghp/MJog\nBYqt2rQqU0OxvIXlXPrXPHXrCLOFwybRCBVREZrg4BZNnjyDTLOu9C+0M3J9ImCh\n3vniYqb7S0gRmoDM0R3Zu4+ajfP2QOGLXw1qHH8CgYEAl0EQ7HBW8V5UYzi7XNcM\nixKOb0YZt83DR74+hC6GujTjeLBfkzw8DX+qvWA8lxLIKVC80YxivAQemryv4h21\nX6Llx/nd1UkXUsI+ZhP9DK5y6I9XroseIRZuk/fyStFWsbVWB6xiOgq2rKkJBzqw\nCCEQpx40E6/gsqNDiIAHvvUCgYBkkjXc6FJ55DWMLuyozfzMtpKsVYeG++InSrsM\nDn1PizQS/7q9mAMPLCOP312rh5CPDy/OI3FCbfI1GwHerwG0QUP/bnQ3aOTBmKoN\n7YnsemIA/5w16bzBycWE5x3/wjXv4aOWr9vJJ/siMm0rtKp4ijyBcevKBxHpeGWB\nWAR1FQKBgGIqAxGnBpip9E24gH894BaGHHMpQCwAxARev6sHKUy27eFUd6ipoTva\n4Wv36iz3gxU4R5B0gyfnxBNiUab/z90cb5+6+FYO13kqjxRRZWffohk5nHlmFN9K\nea7KQHTfTdRhOLUzW2yVqLi9pzfTfA6Yqf3U1YD3bgnWrp1VQnjo\n-----END RSA PRIVATE KEY-----\n",
|
||||
"PrivateKeyType": "",
|
||||
"Serial": "",
|
||||
"Expire": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## PKI mode
|
||||
|
||||
When `MF_CERTS_VAULT_HOST` is set it is presumed that `Vault` is installed and `certs` service will issue certificates using `Vault` API.
|
||||
First you'll need to set up `Vault`.
|
||||
To setup `Vault` follow steps in [Build Your Own Certificate Authority (CA)](https://learn.hashicorp.com/tutorials/vault/pki-engine).
|
||||
|
||||
To setup certs service with `Vault` following environment variables must be set:
|
||||
|
||||
```
|
||||
MF_CERTS_VAULT_HOST=vault-domain.com
|
||||
MF_CERTS_VAULT_PKI_PATH=<vault_pki_path>
|
||||
MF_CERTS_VAULT_ROLE=<vault_role>
|
||||
MF_CERTS_VAULT_TOKEN=<vault_acces_token>
|
||||
```
|
||||
|
||||
For lab purposes you can use docker-compose and script for setting up PKI in [https://github.com/mteodor/vault](https://github.com/mteodor/vault)
|
||||
|
||||
Issuing certificate is same as in **Development** mode.
|
||||
In this mode certificates can also be revoked:
|
||||
|
||||
```bash
|
||||
curl -s -S -X DELETE http://localhost:8204/certs/revoke -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":"c30b8842-507c-4bcd-973c-74008cef3be5"}'
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of certs service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
)
|
||||
|
||||
func issueCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addCertsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.Valid, req.KeyBits, req.KeyType)
|
||||
if err != nil {
|
||||
return certsResponse{Error: err.Error()}, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listCerts(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.ListCerts(ctx, req.token, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return certsPageRes{
|
||||
Error: err.Error(),
|
||||
}, err
|
||||
}
|
||||
res := certsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Certs: []certsResponse{},
|
||||
}
|
||||
|
||||
for _, cert := range page.Certs {
|
||||
view := certsResponse{
|
||||
Serial: cert.Serial,
|
||||
ThingID: cert.ThingID,
|
||||
}
|
||||
res.Certs = append(res.Certs, view)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(revokeReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.RevokeCert(ctx, req.token, req.ThingID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc certs.Service, logger log.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (c certs.Cert, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method issue_cert for token: %s and thing: %s took %s to complete", token, 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.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (cp certs.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_certs for token: %s took %s to complete", token, 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.ListCerts(ctx, token, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RevokeCert(ctx context.Context, token, thingID string) (c certs.Revoke, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke_cert for token: %s and thing: %s took %s to complete", token, 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.RevokeCert(ctx, token, thingID)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metrics.Histogram) certs.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (certs.Cert, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_cert").Add(1)
|
||||
ms.latency.With("method", "issue_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (certs.Page, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_certs").Add(1)
|
||||
ms.latency.With("method", "list_certs").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListCerts(ctx, token, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RevokeCert(ctx context.Context, token, thingID string) (certs.Revoke, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_cert").Add(1)
|
||||
ms.latency.With("method", "revoke_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RevokeCert(ctx, token, thingID)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/certs"
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addCertsReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
Valid string `json:"valid"`
|
||||
}
|
||||
|
||||
func (req addCertsReq) validate() error {
|
||||
if req.ThingID == "" && req.token == "" {
|
||||
return errUnauthorized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
token string
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req *listReq) validate() error {
|
||||
if req.token == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.limit == 0 || req.limit > maxLimitSize {
|
||||
return certs.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
}
|
||||
|
||||
func (req *revokeReq) validate() error {
|
||||
if req.token == "" || req.ThingID == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
}
|
||||
|
||||
type certsPageRes struct {
|
||||
pageRes
|
||||
Certs []certsResponse `json:"certs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type certsResponse struct {
|
||||
ClientCert map[string]string `json:"client_cert"`
|
||||
ClientKey map[string]string `json:"client_key"`
|
||||
Serial string `json:"serial"`
|
||||
ThingID string `json:"thing_id"`
|
||||
CACert string `json:"ca_cert"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (res certsPageRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (res certsResponse) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certsResponse) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certsResponse) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
offset = "offset"
|
||||
limit = "limit"
|
||||
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errUnauthorized = errors.New("missing or invalid credentials provided")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errMalformedEntity = errors.New("malformed entity")
|
||||
errConflict = errors.New("entity already exists")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc certs.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/certs", kithttp.NewServer(
|
||||
issueCert(svc),
|
||||
decodeCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/certs", kithttp.NewServer(
|
||||
listCerts(svc),
|
||||
decodeListCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/certs/revoke", kithttp.NewServer(
|
||||
revokeCert(svc),
|
||||
decodeRevokeCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.GetFunc("/version", mainflux.Version("certs"))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
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 decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := readUintQuery(r, limit, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o, err := readUintQuery(r, offset, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := listReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
limit: l,
|
||||
offset: o,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
strval := vals[0]
|
||||
val, err := strconv.ParseUint(strval, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addCertsReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := revokeReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case io.EOF, errMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import "context"
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list
|
||||
type Page struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Certs []Cert
|
||||
}
|
||||
|
||||
// Repository specifies a Config persistence API.
|
||||
type Repository interface {
|
||||
// Save saves cert for thing into database
|
||||
Save(ctx context.Context, cert Cert) (string, error)
|
||||
|
||||
// RetrieveAll retrieve all issued certificates for given owner
|
||||
RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// Remove certificate from DB for given thing
|
||||
Remove(ctx context.Context, thingID string) error
|
||||
|
||||
// RetrieveByThing certificate by given thing
|
||||
RetrieveByThing(ctx context.Context, thingID string) (Cert, error)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package certs contains the domain concept definitions needed to support
|
||||
// Mainflux certs service functionality.
|
||||
package certs
|
||||
@@ -0,0 +1,144 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Certs service
|
||||
description: HTTP API for Certs service
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/certs:
|
||||
post:
|
||||
summary: Creates a certificate for thing
|
||||
description: Creates a certificate for thing
|
||||
tags:
|
||||
- Thing to proxy
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
201:
|
||||
description: Created
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
500:
|
||||
description: Unexpected server-side error ocurred.
|
||||
/certs/{thingID}:
|
||||
get:
|
||||
summary: Retrieves certificates
|
||||
description: |
|
||||
Retrieves a certificates for given thing ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/CertsRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/certs/revoke:
|
||||
delete:
|
||||
summary: Revokes certificate
|
||||
description: |
|
||||
Revokes a certificates for given thing ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token. Used instead of credentials in env or config.toml.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
ThingID:
|
||||
name: thingID
|
||||
description: Thing ID
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Certs:
|
||||
type: object
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
issuing_ca:
|
||||
type: string
|
||||
description: CA Certificate that is used to issue client certs, usually intermediate.
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Revoke:
|
||||
type: object
|
||||
properties:
|
||||
revocation_time:
|
||||
type: string
|
||||
description: Certificate revocation time
|
||||
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- thing_id
|
||||
- days_valid
|
||||
- rsa_bits
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
days_valid:
|
||||
type: string
|
||||
rsa_bits:
|
||||
type: integer
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
CertsRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Certs"
|
||||
RevokeRes:
|
||||
description: Certificate revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revoke"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import "time"
|
||||
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
type Agent interface {
|
||||
// IssueCert issues certificate on PKI
|
||||
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
|
||||
// Revoke revokes certificate from PKI
|
||||
Revoke(serial string) (Revoke, error)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
issue = "issue"
|
||||
revoke = "revoke"
|
||||
apiVer = "v1"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedVaultCertIssue = errors.New("failed to issue vault certificate")
|
||||
errFailedCertDecoding = errors.New("failed to decode response from vault service")
|
||||
)
|
||||
|
||||
type pkiAgent struct {
|
||||
token string
|
||||
path string
|
||||
role string
|
||||
host string
|
||||
issueURL string
|
||||
revokeURL string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
type certReq struct {
|
||||
CommonName string `json:"common_name"`
|
||||
TTL string `json:"ttl"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
}
|
||||
|
||||
type certRevokeReq struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
}
|
||||
|
||||
func NewVaultClient(token, host, path, role string) (Agent, error) {
|
||||
conf := &api.Config{
|
||||
Address: host,
|
||||
}
|
||||
|
||||
client, err := api.NewClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.SetToken(token)
|
||||
p := pkiAgent{
|
||||
token: token,
|
||||
host: host,
|
||||
role: role,
|
||||
path: path,
|
||||
client: client,
|
||||
issueURL: "/" + apiVer + "/" + path + "/" + issue + "/" + role,
|
||||
revokeURL: "/" + apiVer + "/" + path + "/" + revoke,
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error) {
|
||||
cReq := certReq{
|
||||
CommonName: cn,
|
||||
TTL: ttl,
|
||||
KeyBits: keyBits,
|
||||
KeyType: keyType,
|
||||
}
|
||||
|
||||
r := p.client.NewRequest("POST", p.issueURL)
|
||||
if err := r.SetJSONBody(cReq); err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
return Cert{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, _ := api.ParseSecret(resp.Body)
|
||||
cert := Cert{}
|
||||
|
||||
if err = mapstructure.Decode(s.Data, &cert); err != nil {
|
||||
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
|
||||
}
|
||||
|
||||
// Expire time calc must be revised value doesnt look correct
|
||||
exp, err := s.Data["expiration"].(json.Number).Float64()
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
expTime := time.Unix(0, int64(exp)*int64(time.Millisecond))
|
||||
cert.Expire = expTime
|
||||
return cert, nil
|
||||
|
||||
}
|
||||
|
||||
func (p *pkiAgent) Revoke(serial string) (Revoke, error) {
|
||||
cReq := certRevokeReq{
|
||||
SerialNumber: serial,
|
||||
}
|
||||
|
||||
r := p.client.NewRequest("POST", p.revokeURL)
|
||||
if err := r.SetJSONBody(cReq); err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
return Revoke{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, err := api.ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
rev, err := s.Data["revocation_time"].(json.Number).Float64()
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
revTime := time.Unix(0, int64(rev)*int64(time.Millisecond))
|
||||
return Revoke{
|
||||
RevocationTime: revTime,
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
const duplicateErr = "unique_violation"
|
||||
|
||||
var (
|
||||
errSaveDB = errors.New("failed to save certificate to database")
|
||||
errRetrieveDB = errors.New("failed to retrieve certificate from db")
|
||||
errRemove = errors.New("failed to remove certificate from database")
|
||||
errInvalid = "invalid_text_representation"
|
||||
)
|
||||
|
||||
var _ certs.Repository = (*certsRepository)(nil)
|
||||
|
||||
type Cert struct {
|
||||
ThingID string
|
||||
Serial string
|
||||
Expire time.Time
|
||||
}
|
||||
|
||||
type certsRepository struct {
|
||||
db *sqlx.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewRepository instantiates a PostgreSQL implementation of certs
|
||||
// repository.
|
||||
func NewRepository(db *sqlx.DB, log logger.Logger) certs.Repository {
|
||||
return &certsRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 ORDER BY expire LIMIT $2 OFFSET $3;`
|
||||
rows, err := cr.db.Query(q, ownerID, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
certificates := []certs.Cert{}
|
||||
|
||||
for rows.Next() {
|
||||
c := certs.Cert{}
|
||||
if err := rows.Scan(&c.ThingID, &c.OwnerID, &c.Serial, &c.Expire); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return certs.Page{}, err
|
||||
|
||||
}
|
||||
certificates = append(certificates, c)
|
||||
}
|
||||
|
||||
q = `SELECT COUNT(*) FROM certs WHERE owner_id = $1`
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q, ownerID).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count certs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
|
||||
return certs.Page{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Certs: certificates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, error) {
|
||||
q := `INSERT INTO certs (thing_id, owner_id, serial, expire) VALUES (:thing_id, :owner_id, :serial, :expire)`
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
}
|
||||
|
||||
dbcrt := toDBCert(cert)
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcrt); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = errors.New("error conflict")
|
||||
}
|
||||
|
||||
cr.rollback("Failed to insert a Cert", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
}
|
||||
|
||||
return cert.Serial, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Remove(ctx context.Context, serial string) error {
|
||||
if _, err := cr.retrieveBySerial(ctx, serial); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
q := `DELETE FROM certs WHERE serial = :serial`
|
||||
var c certs.Cert
|
||||
c.Serial = serial
|
||||
dbcrt := toDBCert(c)
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcrt); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE thing_id = $1`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, thingID).StructScan(&dbcrt); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return c, errors.Wrap(things.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) retrieveBySerial(ctx context.Context, serial string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE serial = $1`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, serial).StructScan(&dbcrt); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return c, errors.Wrap(things.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) 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))
|
||||
}
|
||||
}
|
||||
|
||||
type dbCert struct {
|
||||
ThingID string `db:"thing_id"`
|
||||
Serial string `db:"serial"`
|
||||
Expire time.Time `db:"expire"`
|
||||
OwnerID string `db:"owner_id"`
|
||||
}
|
||||
|
||||
func toDBCert(c certs.Cert) dbCert {
|
||||
return dbCert{
|
||||
ThingID: c.ThingID,
|
||||
OwnerID: c.OwnerID,
|
||||
Serial: c.Serial,
|
||||
Expire: c.Expire,
|
||||
}
|
||||
}
|
||||
|
||||
func toCert(cdb dbCert) certs.Cert {
|
||||
var c certs.Cert
|
||||
c.OwnerID = cdb.OwnerID
|
||||
c.ThingID = cdb.ThingID
|
||||
c.Serial = cdb.Serial
|
||||
c.Expire = cdb.Expire
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
const primaryKey = "primary_key"
|
||||
|
||||
// ErrMigrate indicates error during database migrations.
|
||||
var ErrMigrate = errors.New("error executing database migrations")
|
||||
|
||||
// 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 {
|
||||
mErr, ok := err.(*migrate.TxError)
|
||||
if ok && mErr.Migration.Id == primaryKey {
|
||||
return db, ErrMigrate
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sqlx.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "certs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS certs (
|
||||
thing_id TEXT NOT NULL,
|
||||
owner_id TEXT NOT NULL,
|
||||
expire TIMESTAMPTZ NOT NULL,
|
||||
serial TEXT NOT NULL,
|
||||
PRIMARY KEY (thing_id, owner_id)
|
||||
);`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE IF EXISTS certs;",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/certs/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = logger.New(os.Stdout, logger.Info.String())
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("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 {
|
||||
testLog.Error(fmt.Sprintf("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 {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/certs/pki"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/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")
|
||||
|
||||
errFailedKeyCreation = errors.New("failed to create client private key")
|
||||
errFailedDateSetting = errors.New("failed to set date for certificate")
|
||||
errKeyBitsValueWrong = errors.New("missing RSA bits for certificate creation")
|
||||
errMissingCACertificate = errors.New("missing CA certificate for certificate signing")
|
||||
errFailedSerialGeneration = errors.New("failed to generate certificate serial")
|
||||
errFailedPemKeyWrite = errors.New("failed to write PEM key")
|
||||
errFailedPemDataWrite = errors.New("failed to write pem data for certificate")
|
||||
errPrivateKeyUnsupportedType = errors.New("private key type is unsupported")
|
||||
errPrivateKeyEmpty = errors.New("private key is empty")
|
||||
errFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
|
||||
errFailedCertCreation = errors.New("failed to create client certificate")
|
||||
errFailedCertRevocation = errors.New("failed to revoke certificate")
|
||||
)
|
||||
|
||||
var _ Service = (*certsService)(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 {
|
||||
// IssueCert issues certificate for given thing id if access is granted with token
|
||||
IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (Cert, error)
|
||||
|
||||
// ListCerts lists all certificates issued for given owner
|
||||
ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error)
|
||||
|
||||
// RevokeCert revokes certificate for given thing
|
||||
RevokeCert(ctx context.Context, token, thingID string) (Revoke, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
LogLevel string
|
||||
ClientTLS bool
|
||||
CaCerts string
|
||||
HTTPPort string
|
||||
ServerCert string
|
||||
ServerKey string
|
||||
BaseURL string
|
||||
ThingsPrefix string
|
||||
JaegerURL string
|
||||
AuthnURL string
|
||||
AuthnTimeout time.Duration
|
||||
SignTLSCert tls.Certificate
|
||||
SignX509Cert *x509.Certificate
|
||||
SignRSABits int
|
||||
SignHoursValid string
|
||||
PKIHost string
|
||||
PKIPath string
|
||||
PKIRole string
|
||||
PKIToken string
|
||||
}
|
||||
|
||||
type certsService struct {
|
||||
auth mainflux.AuthNServiceClient
|
||||
certsRepo Repository
|
||||
sdk mfsdk.SDK
|
||||
conf Config
|
||||
pki pki.Agent
|
||||
}
|
||||
|
||||
// New returns new Certs service.
|
||||
func New(auth mainflux.AuthNServiceClient, certs Repository, sdk mfsdk.SDK, config Config, pki pki.Agent) Service {
|
||||
return &certsService{
|
||||
certsRepo: certs,
|
||||
sdk: sdk,
|
||||
auth: auth,
|
||||
conf: config,
|
||||
pki: pki,
|
||||
}
|
||||
}
|
||||
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
OwnerID string `json:"owner_id" mapstructure:"owner_id"`
|
||||
ThingID string `json:"thing_id" mapstructure:"thing_id"`
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
|
||||
var c Cert
|
||||
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return c, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
// If PKIHost is not set we don't use 3rd party PKI service.
|
||||
if cs.conf.PKIHost == "" {
|
||||
c.ClientCert, c.ClientKey, err = cs.certs(thing.Key, daysValid, keyBits)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
cert, err := cs.pki.IssueCert(thingID, daysValid, keyType, keyBits)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
c.ThingID = thingID
|
||||
c.OwnerID = owner.GetValue()
|
||||
c.ClientCert = cert.ClientCert
|
||||
c.IssuingCA = cert.IssuingCA
|
||||
c.CAChain = cert.CAChain
|
||||
c.ClientKey = cert.ClientKey
|
||||
c.PrivateKeyType = cert.PrivateKeyType
|
||||
c.Serial = cert.Serial
|
||||
c.Expire = cert.Expire
|
||||
|
||||
_, err = cs.certsRepo.Save(context.Background(), c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
|
||||
var revoke Revoke
|
||||
_, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
cert, err := cs.certsRepo.RetrieveByThing(ctx, thing.ID)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
r, err := cs.pki.Revoke(cert.Serial)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
revoke.RevocationTime = r.RevocationTime
|
||||
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
|
||||
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
|
||||
}
|
||||
return revoke, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return cs.certsRepo.RetrieveAll(ctx, u.GetValue(), offset, limit)
|
||||
}
|
||||
|
||||
func (cs *certsService) certs(thingKey, daysValid string, keyBits int) (string, string, error) {
|
||||
if cs.conf.SignX509Cert == nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, errMissingCACertificate)
|
||||
}
|
||||
if keyBits == 0 {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, errKeyBitsValueWrong)
|
||||
}
|
||||
var priv interface{}
|
||||
priv, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedKeyCreation, err)
|
||||
}
|
||||
|
||||
if daysValid == "" {
|
||||
daysValid = cs.conf.SignHoursValid
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
validFor, err := time.ParseDuration(daysValid)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedDateSetting, err)
|
||||
}
|
||||
notAfter := notBefore.Add(validFor)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedSerialGeneration, err)
|
||||
}
|
||||
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Mainflux"},
|
||||
CommonName: thingKey,
|
||||
OrganizationalUnit: []string{"mainflux"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||
}
|
||||
|
||||
pubKey, err := publicKey(priv)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, cs.conf.SignX509Cert, pubKey, cs.conf.SignTLSCert.PrivateKey)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
var bw, keyOut bytes.Buffer
|
||||
buffWriter := bufio.NewWriter(&bw)
|
||||
buffKeyOut := bufio.NewWriter(&keyOut)
|
||||
|
||||
if err := pem.Encode(buffWriter, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemDataWrite, err)
|
||||
}
|
||||
buffWriter.Flush()
|
||||
cert := bw.String()
|
||||
|
||||
block, err := pemBlockForKey(priv)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
||||
}
|
||||
if err := pem.Encode(buffKeyOut, block); err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
||||
}
|
||||
buffKeyOut.Flush()
|
||||
key := keyOut.String()
|
||||
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
func publicKey(priv interface{}) (interface{}, error) {
|
||||
if priv == nil {
|
||||
return nil, errPrivateKeyEmpty
|
||||
}
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return nil, errPrivateKeyUnsupportedType
|
||||
}
|
||||
}
|
||||
|
||||
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
b, err := x509.MarshalECPrivateKey(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
+159
-25
@@ -1,108 +1,242 @@
|
||||
# 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
|
||||
```
|
||||
mainflux-cli users create john.doe@email.com password
|
||||
```bash
|
||||
mainflux-cli users create <user_email> <user_password>
|
||||
```
|
||||
|
||||
#### Login User
|
||||
```bash
|
||||
mainflux-cli users token <user_email> <user_password>
|
||||
```
|
||||
mainflux-cli users token john.doe@email.com 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 (type Device)
|
||||
```
|
||||
mainflux-cli things create '{"name":"myDevice"}' <user_auth_token>
|
||||
#### Create Thing
|
||||
```bash
|
||||
mainflux-cli things create '{"name":"myThing"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Create Thing (type Application)
|
||||
#### Create Thing with metadata
|
||||
```bash
|
||||
mainflux-cli things create '{"name":"myThing", "metadata": {\"key1\":\"value1\"}}' <user_auth_token>
|
||||
```
|
||||
mainflux-cli things create '{"name":"myDevice"}' <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>
|
||||
```
|
||||
|
||||
#### Update Channel
|
||||
#### Bulk Provision Channels
|
||||
```bash
|
||||
mainflux-cli provision channels <file> <user_auth_token>
|
||||
```
|
||||
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <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>
|
||||
```
|
||||
|
||||
#### Disconnect Thing from Channel
|
||||
#### 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>
|
||||
```
|
||||
|
||||
### Groups
|
||||
#### Create new group
|
||||
```bash
|
||||
mainflux-cli groups create '{"name":"<group_name>","parent_id":"<parent_group_id>","description":"<description>","metadata":{"key":"value",...}}' <user_auth_token>
|
||||
```
|
||||
#### Delete group
|
||||
```bash
|
||||
mainflux-cli groups delete <group_id> <user_auth_token>
|
||||
```
|
||||
#### Get group with id
|
||||
```bash
|
||||
mainflux-cli groups get <group_id> <user_auth_token>
|
||||
```
|
||||
#### List all groups
|
||||
```bash
|
||||
mainflux-cli groups get all <user_auth_token>
|
||||
```
|
||||
#### List children groups for some group
|
||||
```bash
|
||||
mainflux-cli groups get children <parent_group_id> <user_auth_token>
|
||||
```
|
||||
#### Assign user to a group
|
||||
```bash
|
||||
mainflux-cli groups assign <user_id> <group_id> <user_auth_token>
|
||||
```
|
||||
#### Unassign user from group
|
||||
```bash
|
||||
mainflux-cli groups unassign <user_id> <group_id> <user_auth_token>
|
||||
```
|
||||
#### List users for a group
|
||||
```bash
|
||||
mainflux-cli groups members <group_id> <user_auth_token>
|
||||
```
|
||||
#### List groups that user belongs to
|
||||
```bash
|
||||
mainflux-cli groups membership <user_id> <user_auth_token>
|
||||
```
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user