Compare commits

..

144 Commits

Author SHA1 Message Date
dborovcanin 0ada09577e deploy: 168e8b90cb 2026-07-30 15:56:34 +00:00
dborovcanin 7a747fade7 deploy: 16ba29cf4a 2026-06-26 20:07:08 +00:00
dborovcanin 2d2d23b3ce deploy: 7f03134d8e 2026-05-08 08:35:18 +00:00
dborovcanin f18e96e990 deploy: 61d0427898 2026-04-06 13:23:53 +00:00
dborovcanin e95e0e4472 deploy: 791e084de6 2026-04-03 16:37:10 +00:00
dborovcanin 4f18a129b3 deploy: ef5c253c51 2026-04-01 07:55:20 +00:00
dborovcanin b78b1eb706 deploy: f8410b8940 2026-03-04 11:37:45 +00:00
dborovcanin ee2c0435fc deploy: 9c2608659f 2026-03-03 14:22:39 +00:00
dborovcanin 8fde65fee0 deploy: 2c476c17ee 2026-03-03 14:14:14 +00:00
dborovcanin 02b2facbb0 deploy: 4e77ae65e2 2026-02-04 16:14:00 +00:00
dborovcanin 01125764fb deploy: 59d9f91813 2026-01-23 13:43:06 +00:00
dborovcanin 41c9067328 deploy: 5b913dd46b 2026-01-22 07:55:36 +00:00
dborovcanin 7a6241a80d deploy: 28fc9738df 2026-01-19 11:10:56 +00:00
dborovcanin 7b1267e394 deploy: 67c28ff134 2025-12-31 09:44:02 +00:00
dborovcanin 2ae21cb38a deploy: 3e6b771850 2025-12-01 23:13:52 +00:00
dborovcanin 2380af8a85 deploy: 15e756a5a3 2025-12-01 17:53:16 +00:00
dborovcanin e289794cfa deploy: aa3cc3d7ee 2025-10-30 17:16:51 +00:00
Dušan Borovčanin 1a977ff4b1 Create CNAME 2025-10-30 18:07:14 +01:00
Dušan Borovčanin 3a0a958d22 Delete CNAME 2025-10-30 18:06:11 +01:00
Dušan Borovčanin 1c0b6623ad Update CNAME 2025-10-30 18:02:56 +01:00
dborovcanin 461f464329 deploy: 9650dde716 2025-10-30 16:37:20 +00:00
dborovcanin 391203effd deploy: 3e400da5ad 2025-10-15 08:26:10 +00:00
Dušan Borovčanin 86a63dce92 Update CNAME 2025-10-14 12:39:02 +02:00
dborovcanin d02df10d33 deploy: ac9bc9aad6 2025-10-06 08:49:10 +00:00
Dušan Borovčanin 9849b60d68 Update CNAME 2025-10-02 16:14:56 +02:00
dborovcanin fa2d4c13f4 deploy: ae7c93020a 2025-10-01 09:08:51 +00:00
dborovcanin dd9d6f0a88 deploy: b031dc08b8 2025-09-26 11:55:13 +00:00
dborovcanin d12b1b164a deploy: 61f1e9aca2 2025-09-18 10:18:17 +00:00
dborovcanin 88f5a60ebb deploy: e57ad79cd4 2025-09-05 13:24:25 +00:00
dborovcanin 7d6a8998b8 deploy: ec75f4eadb 2025-09-02 08:17:07 +00:00
dborovcanin bcf6eb5449 deploy: 0e1a090f16 2025-08-25 14:17:44 +00:00
dborovcanin 0c355c69f0 deploy: 4927333691 2025-08-20 13:57:11 +00:00
drasko f2eb2f76ef deploy: 8188da16b3 2025-08-13 12:48:29 +00:00
dborovcanin e72f7e700f deploy: 8e70e1113e 2025-07-28 10:04:13 +00:00
dborovcanin 56c5e6acc7 deploy: fa5b6b86c6 2025-07-07 08:43:27 +00:00
dborovcanin 639a945777 deploy: 16f1b411ec 2025-07-07 08:23:40 +00:00
dborovcanin 16471f4ab1 deploy: f0d014eba2 2024-12-19 17:30:11 +00:00
dborovcanin c2731d34b2 deploy: 9e6da193c8 2024-12-19 11:39:58 +00:00
dborovcanin f9383cd8b0 deploy: 901ed73fc9 2024-12-11 14:23:19 +00:00
dborovcanin b2e067341c deploy: cff356461d 2024-12-11 08:15:23 +00:00
dborovcanin cc99241a2b deploy: 1e77579b33 2024-12-10 10:56:03 +00:00
dborovcanin c9b214be86 deploy: 178aa46ad4 2024-12-04 10:17:32 +00:00
dborovcanin 7cef070f47 deploy: 10bc218dee 2024-12-03 16:18:20 +00:00
Dušan Borovčanin 375d6a83d7 Create CNAME 2024-12-03 16:26:24 +01:00
Dušan Borovčanin 11791c9582 Delete CNAME 2024-12-03 16:26:14 +01:00
Dušan Borovčanin 3c2a7c00ac Update CNAME 2024-12-03 16:25:47 +01:00
dborovcanin 6d1136c0a0 deploy: 252113aad6721729a3f0c47b711d4082484ca702 2024-12-03 15:01:05 +00:00
Dušan Borovčanin 7153e8aa4a Update CNAME 2024-12-03 15:54:59 +01:00
dborovcanin 7ef8d6f101 deploy: da1717cba3 2024-12-02 11:42:57 +00:00
dborovcanin 5c3d74a7e7 deploy: bb5f21b1f5 2024-11-26 20:20:17 +00:00
Dušan Borovčanin a3121a6850 Delete CNAME 2024-11-26 17:13:59 +01:00
dborovcanin ff0fba9914 deploy: 8d4e752274 2024-11-26 08:36:46 +00:00
dborovcanin 3159cfc32a deploy: b20b45023d 2024-11-25 12:02:44 +00:00
dborovcanin cc45fe5006 deploy: 3a02788e3a 2024-11-10 18:00:04 +00:00
dborovcanin d42287f43a deploy: a5059a7946 2024-11-08 17:31:19 +00:00
dborovcanin 96144cf37b deploy: 233bbf9861 2024-11-04 08:42:27 +00:00
dborovcanin 456adc3a6a deploy: 0019f71b46 2024-10-30 18:19:55 +00:00
dborovcanin 3cffdfe0c4 deploy: f88e11bdb2 2024-10-25 12:42:08 +00:00
dborovcanin 052db951e3 deploy: ca8ed3b006 2024-10-23 19:40:30 +00:00
dborovcanin f47ac0753e deploy: eb881690c7 2024-10-17 14:25:10 +00:00
dborovcanin 161ca011ee deploy: fdaecf54e5 2024-09-12 12:45:38 +00:00
dborovcanin f3bf4af82f deploy: 48c4a7e7d4 2024-09-03 14:50:49 +00:00
dborovcanin 53c95f1425 deploy: c398908d50 2024-07-22 11:13:06 +00:00
dborovcanin 96ea848b4f deploy: b49a2cd012 2024-07-09 11:03:28 +00:00
dborovcanin d9cbba5f23 deploy: 4e2ae73fb0 2024-07-02 20:50:09 +00:00
dborovcanin 5b99f1f137 deploy: 2fe2f850c2 2024-06-28 09:13:27 +00:00
dborovcanin 6641438d2a deploy: 0794363a3c 2024-06-27 14:38:41 +00:00
dborovcanin 3f8e14c593 deploy: b0e37dacf4 2024-06-26 20:12:18 +00:00
dborovcanin 93086ce204 deploy: 3355332ebd 2024-06-12 14:19:30 +00:00
dborovcanin dc41f3a5f1 deploy: 27f5fb8688 2024-06-12 10:07:56 +00:00
dborovcanin ef58136b85 deploy: d62982b4b6 2024-05-20 10:31:37 +00:00
dborovcanin 045517783f deploy: e72e8a663c5d1f8a889e8d44b4acc59dee6573aa 2024-05-20 10:05:56 +00:00
dborovcanin 98a21ad6d1 deploy: 75db28c522 2024-05-10 13:09:38 +00:00
dborovcanin 450dc6320b deploy: 19c1261e81 2024-04-29 14:08:14 +00:00
dborovcanin 5ebd3710e3 deploy: 07635ad828 2024-04-23 08:58:21 +00:00
dborovcanin 44a7e1cc89 deploy: 66c737a22d 2024-04-18 15:21:46 +00:00
dborovcanin b2f345f07c deploy: 54e322ee3e 2024-04-16 12:54:52 +00:00
dborovcanin 58670a8286 deploy: f334ee2606 2024-04-16 08:39:51 +00:00
dborovcanin 854b37724a deploy: c10c453eac 2024-04-12 15:41:04 +00:00
dborovcanin df182e3623 deploy: 3d0678725e 2024-04-12 15:05:24 +00:00
dborovcanin fad9f86a7f deploy: 76788d3ae2 2024-04-11 07:58:45 +00:00
dborovcanin 2a312d8b4b deploy: 658003080e 2024-04-10 10:02:53 +00:00
dborovcanin d736867bb5 deploy: e803f36f78 2024-04-10 08:19:12 +00:00
dborovcanin bd9dade57d deploy: 33704abc0c 2024-04-09 08:09:43 +00:00
dborovcanin 937231fe2d deploy: 5c62e339c1 2024-04-08 17:18:48 +00:00
dborovcanin 3043d938c1 deploy: eb6b201a2a 2024-04-03 11:01:58 +00:00
dborovcanin ecf9bb3dc1 deploy: a39d46238b 2024-04-03 09:55:26 +00:00
dborovcanin 0169fe9653 deploy: 65941368b8 2024-04-01 10:38:34 +00:00
Dušan Borovčanin 7b640b9d36 Update CNAME 2024-03-27 17:15:53 +01:00
dborovcanin 714f621be8 deploy: 70c3d65863 2024-03-22 15:16:24 +00:00
dborovcanin ad4cfc64ee deploy: 163ccbbe59 2024-03-08 09:46:07 +00:00
Dušan Borovčanin 296a315369 Update CNAME 2024-01-25 16:25:18 +01:00
Dušan Borovčanin 589c6d290b Update CNAME 2024-01-25 14:39:31 +01:00
drasko 4bd83baad5 deploy: cd82cc5a43 2023-10-17 13:38:29 +00:00
drasko bbbfc67c66 deploy: 09236e87a7 2023-10-16 11:43:27 +00:00
drasko 518495dfc7 deploy: 3d3aa525a6 2023-10-15 20:02:38 +00:00
drasko 0e4f98c0dc deploy: 896a74ad49 2023-08-09 10:26:46 +00:00
drasko 79fed471e3 deploy: 20fde26bbd 2023-08-01 15:21:23 +00:00
drasko 9d41c15920 deploy: b7b14cc8b6 2023-07-31 12:17:43 +00:00
dborovcanin c27a1cb982 deploy: aee0081864 2023-07-13 14:28:26 +00:00
drasko 321cb1d1dd deploy: 892015a60f 2023-07-07 09:15:17 +00:00
drasko 6a31b3c3bc deploy: 55e09c1921 2023-06-14 10:41:06 +00:00
drasko 9ff6dd25c0 deploy: 7cc1dd9f89 2023-05-24 22:13:57 +00:00
drasko a5cac6d108 deploy: 408eabaaa6 2023-05-24 22:03:10 +00:00
drasko 99e1f52c23 deploy: 7948aa72e8 2023-04-20 19:53:25 +00:00
drasko f4da6959d2 deploy: 5f9b3c9905 2023-01-24 14:10:53 +00:00
drasko e87b5f89ba deploy: d008ae5d97 2023-01-13 13:33:30 +00:00
dborovcanin 46ed0cc584 deploy: 47fd646030 2022-12-07 11:31:28 +00:00
dborovcanin 9e297c4a75 deploy: 8a4cc12cd3 2022-09-16 12:07:37 +00:00
drasko db09b536bf deploy: 721ee545f9 2022-08-11 16:59:19 +00:00
dusanb94 9e0226eda6 deploy: ce46723a8f 2022-03-30 09:19:33 +00:00
manuio 4629441c1f deploy: ce46723a8f 2022-03-29 09:53:54 +00:00
dusanb94 8502263d1f deploy: 29f9cbb10a 2022-03-23 12:20:27 +00:00
dusanb94 c5203886a1 deploy: 127044efad 2022-03-14 10:03:35 +00:00
drasko 0e0ff18466 deploy: f3ed852b36 2022-03-06 00:59:13 +00:00
dusanb94 3211e79a29 deploy: e5278c463f 2022-03-03 16:14:21 +00:00
drasko f1da6fb601 deploy: cc5d5195ab 2022-03-02 22:38:53 +00:00
drasko da4471eb63 deploy: 655ac838be 2022-02-09 20:26:01 +00:00
dusanb94 017ca584aa deploy: 1f8a221c22 2022-02-07 12:52:09 +00:00
drasko ea9d5857e7 deploy: 9e0947a355 2022-02-01 16:33:56 +00:00
dusanb94 74161fa2fd deploy: 81b2a1a2d3 2022-01-27 10:09:06 +00:00
manuio b4edae0070 deploy: 89061d33e5 2022-01-26 09:33:36 +00:00
drasko 077d9e53e6 deploy: 2abf9da27e 2022-01-25 18:43:16 +00:00
drasko 59c1ab659e deploy: 42dd813521 2022-01-24 20:19:19 +00:00
drasko b240a2dc2b deploy: bcc8cf7119 2022-01-19 15:46:42 +00:00
drasko f859efc984 deploy: bea09d97dd 2022-01-14 22:54:05 +00:00
drasko 9280d7b714 deploy: aa014c2191 2022-01-04 18:42:38 +00:00
drasko 8b1188a200 deploy: dd7d52ef10 2021-12-24 13:53:30 +00:00
drasko 13bf66ca8f deploy: b18c9e79dc 2021-12-14 10:17:50 +00:00
drasko 64bed5b859 deploy: 7e9ab453d4 2021-12-08 11:47:25 +00:00
drasko 79e3de997f deploy: 31d30b204d 2021-11-26 13:12:38 +00:00
drasko ed1b174a84 deploy: 3042d6b40b 2021-11-08 13:46:08 +00:00
drasko b9aee21eff deploy: b570c38ed0 2021-11-08 10:40:55 +00:00
drasko 73240ffedd deploy: 27d4646db4 2021-10-26 22:38:54 +00:00
drasko 7f7da3bb80 deploy: 5e9a91bd03 2021-10-15 22:26:47 +00:00
drasko e1fe70766e deploy: 94cba4aab2 2021-10-11 13:37:04 +00:00
drasko 641ada78da deploy: 66d3da0531 2021-10-06 12:34:51 +00:00
drasko 3b76ee386d deploy: 7b3c26f60f 2021-08-13 11:56:38 +00:00
drasko 56d8ff7a7c deploy: d73a5d53fe 2021-08-11 14:58:38 +00:00
drasko 276531fcf1 deploy: d6a3830ef4 2021-07-22 13:48:22 +00:00
dusanb94 2a3de350cc deploy: 5ac1203b55 2021-07-22 09:21:18 +00:00
drasko 04d614df43 deploy: 19f0437f57 2021-07-13 11:31:46 +00:00
drasko 2e49885404 deploy: af0162f0df 2021-07-08 11:52:03 +00:00
drasko 0e0a0b031c deploy: cddfdf4038 2021-06-23 11:38:02 +00:00
3383 changed files with 13462 additions and 806334 deletions
-6
View File
@@ -1,6 +0,0 @@
.git
.github
build
docker
metrics
scripts
-244
View File
@@ -1,244 +0,0 @@
# Docker: Environment variables in Compose
## NginX
MF_NGINX_HTTP_PORT=80
MF_NGINX_SSL_PORT=443
MF_NGINX_MQTT_PORT=1883
MF_NGINX_MQTTS_PORT=8883
## NATS
MF_NATS_URL=nats://nats:4222
## Redis
MF_REDIS_TCP_PORT=6379
## Grafana
MF_GRAFANA_PORT=3000
## Jaeger
MF_JAEGER_PORT=6831
MF_JAEGER_FRONTEND=16686
MF_JAEGER_COLLECTOR=14268
MF_JAEGER_CONFIGS=5778
MF_JAEGER_URL=jaeger:6831
## Core Services
### AuthN
MF_AUTHN_LOG_LEVEL=debug
MF_AUTHN_HTTP_PORT=8189
MF_AUTHN_GRPC_PORT=8181
MF_AUTHN_GRPC_URL=authn:8181
MF_AUTHN_GRPC_TIMEOUT=1
MF_AUTHN_DB_PORT=5432
MF_AUTHN_DB_USER=mainflux
MF_AUTHN_DB_PASS=mainflux
MF_AUTHN_DB=authn
MF_AUTHN_SECRET=secret
### Users
MF_USERS_LOG_LEVEL=debug
MF_USERS_HTTP_PORT=8180
MF_USERS_DB_PORT=5432
MF_USERS_DB_USER=mainflux
MF_USERS_DB_PASS=mainflux
MF_USERS_DB=users
### Email utility
MF_EMAIL_DRIVER=smtp
MF_EMAIL_HOST=smtp.mailtrap.io
MF_EMAIL_PORT=2525
MF_EMAIL_USERNAME=18bf7f70705139
MF_EMAIL_PASSWORD=2b0d302e775b1e
MF_EMAIL_FROM_ADDRESS=from@example.com
MF_EMAIL_FROM_NAME=Example
MF_EMAIL_TEMPLATE=email.tmpl
### Token utility
MF_TOKEN_RESET_ENDPOINT=/reset-request
### Things
MF_THINGS_LOG_LEVEL=debug
MF_THINGS_HTTP_PORT=8182
MF_THINGS_AUTH_HTTP_PORT=8989
MF_THINGS_AUTH_GRPC_PORT=8183
MF_THINGS_AUTH_GRPC_URL=things:8183
MF_THINGS_AUTH_GRPC_TIMEOUT=1
MF_THINGS_DB_PORT=5432
MF_THINGS_DB_USER=mainflux
MF_THINGS_DB_PASS=mainflux
MF_THINGS_DB=things
MF_THINGS_SECRET=secret
MF_THINGS_ES_URL=localhost:6379
MF_THINGS_ES_PASS=
MF_THINGS_ES_DB=0
### HTTP
MF_HTTP_ADAPTER_PORT=8185
### MQTT
MF_MQTT_ADAPTER_LOG_LEVEL=debug
MF_MQTT_ADAPTER_MQTT_PORT=1883
MF_MQTT_BROKER_PORT=1883
MF_MQTT_ADAPTER_WS_PORT=8080
MF_MQTT_BROKER_WS_PORT=8080
MF_MQTT_ADAPTER_ES_DB=0
MF_MQTT_ADAPTER_ES_PASS=
### VERMEMQ
MF_DOCKER_VERNEMQ_ALLOW_ANONYMOUS=on
MF_DOCKER_VERNEMQ_LOG__CONSOLE__LEVEL=error
### CoAP
MF_COAP_ADAPTER_LOG_LEVEL=debug
MF_COAP_ADAPTER_PORT=5683
## Addons Services
### Bootstrap
MF_BOOTSTRAP_LOG_LEVEL=debug
MF_BOOTSTRAP_PORT=8202
MF_BOOTSTRAP_DB_PORT=5432
MF_BOOTSTRAP_DB_USER=mainflux
MF_BOOTSTRAP_DB_PASS=mainflux
MF_BOOTSTRAP_DB=bootstrap
MF_BOOTSTRAP_DB_SSL_MODE=disable
### Provision
MF_PROVISION_CONFIG_FILE=/configs/config.toml
MF_PROVISION_LOG_LEVEL=debug
MF_PROVISION_HTTP_PORT=8190
MF_PROVISION_ENV_CLIENTS_TLS=false
MF_PROVISION_CA_CERTS=
MF_PROVISION_SERVER_CERT=
MF_PROVISION_SERVER_KEY=
MF_PROVISION_MQTT_URL=tcp://localhost
MF_PROVISION_USERS_LOCATION=http://localhost:8180
MF_PROVISION_THINGS_LOCATION=http://things:8182
MF_PROVISION_USER=
MF_PROVISION_PASS=
MF_PROVISION_API_KEY=
MF_PROVISION_CERTS_SVC_URL=http://localhost/certs
MF_PROVISION_X509_PROVISIONING=false
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things/configs
MF_PROVISION_BS_SVC_WHITELIST_URL=http://bootstrap:8202/things/state
MF_PROVISION_BS_CONFIG_PROVISIONING=true
MF_PROVISION_BS_AUTO_WHITELIST=true
MF_PROVISION_BS_CONTENT=
### LoRa
MF_LORA_ADAPTER_LOG_LEVEL=debug
MF_LORA_ADAPTER_MESSAGES_URL=tcp://lora.mqtt.mainflux.io:1883
MF_LORA_ADAPTER_HTTP_PORT=8187
MF_LORA_ADAPTER_ROUTE_MAP_URL=localhost:6379
MF_LORA_ADAPTER_ROUTE_MAP_PASS=
MF_LORA_ADAPTER_ROUTE_MAP_DB=0
### OPC-UA
MF_OPCUA_ADAPTER_HTTP_PORT=8188
MF_OPCUA_ADAPTER_LOG_LEVEL=debug
MF_OPCUA_ADAPTER_POLICY=
MF_OPCUA_ADAPTER_MODE=
MF_OPCUA_ADAPTER_CERT_FILE=
MF_OPCUA_ADAPTER_KEY_FILE=
MF_OPCUA_ADAPTER_ROUTE_MAP_URL=localhost:6379
MF_OPCUA_ADAPTER_ROUTE_MAP_PASS=
MF_OPCUA_ADAPTER_ROUTE_MAP_DB=0
MF_OPCUA_ADAPTER_EVENT_CONSUMER=opcua
### Cassandra Writer
MF_CASSANDRA_WRITER_LOG_LEVEL=debug
MF_CASSANDRA_WRITER_PORT=8902
MF_CASSANDRA_WRITER_DB_PORT=9042
MF_CASSANDRA_WRITER_DB_CLUSTER=mainflux-cassandra
MF_CASSANDRA_WRITER_DB_KEYSPACE=mainflux
MF_CASSANDRA_WRITER_CONTENT_TYPE=application/senml+json
### Cassandra Reader
MF_CASSANDRA_READER_LOG_LEVEL=debug
MF_CASSANDRA_READER_PORT=8903
MF_CASSANDRA_READER_DB_PORT=9042
MF_CASSANDRA_READER_DB_CLUSTER=mainflux-cassandra
MF_CASSANDRA_READER_DB_KEYSPACE=mainflux
MF_CASSANDRA_READER_SERVER_CERT=
MF_CASSANDRA_READER_SERVER_KEY=
### InfluxDB Writer
MF_INFLUX_WRITER_LOG_LEVEL=debug
MF_INFLUX_WRITER_PORT=8900
MF_INFLUX_WRITER_BATCH_SIZE=5000
MF_INFLUX_WRITER_BATCH_TIMEOUT=5
MF_INFLUX_WRITER_DB_PORT=8086
MF_INFLUX_WRITER_DB_USER=mainflux
MF_INFLUX_WRITER_DB_PASS=mainflux
MF_INFLUX_WRITER_DB=mainflux
MF_INFLUX_WRITER_GRAFANA_PORT=3001
MF_INFLUX_WRITER_CONTENT_TYPE=application/senml+json
### InfluxDB Reader
MF_INFLUX_READER_LOG_LEVEL=debug
MF_INFLUX_READER_PORT=8905
MF_INFLUX_READER_DB_PORT=8086
MF_INFLUX_READER_DB_USER=mainflux
MF_INFLUX_READER_DB_PASS=mainflux
MF_INFLUX_READER_DB=mainflux
MF_INFLUX_READER_SERVER_CERT=
MF_INFLUX_READER_SERVER_KEY=
### MongoDB Writer
MF_MONGO_WRITER_LOG_LEVEL=debug
MF_MONGO_WRITER_PORT=8901
MF_MONGO_WRITER_DB=mainflux
MF_MONGO_WRITER_DB_PORT=27017
MF_MONGO_WRITER_CONTENT_TYPE=application/senml+json
### MongoDB Reader
MF_MONGO_READER_LOG_LEVEL=debug
MF_MONGO_READER_PORT=8904
MF_MONGO_READER_DB=mainflux
MF_MONGO_READER_DB_PORT=27017
MF_MONGO_READER_SERVER_CERT=
MF_MONGO_READER_SERVER_KEY=
### Postgres Writer
MF_POSTGRES_WRITER_LOG_LEVEL=debug
MF_POSTGRES_WRITER_PORT=9104
MF_POSTGRES_WRITER_DB_PORT=5432
MF_POSTGRES_WRITER_DB_USER=mainflux
MF_POSTGRES_WRITER_DB_PASS=mainflux
MF_POSTGRES_WRITER_DB=messages
MF_POSTGRES_WRITER_DB_SSL_MODE=disable
MF_POSTGRES_WRITER_DB_SSL_CERT=""
MF_POSTGRES_WRITER_DB_SSL_KEY=""
MF_POSTGRES_WRITER_DB_SSL_ROOT_CERT=""
MF_POSTGRES_WRITER_CONTENT_TYPE=application/senml+json
### Postgres Reader
MF_POSTGRES_READER_LOG_LEVEL=debug
MF_POSTGRES_READER_PORT=9204
MF_POSTGRES_READER_CLIENT_TLS=false
MF_POSTGRES_READER_CA_CERTS=""
MF_POSTGRES_READER_DB_PORT=5432
MF_POSTGRES_READER_DB_USER=mainflux
MF_POSTGRES_READER_DB_PASS=mainflux
MF_POSTGRES_READER_DB=messages
MF_POSTGRES_READER_DB_SSL_MODE=disable
MF_POSTGRES_READER_DB_SSL_CERT=""
MF_POSTGRES_READER_DB_SSL_KEY=""
MF_POSTGRES_READER_DB_SSL_ROOT_CERT=""
# Twins
MF_TWINS_LOG_LEVEL=debug
MF_TWINS_HTTP_PORT=9021
MF_TWINS_SERVER_CERT=""
MF_TWINS_SERVER_KEY=""
MF_TWINS_DB=mainflux-twins
MF_TWINS_DB_HOST=twins-db
MF_TWINS_DB_PORT=27018
MF_TWINS_SINGLE_USER_EMAIL=""
MF_TWINS_SINGLE_USER_TOKEN=""
MF_TWINS_CLIENT_TLS=""
MF_TWINS_CA_CERTS=""
MF_TWINS_MQTT_URL=tcp://mqtt-adapter:1883
MF_TWINS_THING_ID=
MF_TWINS_THING_KEY=
MF_TWINS_CHANNEL_ID=
-1
View File
@@ -1 +0,0 @@
* @mainflux/maintainers
-30
View File
@@ -1,30 +0,0 @@
<!--
The GitHub issue tracker is for bug reports and feature requests. General support can be found at
the following locations:
- Google group - https://groups.google.com/forum/#!forum/mainflux
- Gitter - https://gitter.im/mainflux/mainflux
-->
**FEATURE REQUEST**
1. Is there an open issue addressing this request? If it does, please add a "+1" reaction to the
existing issue, otherwise proceed to step 2.
2. Describe the feature you are requesting, as well as the possible use case(s) for it.
3. Indicate the importance of this feature to you (must-have, should-have, nice-to-have).
**BUG REPORT**
1. What were you trying to achieve?
2. What are the expected results?
3. What are the received results?
4. What are the steps to reproduce the issue?
5. In what environment did you encounter the issue?
6. Additional information you deem important:
-15
View File
@@ -1,15 +0,0 @@
Pull request title should be `MF-XXX - description` or `NOISSUE - description` where XXX is ID of issue that this PR relate to.
Please review the [CONTRIBUTING.md](./CONTRIBUTING.md) file for detailed contributing guidelines.
### What does this do?
### Which issue(s) does this PR fix/relate to?
Put here `Resolves #XXX` to auto-close the issue that your PR fixes (if such)
### List any changes that modify/break current functionality
### Have you included tests for your changes?
### Did you document any new/modified functionality?
### Notes
-8
View File
@@ -1,8 +0,0 @@
# Copyright (c) Mainflux
# SPDX-License-Identifier: Apache-2.0
# Set your private global .gitignore:
# https://digitalfortress.tech/tricks/creating-a-global-gitignore/
build
-502
View File
@@ -1,502 +0,0 @@
# Mainflux Changelog
## Generation
Mainflux release notes for the latest release can be obtained via:
```
make changelog
```
Otherwise, whole log in a similar format can be observed via:
```
git log --pretty=oneline --abbrev-commit
```
## 0.11.0 - 29. MAY 2020.
### Features and Bugfixes
- Add VerneMQ docker image build from source (#1178)
- MF-994 - Add tracing middleware for twins and states repos (#1181)
- MF-995 - Add Twins tests for endpoint list twins and list states (#1174)
- NOISSUE - Update dependencies (#1176)
- MF-1163 - Fix influxdb-reader to use nanoseconds precision (#1171)
- Rename environment variable MF_MQTT_ADAPTER_PORT to MF_MQTT_ADAPTER_MQTT_PORT in docker environment (#1170)
- Remove thing related code from twins service (#1169)
- MF-997 - Add twins service swagger file (#1167)
- MF-1079 - Add MQTT forwarder (#1164)
- MF-1159 - add gateway metadata update in provision method (#1160)
- MF-1055 - rollback/release transaction on error (#1166)
- NOISSUE - Use log level error for VermeMQ docker (#1162)
- NOISSUE - Fix default nats pubsub subject (#1153)
- MF-1125 - Document Provision service (#1143)
- NOISSUE - Fix bootstrap SDK args naming (#1151)
- Use VerneMQ default log level (#1150)
- NOISSUE - Update provision service (#1133)
- NOISSUE - Refactor messaging (#1141)
- Add JSON tags to SDK entities (#1146)
- NOISSUE - Update CLI README.md (#1139)
- NOISSUE - Update mProxy version (#1137)
- fix nginx, channel connect (#1136)
- Remove concurrency flag for golangci-lint (#1134)
- MF-1088 - Remove message payload content type (#1121)
- MF-1129 - Use snake_case for Lora and OPC-UA metadata fields (#1130)
- MF-1128 - Add golangci-linter to a CI script (#1131)
- MF-1123 - Move Provision service to monorepo (#1132)
- MF-845 - Add FOSSA badge for licensing (#1127)
- MF-1087 - Remove WebSocket adapter (#1120)
- NOISSUE - Use HTTP Status in SDK error messages (#1119)
- NOISSUE - Fix bootstrap token naming and interfaces named args (#1117)
- MF-1115 - Improve the SDK error encoding (#1118)
- MF-862 - Add boostrap CRUD to SDK and CLI (#1114)
- NOISSUE - Update coding style in Things service (#1116)
- NOISSUE - Remove defers from TestMain (#1111)
- NOISSUE - Create func to encode SDK errors (#1110)
- MF-1078 - Add timestamp to published messages and use it in Transformer (#1106)
- Fix prometheus namespace in postgres reader & writer (#1109)
- NOISSUE - Implement errors package in senml transformer, readers and writers (#1108)
- NOISSUE - Implement errors package in Authentication service (#1105)
- MF-1103 - API key should ignore empty expiration time (#1104)
- MF-1096 - Fix AuthN and Things Auth ENVARS (#1066)
- fix Contains function for nil arguments (#1102)
- MF-1099 - Add email subdomain validator (#1101)
- MF-1091 - Use channels. as broker prefix (#1098)
- MF-1090 - Use named Interfaces args (#1097)
- NOISSUE - Create broker package for NATS (#1080)
- NOISSUE - Implement errors package in bootstrap service (#1093)
- NOISSUE - Fix writers loadSubjectsConfig if file is missing (#1094)
- NOISSUE - Adding subtopics filtering in writer services (#1072)
- NOISSUE - Improve errors package (#1086)
- NOISSUE - Enable MQTT over WS in docker composition (#1085)
- NOISSUE - Rm unused opc-ua envars (#1083)
- MF-798 - Add utf8 support for email validation (#1082)
- Remove unused Tokenizer interface (#1084)
- Update mqtt adapter imports (#1081)
- NOISSUE - Update state based on SenML time value (#1075)
- NOISSUE - Fix StatusBadDecodingError for opc-ua browse (#1074)
- Save senml array msg to multiple states (#1073)
- NOISSUE - Fix opc-ua message type handling (#1071)
- NOISSUE - Add Publisher field to MQTT adapter (#1067)
- NOISSUE - Fix users CLI (#1062)
- NOISSUE - Fix SDK Messages response (#1064)
- Merged MQTT docker compose in core composition file (#1060)
- MF-1016 - Add UserUpdate and UpdatePassword to sdk and CLI (#1057)
- Update mProxy (#1058)
- MF-1053 - Add disconnect event to MQTT adapter (#1056)
- Fix data type for data_value in databases (#1054)
- NOISSUE - Fix opc-ua subscriptions store (#1052)
- NOISSUE - Fix connect CLI command and remove ConnectThing func from SDK (#1051)
- NOISSUE - Update Vernemq image repository (#1050)
- Removed VerneMQ auth plugin, Aedes impl. Added mproxy support in docker (#1049)
- NOISSUE - Add default subscription nodeID and Interval ENVAR (#1046)
- MF-415 - Merge mProxy support (#1045)
- NOISSUE - Remove twins-service mqtt dependency and publish notifs to nats (#1042)
- Add arbitrary SenML value type saving to twin state (#1039)
- Fixed Aedes dependencies (#1036)
- MF-998 - Add Twins service to Makefile and docker-compose.yml (#1035)
- MF-1032 - Fix redis docker volume of opcua-adapter (#1033)
- NOISSUE - add nats conf (#1031)
- MF-442 - Add SSL encryption to the MongoDB, InfluxDB and Cassanda readers (#1024)
- NOISSUE - Add opc-ua type handling and unsubscription (#1029)
- NOISSUE - Add aggregate attribute-based search for twin retrieval (#1027)
- NOISSUE - Fix metadata in add Things endpoint (#1028)
- NOISSUE - Fix minimal password length (#1023)
- MF-1020 - Change default password for CLI provision test (#1021)
- NOISSUE - Add subtopic to opcua messages (#1022)
- NOISSUE - Add details to browsed OPC-UA nodes (#1019)
- NOISSUE Fix obsolete attribute persistance (#1018)
- Fix twins update revision counter (#1011)
- Fixed docs instructions in README (#1010)
- Fix copyright year (#1009)
- Fix issuing recovery key (#1007)
- Removed gatling load-test (#1005)
- Removed old k8s manifests (#1004)
- NOISSUE - Remove UI from docker-compose (#1001)
- NOISSUE - Store successfull OPC-UA subscriptions (#999)
- MF-730 - Add digital twin service for things (#855)
- Fix Redis event naming (#996)
- NOISSUE - Add a Browse endpoint in opcua-adapter (#988)
- NOISSUE - Add Redis ES Username/Pass for VerneMQ (#991)
- MF-982 - Add error when connecting empty channels or things (#985)
## 0.10.0 - 17. DEC 2019.
### Features
- MF-932 - User API keys (#941)
- NOISSUE - Use opcua server timestamp in opcua-adapter messages (#980)
- Simplify CI script (#979)
- NOISSUE - Add opcua-adapter conn route-map, use ServerURI and NodeID (#975)
- Move docs to a separate repo (#976)
- NOISSUE - Support multiple types values in opcua-adapter (#973)
- Migrate from dep to go modules (#971)
- NOISSUE - Add Node IdentifierType config in opcua-adapter (#967)
- NOISSUE - Remove messages limit in influxdb-reader (#968)
- MF-898 - Add bulk connect to CLI and SDK (#956)
- MF-538 - Improve logging and API errors (#866)
- NOISSUE - Remove Elm UI (#953)
- MF-898 - Add bulk connections endpoint (#948)
- MF-898 - Change thing's service to use bulk connect (#946)
- MF-898 - Add transactions to postgres connect (#940)
- Add missing user service tests (#945)
- Remove Normalizer service from compose (#937)
- MF-919 - Mainflux message updates (#924)
- NOISSUE - Remove ARM multi-arch images (#929)
- MF-906 - Change single creation endpoints to use bulk service calls (#927)
- MF-922 - Add UpdateUser endpoint (#923)
- MF-780 - Use Normalizer as a lib (#915)
- NOISSUE - Switch to grpcbox for VerneMQ (#914)
- Change channels to chs (#918)
- MF-484 - Add bulk provisioning for things and channels (#889)
- MF-899 - Update README and official docs (#910)
- NOISSUE - Fix Redis envars (#903)
- Add disconnect on gen_server terminate() (#913)
- MF-890 - Add OPC-UA docs (#904)
- NOISSUE - Update Protobuf version (#902)
- MF-886 - Add OPC-UA adapter (#878)
- MF-532 - Password reset (#873)
- MF-785 - Change CanAccess to CanAccessByKey (#894)
- NOISSUE - Add MQTT UserName check on register and InstanceId in Redis (#884)
- Add MQTT troubleshooting section (#882)
- MF-875 - Add tracing to official documentation (#877)
- MF-788 - Remove date and minimize copyright comments (#876)
- MF-787 - Add tags to user, thing, and channel spans (#869)
- Update docker-compose version for addons (#874)
- MF-859 - Channels metadata search (#867)
- MF-858 Users metadata (#861)
- NOISSUE - Simplify MQTT benchmarking tool (#852)
- NOISSUE - Upgrade Go version to 1.13 in container images (#868)
- MF-820 - Fetch messages for a particular device (#843)
- Update gorilla websocket version (#865)
- NOISSUE - Update aedes version and fix Dockerfile (#863)
- NOISSUE - Search by metadata (#849)
- MF-846 - Install python in docker build for aedes mqtt image (#860)
- NOISSUE - Clean NginX files, move .gitignores to dirs (#853)
- NOISSUE - Add docker-compose for MQTT cluster (#841)
- Add debug logs to the WS adapter (#848)
- NOISSUE - Add measuring time from pub to sub (#839)
- NOISSUE - update mqtt prov tool and some refactor (#831)
- NOISSUE - Use Thing ID to update certs data (#827)
- NOISSUE - Improve VerneMQ plugin code, add configurable gRPC pool size (#836)
- NOISSUE - Use gRPC for VerneMQ (#835)
- Switch secure of WS connection according to secure of http connection of UI (#829)
- NOISSUE - Use current hostname instead of localhost for a WebSocket connection in the UI (#826)
- NOISSUE - Improve MQTT benchmarking tools (#828)
- NOISSUE - update mqtt benchmark (#824)
- Add encryption key to env vars table (#823)
- NOISSUE - Add version endpoint to MQTT adapter (#816)
- MF-295 add mqtt benchmark tool (#817)
- update mqtts commands (#815)
- NOISSUE - Support encrypted bootstrap (#796)
- Add config to writers docs (#812)
- NOISSUE - Add VerneMQ support (#809)
- NOISSUE - Add content type as part of MQTT subscription topic (#810)
### Bugfixes
- Fix MQTT protobuf filename(#981)
- MF-950 - Runtime error in normalizer - CBOR SenML (#974)
- NOISSUE - Fix opcua-adapter events warnings (#965)
- NOISSUE - Fix opcua-adapter events decode (#951)
- Fix subtopic handling in VerneMQ (#962)
- NOISSUE - Fix Update User (#959)
- NOISSUE - Fix make dockers (#957)
- Add dev_ back to make dockers_dev (#955)
- NOISSUE - Fix docs (#952)
- MF-916 - Fix Things and Channels counters (#947)
- MF-942 - Fix email template logic (#944)
- NOISSUE - Fix HTTP header for Things and Channels creation (#939)
- NOISSUE - Fix docker ui image name (#938)
- NOISSUE - Fix lora-adapter (#936)
- NOISSUE - Fix lora creation events (#933)
- Fix doc for ENV vars in README (#920)
- Fix compilation (#911)
- Revert "NOISSUE - Make event sourcing optional (#907)" (#909)
- NOISSUE - Make event sourcing optional (#907)
- NOISSUE - Fix InfluxDB env vars (#908)
- Fix Elm version for ARM Docker images (#905)
- Fix Elm version in Dockerfile (#901)
- NOISSUE - fix security doc (#897)
- NOISSUE - Fix typo in docs and README (#891)
- Fix Nginx mTLS configuration (#885)
- Fix provision tool connect error handling (#879)
- Fix: Correct 404 and Content-Type Issues in MQTT Version Endpoint (#837)
- NOISSUE - Fix proto files in VerneMQ (#834)
- NOISSUE - Fix hackney HTTP request (#833)
- Add socket pool and fix pattern matching (#830)
- Fix typo (#814)
## 0.9.0 - 19. JUL 2019.
### Features
- Create and push docker manifest for new release from Makefile (#794)
- MF-399 - Add open tracing support (#782)
- MF-783 - Allow access checking by a thing ID (#784)
- NOISSUE - Add authorization HTTP API to things service (#772)
- Remove cli executable from repo (#776)
- NOISSUE - Use .env vars in docker-compose (#770)
- MF-663 - enable nginx port conf from docker env (#769)
- Update docs (#766)
- NOISSUE - Remove installing non-existent package in ci (#758)
- NOISSUE - Add searchable Channels name (#754)
- MF-466 - ARM docker deployment (#756)
- Add missing Websocket.js into docker ui image (#755)
- NOISSUE - Add searchable Things name (#750)
- NOISSUE - Add certificate fields to the Bootstrap service (#752)
- Update grpc and protobuf deps in mqtt adapter (#751)
- MF-742 - Things to support single user scenario (#749)
- MF-732 - Add Postgres reader (#740)
- MF-722 - Change UUID lib (#746)
- Add performance improvement to writer filtering (#744)
- NOISSUE - Update nginx version (#748)
- MF-574 - Add missing environment variables to Cassandra writer (#745)
- NOISSUE - Add compile test to CI (#743)
- MF-708 - Assign Writer(s) to a channel (#737)
- MF-732 - Add PostgreSQL writer (#733)
- NOISSUE - Add readers pagination in SDK (#736)
- Add UI websocket open/close and send/receive (#728)
- MF-707 - Allow custom Thing key (#726)
- MF-525 - Add pagination response to the readers (#729)
- NOISSUE - Rm Things type from lora-adapter (#727)
- skip deleting of persistent volumes by default (#723)
- MF-488 - Remove Thing type (app or device) (#718)
- Remove empty channels check (#720)
- MF-655 Proper usage of docker volumes (#657)
- NOISSUE - Improve UI styling (#719)
- MF-715 - Conflict on updating connection with a valid list of channels (#716)
- MF-711 - Create separate Redis instance for ES (#717)
- NOISSUE - Update event fields naming (#713)
- MF-698 - Add missing info and docs about sys event sourcing (#712)
- MF-549 - Change metadata format from JSON string to JSON object (#706)
- NOISSUE - Replace repeating code by card gen func (#697)
- Update Bootstrap service docker-compose.yml (#700)
- Remove Debug function (#699)
- MF-687 - Add event sourcing to Bootstrap service (#695)
- NOISSUE - Remove debugging message from response of handle error function (#696)
- Add event stream to MQTT adapter for conn status (#692)
- NOISSUE - Improve UI style (#691)
- Update docs structure (#686)
- Use images instead of carousel (#685)
- NOISSUE - Update docs (#683)
- MF-662 - Change menu style (#678)
- MF-651 - X509 Mutual TLS authentication (#676)
- Update Aedes version for MQTT adapter (#677)
- MF-661 - Bootstrap pagination in UI (#672)
- Update subtopics section in documentation (#670)
- Remove default base URL value (#671)
### Bugfixes
- NOISSUE - Fix Readers logs (#735)
- NOISSUE - Fix Docker for ARM (#760)
- NOISSUE - Fix count when search by name is performed (#767)
- NOISSUE - Typo fix (#777)
- NOISSUE - Fix Postgres logs in Things service (#734)
- Fix CI with fixed plugin versions (#747)
- fix building problems (#741)
- fix docker-compose env (#775)
- Fix MF_THINGS_AUTH_GRPC_PORT in addons' docker-compose files (#781)
- Fix MQTT raw message deserialization (#753)
- fix variant option for manifest annotate (#765)
- fix to makefile for OSX/Darwin (#724)
- Fix .dockerignore file by removing index.html (#725)
- Fix things and channels metadata create and edit & remove thing type (#721)
- Fix Bootstrap service event map keys (#705)
- Fix logging in publish event callback (#694)
- Fix InfluxDB time bug (#689)
- Fix users service to work in offline mode (#795)
- fix mainflux_id parameter in bootstrap swagger (#789)
- Fix offset calculation after deleting thing/channel, not to go to negative offset after deleting last thing/channel (#679)
- Use errors and null packets in authorized pub/sub (#773)
- NOISSUE - Fix CoAP adapter (#779)
### Summary
https://github.com/mainflux/mainflux/milestone/10?closed=1
## 0.8.0 - 20. MAR 2019.
### Features
- MF-571 - Add Env.elm to set custom base URL (#654)
- NOISSUE Added docs about docker-compose config overriding (#653)
- MF-539 - Improve Bootstrap Service documentation (#646)
- MF-596 - Add subtopic to RawMessage (#642)
- NOISSUE - Prevent infinite loop in lora-adapter if Redis init fail (#647)
- Corrected grammar and rephrased a few sentences to read nicely (#641)
- MF-571 - Elm UI (#632)
- MF-552 - Use event sourcing to keep Bootstrap service in sync with Things service (#603)
- MF-540 - Add pagination in API responses for Bootstrap service (#575)
- MF-600 - Handle custom LoRa Server application decoder (#608)
- update docker-compose (#590)
- Update generated code (#602)
- Add generated files check (#601)
- MF-597 - Removed legacy code as not needed anymore (#598)
- NOISSUE - Added normalizer service to run script (#594)
- Changed RawMessage (#587)
- NOISSUE - fix CLI log (#581)
- MF-519 - Refine Message (#567)
- NOISSUE - Add name field for Bootstrap Config (#564)
- Fix non-SenML message routing in normalizer (#573)
- NOISSUE - Update authors list (#569)
- Update lora.md (#568)
- NOISSUE- Improve LoRa doc (#562)
- MF-551 - Add metadata fields to Bootstrap Channels (#563)
- Fix MQTT adapter by setting subscription queue (#561)
- MF-558 - Add MQTT subtopics documentation (#559)
- Fix regexp for SUB (#557)
- Simplify MQTT topipc regexp (#555)
- MF-429 -Enabled MQTT subtopic's (#554)
- Add env var for number of concurrent messages (#545)
- NOISSUE - Update doc and fix empty key bug (#544)
- MF-370 - Simplify and refine CI (#541)
- NOISSUE - Add connection commands to CLI (#542)
- NOISSUE - Refine docs (#537)
- Update licnese year (#533)
- MF-513 - Add Bootstrapping service (#524)
- Add dedicated env vars for event sourcing (#536)
- NOISSUE - Fix docs (#535)
- Add lora doc to getting-started.md (#529)
- MF-483 - Enable channels and devices corresponding lists in backend (#520)
- Add missing components doc to architecture.md (#531)
### Bugfixes
- MF-639 Split Content-Type header field on semicolon and evaluate all substrings (#644)
- MF-656 - Change bootstrap service port to 8200 (#658)
- Replace crossOrigin with relative path and fix messaging bug (#645)
- MF-579 Things & Channels returns 404 when not found or ID is malformed, not 500 (#633)
- Fix run command in dev guide (#605)
- MF-583 - Correct cmd/mongodb-reader HTTPServer log Info (#584)
- Fix Dusan Maldenovic GitHub (#570)
- Fix CLI docs (#566)
- Fix pagination response for empty page (#547)
- Fix swagger and provisioning docs (#546)
- NOISSUE - Fix event sourcing client on LoRa adapter (#527)
- Fix MQTT adapter scaling issue (#526)
- NOISSUE - Fix subtopic regex and restrict empty subtopic parts (#659)
- Fix missing css in container ui (#638)
- NOISSUE - Fix lora-adapter Object decode (#610)
- NOISSUE - Fix users logs in main.go (#577)
- NOISSUE - Fix normalizer exposed port in docker-compose (#548)
### Summary
https://github.com/mainflux/mainflux/milestone/9?closed=1
## 0.7.0 - 08. DEC 2018.
### Features
- MF-486 - Add provisioning command to CLI (#487)
- Fix lora-adapter event store handlers (#492)
- NOISSUE - Add LoRa route map validation and fix LoRa messages URL (#491)
- MF-475 - Replace increment ID with UUID (#490)
- MF-166 - Add lora-adapter service (#481)
- NOISSUE - Add Makefile target to clean old imgs (#485)
- MF-473 - Add metadata field to channel (#476)
- Make CoAP ping period configurable (#469)
- Add nginx ingress config to k8s services (#472)
- Add CoAP section in getting-started (#468)
- NOISSUE - Move CLI documentation from getting started guide to separate page (#470)
- NOISSUE - Update Getting Started doc with CLI usage (#465)
- Update CoAP docs with URL example (#463)
- MF-447 - Add event sourcing to things service (#460)
- Add TLS support to CoAP adapter and all readers (#459)
- MF-417 - Implement SDK tests (#438)
- MF-454 - Use message Time field as a time for InfluxDB points (#455)
- NOISSUE - Add .dockerignore to project root (#457)
- Update docker-compose so that every service has debug log level (#453)
- NOISSUE - Add TLS flag for Mainflux services (#452)
- MF-448 - Option for Postgres SSL Mode (#449)
- MF-443 Update project dependencies (#444)
- MF-426 - Add optional MF_CA_CERTS env variable to allow GRPC client to use TLS certs (#430)
- Expose the InfluxDB and Cassandra ports to host (#441)
- MF-374 - Bring back CoAP adapter (#413)
### Bugfixes
- gRPC Load Balancing between http-adapter and things (#387)
- MF-407 - Values of zero are being omitted (#434)
### Summary
https://github.com/mainflux/mainflux/milestone/8?closed=1
## 0.6.0 - 26. OCT 2018.
### Features
- Added Go SDK (#357)
- Updated NATS version (#412)
- Added debbug level to MFX logger (#379)
- Added Documentation for readers (#389)
- Added Redis cache to improve performance (#382)
## 0.5.1 - 05. SEP 2018.
### Features
- Improve performance by adding Redis cache (#382)
### Bugfixes
- Mixed up name and type of the things (#375)
- Fix MQTT topic (#380)
## 0.5.0 - 28. AUG 2018
### Features
- InfluxDB Reader (#311)
- Cassandra Reader (#313)
- MongoDB Reader (#344)
- MQTT Persistance via Redis (#328)
- CLI integrated into monorepo (#216)
- Normalizer logging (#333)
- WS swagger doc (#337)
- Payload renamed to Metadata (#343)
- Protobuf files added (#363)
- SPDX headers added (#325)
### Bugfixes
- Docker network for InfluxDB (#346)
- Vendor correct gRPC version (#340)
### Summary
https://github.com/mainflux/mainflux/milestone/6?closed=1
## 0.4.0 - 01. JUN 2018.
* Integrated MQTT adapter (#165 )
* Support for storing messages in MongoDB (#237)
* Support for storing messages in InfluxDB (#236)
* Use UUID PKs with auto-incremented values (#269 )
* Replaced JWT with plain string tokens in things service (#268 )
* Emit non-SenML messages (#239 )
* Support for Grafana (#296)
* Added WS Load test (#299 )
## 0.3.0 - 14. MAY 2018.
- CoAP API for message exchange (#186)
- Split `manager` service into `clients` and `users` (#266)
- Replaced ORM with raw SQL (#265)
- Setup Kubernetes (#226, #273)
- Fix docker compose (#274)
- Integrated `dashflux` into monorepo (#258)
- Integrated (*non-compatible*) `mqtt` into monorepo (#260)
## 0.2.3 - 24. APR 2018.
- Fix examples in the documentation (#243)
- Add service name in info response (#241)
- Improve code coverage in WS adapter (#242)
## 0.2.2 - 23. APR 2018.
- Setup load testing scenarios (#225)
## 0.2.1 - 22. APR 2018.
- Fixed `Content-Type` header checking (#238)
## 0.2.0 - 18. APR 2018
- Protobuf message serialization (#192)
- Websocket API for exchanging messages (#188)
- Channel & client retrieval paging (#227)
- Service instrumentation (#213)
- `go-kit` based JSON logger (#212)
- Project documentation (#218, #220)
- API tests (#211, #224)
## 0.1.2 - 18. MAR 2018.
### Bug fixes
- Fixed go lint warnings (#189)
- Compose failing startup (#185)
- Added missing service startup messages (#190)
+1
View File
@@ -0,0 +1 @@
docs.api.magistrala.absmach.eu
-87
View File
@@ -1,87 +0,0 @@
# Contributing to Mainflux
The following is a set of guidelines to contribute to Mainflux and its libraries, which are
hosted on the [Mainflux Organization](https://github.com/mainflux) on GitHub.
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
By participating, you are expected to uphold this code. Please report unacceptable behavior to
[abuse@mainflux.com](mailto:abuse@mainflux.com).
## Reporting issues
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written,
thorough bug report.
Before raising a new issue, check [our issue
list](https://github.com/mainflux/mainflux/issues) to determine if it already contains the
problem that you are facing.
A good bug report shouldn't leave others needing to chase you for more information. Please be as detailed as possible. The following questions might serve as a template for writing a detailed
report:
- What were you trying to achieve?
- What are the expected results?
- What are the received results?
- What are the steps to reproduce the issue?
- In what environment did you encounter the issue?
## Pull requests
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should
remain focused in scope and avoid unrelated commits.
**Please ask first** before embarking on any significant pull request (e.g. implementing new features,
refactoring code etc.), otherwise you risk spending a lot of time working on something that the
maintainers might not want to merge into the project.
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
To contribute to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
clone your fork repository, and configure the remotes:
```
git clone https://github.com/<your-username>/mainflux.git
cd mainflux
git remote add upstream https://github.com/mainflux/mainflux.git
```
If your cloned repository is behind the upstream commits, then get the latest changes from upstream:
```
git checkout master
git pull --rebase upstream master
```
Create a new topic branch from `master` using the naming convention `MF-[issue-number]`
to help us keep track of your contribution scope:
```
git checkout -b MF-[issue-number]
```
Commit your changes in logical chunks. When you are ready to commit, make sure
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
if you're unsure of what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
to group your commits into logical units of work before making it public.
Note that every commit you make must be signed. By signing off your work you indicate that you
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
Locally merge (or rebase) the upstream development branch into your topic branch:
```
git pull --rebase upstream master
```
Push your topic branch up to your fork:
```
git push origin MF-[issue-number]
```
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
and detailed description.
-191
View File
@@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2015-2020 Mainflux
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-46
View File
@@ -1,46 +0,0 @@
# Mainflux follows the timeless, highly efficient and totally unfair system
# known as [Benevolent dictator for
# life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
# Drasko DRASKOVIC in the role of BDFL.
[bdfl]
[[drasko]]
Name = "Drasko DRASKOVIC"
Email = "drasko@mainflux.com"
GitHub = "drasko"
# However, this role serves only in dead-lock events, or in a special and very rare cases
# when BDFL completely disagrees with the decisions made.
# In the normal flow of events, decisions on the project design are made through discussions,
# most often on the Pull Requests.
#
# Maintainers have the special role in the project in managing and accepting PRs,
# overall leading the project and making design decisions on the maintained subsystems.
#
# A reference list of all maintainers of the Mainflux project.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[maintainers]
[[aleksandar]]
Name = "Aleksandar NOVAKOVIC"
Email = "aleksandar@mainflux.com"
GitHub = "anovakovic01"
[[dusan]]
Name = "Dusan BOROVCANIN"
Email = "dusan@mainflux.com"
GitHub = "dusanb94"
[[manuel]]
Name = "Manuel IMPERIALE"
Email = "manuel@mainflux.com"
GitHub = "manuIO"
[[nikola]]
Name = "Nikola MARCETIC"
Email = "nikola@mainflux.com"
GitHub = "nmarcetic"
-123
View File
@@ -1,123 +0,0 @@
# Copyright (c) Mainflux
# SPDX-License-Identifier: Apache-2.0
BUILD_DIR = build
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
bootstrap opcua authn twins mqtt provision
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
GOARCH ?= amd64
define compile_service
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -mod=vendor -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
endef
define make_docker
$(eval svc=$(subst docker_,,$(1)))
docker build \
--no-cache \
--build-arg SVC=$(svc) \
--build-arg GOARCH=$(GOARCH) \
--build-arg GOARM=$(GOARM) \
--tag=mainflux/$(svc) \
-f docker/Dockerfile .
endef
define make_docker_dev
$(eval svc=$(subst docker_dev_,,$(1)))
docker build \
--no-cache \
--build-arg SVC=$(svc) \
--tag=mainflux/$(svc) \
-f docker/Dockerfile.dev ./build
endef
all: $(SERVICES)
.PHONY: all $(SERVICES) dockers dockers_dev latest release
clean:
rm -rf ${BUILD_DIR}
cleandocker:
# Stop all containers (if running)
docker-compose -f docker/docker-compose.yml stop
# Remove mainflux containers
docker ps -f name=mainflux -aq | xargs -r docker rm
# Remove exited containers
docker ps -f name=mainflux -f status=dead -f status=exited -aq | xargs -r docker rm -v
# Remove unused images
docker images "mainflux\/*" -f dangling=true -q | xargs -r docker rmi
# Remove old mainflux images
docker images -q mainflux\/* | xargs -r docker rmi
ifdef pv
# Remove unused volumes
docker volume ls -f name=mainflux -f dangling=true -q | xargs -r docker volume rm
endif
install:
cp ${BUILD_DIR}/* $(GOBIN)
test:
go test -mod=vendor -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
proto:
protoc --gofast_out=plugins=grpc:. *.proto
protoc --gofast_out=plugins=grpc:. messaging/*.proto
$(SERVICES):
$(call compile_service,$(@))
$(DOCKERS):
$(call make_docker,$(@),$(GOARCH))
$(DOCKERS_DEV):
$(call make_docker_dev,$(@))
dockers: $(DOCKERS)
dockers_dev: $(DOCKERS_DEV)
define docker_push
for svc in $(SERVICES); do \
docker push mainflux/$$svc:$(1); \
done
endef
changelog:
git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s"
latest: dockers
$(call docker_push,latest)
release:
$(eval version = $(shell git describe --abbrev=0 --tags))
git checkout $(version)
$(MAKE) dockers
for svc in $(SERVICES); do \
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
done
$(call docker_push,$(version))
rundev:
cd scripts && ./run.sh
run:
docker-compose -f docker/docker-compose.yml up
runlora:
docker-compose \
-f docker/docker-compose.yml \
-f docker/addons/influxdb-writer/docker-compose.yml \
-f docker/addons/lora-adapter/docker-compose.yml up \
# Run all Mainflux core services except distributed tracing system - Jaeger. Recommended on gateways:
rungw:
MF_JAEGER_URL= docker-compose -f docker/docker-compose.yml up --scale jaeger=0
-166
View File
@@ -1,166 +0,0 @@
# Mainflux
[![build][ci-badge]][ci-url]
[![go report card][grc-badge]][grc-url]
[![coverage][cov-badge]][cov-url]
[![license][license]](LICENSE)
[![chat][gitter-badge]][gitter]
![banner][banner]
Mainflux is modern, scalable, secure, open-source, and patent-free IoT cloud platform written in Go.
It accepts user and thing connections over various network protocols (i.e. HTTP,
MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware
for building complex IoT solutions.
For more details, check out the [official documentation][docs].
Mainflux is member of the [Linux Foundation][lf] and an active contributor
to the [EdgeX Foundry][edgex] project. It has been made with :heart: by [Mainflux Labs][company],
which maintains the project and offers professional services around it.
## Features
- Multi-protocol connectivity and bridging (HTTP, MQTT, WebSocket and CoAP)
- Device management and provisioning (Zero Touch provisioning)
- Mutual TLS Authentication (mTLS) using X.509 Certificates
- Fine-grained access control
- Message persistence (Cassandra, InfluxDB, MongoDB and PostgresSQL)
- Platform logging and instrumentation support (Grafana, Prometheus and OpenTracing)
- Event sourcing
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
- [LoRaWAN][lora] network integration
- SDK
- CLI
- Small memory footprint and fast execution
- Domain-driven design architecture, high-quality code and test coverage
## Prerequisites
The following are needed to run Mainflux:
- [Docker](https://docs.docker.com/install/) (version 18.09)
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.24.1)
Developing Mainflux will also require:
- [Go](https://golang.org/doc/install) (version 1.13.3)
- [Protobuf](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation) (version 3.6.1)
## Install
Once the prerequisites are installed, execute the following commands from the project's root:
```bash
docker-compose -f docker/docker-compose.yml up
```
This will bring up the Mainflux docker services and interconnect them. This command can also be executed using the project's included Makefile:
```bash
make run
```
## Usage
The quickest way to start using Mainflux is via the CLI. The latest version can be downloaded from the [official releases page][rel].
It can also be built and used from the project's root directory:
```bash
make cli
./build/mainflux-cli version
```
Additional details on using the CLI can be found in the [CLI documentation](https://mainflux.readthedocs.io/en/latest/cli/).
## Documentation
Official documentation is hosted at [Mainflux Read The Docs page][docs]. Documentation is auto-generated, checkout the instructions on [official docs repository](https://github.com/mainflux/docs):
If you spot an error or a need for corrections, please let us know - or even better: send us a PR.
Additional practical information, news and tutorials can be found on the [Mainflux blog][blog].
## Authors
Main architect and BDFL of Mainflux project is [@drasko][drasko].
Additionally, [@nmarcetic][nikola] and [@janko-isidorovic][janko] assured
overall architecture and design, while [@manuio][manu] and [@darkodraskovic][darko]
helped with crafting initial implementation and continuously worked on the project evolutions.
Besides them, Mainflux is constantly improved and actively
developed by [@anovakovic01][alex], [@dusanb94][dusan], [@srados][sava],
[@gsaleh][george], [@blokovi][iva], [@chombium][kole], [@mteodor][mirko] and a large set of contributors.
Maintainers are listed in [MAINTAINERS](MAINTAINERS) file.
The Mainflux team would like to give special thanks to [@mijicd][dejan] for his monumental work
on designing and implementing a highly improved and optimized version of the platform,
and [@malidukica][dusanm] for his effort on implementing the initial user interface.
## Contributing
Thank you for your interest in Mainflux and the desire to contribute!
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues). The [good-first-issue](https://github.com/mainflux/mainflux/labels/good-first-issue) label is specifically for issues that are great for getting started.
2. Checkout the [contribution guide](CONTRIBUTING.md) to learn more about our style and conventions.
3. Make your changes compatible to our workflow.
### We're Hiring
If you are interested in working professionally on Mainflux,
please head to company's [careers page][careers] or shoot us an e-mail at <careers@mainflux.com>.
>The best way to grab our attention is by sending PRs :sunglasses:.
## Community
- [Google group][forum]
- [Gitter][gitter]
- [Twitter][twitter]
## License
[Apache-2.0](LICENSE)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmainflux%2Fmainflux.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmainflux%2Fmainflux?ref=badge_large)
[banner]: https://github.com/mainflux/docs/blob/master/docs/img/gopherBanner.jpg
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
[ci-url]: https://semaphoreci.com/mainflux/mainflux
[docs]: http://mainflux.readthedocs.io
[docker]: https://www.docker.com
[forum]: https://groups.google.com/forum/#!forum/mainflux
[gitter]: https://gitter.im/mainflux/mainflux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
[grc-badge]: https://goreportcard.com/badge/github.com/mainflux/mainflux
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
[cov-badge]: https://codecov.io/gh/mainflux/mainflux/branch/master/graph/badge.svg
[cov-url]: https://codecov.io/gh/mainflux/mainflux
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
[twitter]: https://twitter.com/mainflux
[lora]: https://lora-alliance.org/
[kubernetes]: https://kubernetes.io/
[rel]: https://github.com/mainflux/mainflux/releases
[careers]: https://www.mainflux.com/careers.html
[lf]: https://www.linuxfoundation.org/
[edgex]: https://www.edgexfoundry.org/
[company]: https://www.mainflux.com/
[blog]: https://medium.com/mainflux-iot-platform
[drasko]: https://github.com/drasko
[nikola]: https://github.com/nmarcetic
[dejan]: https://github.com/mijicd
[manu]: https://github.com/manuIO
[darko]: https://github.com/darkodraskovic
[janko]: https://github.com/janko-isidorovic
[alex]: https://github.com/anovakovic01
[dusan]: https://github.com/dusanb94
[sava]: https://github.com/srados
[george]: https://github.com/gesaleh
[iva]: https://github.com/blokovi
[kole]: https://github.com/chombium
[dusanm]: https://github.com/malidukica
[mirko]: https://github.com/mteodor
+508
View File
@@ -0,0 +1,508 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala Alarms API
description: |
HTTP API for managing alarms service.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.5
servers:
- url: http://localhost:8050
- url: https://localhost:8050
tags:
- name: alarms
description: Everything about your Alarms
externalDocs:
description: Find out more about alarms
url: https://magistrala.absmach.eu/docs/
paths:
/{domainID}/alarms:
get:
operationId: listAlarms
summary: List Alarms
description: |
Retrieves a list of alarms with optional filtering
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Order'
- $ref: '#/components/parameters/Dir'
- $ref: '#/components/parameters/ChannelID'
- $ref: '#/components/parameters/ClientID'
- $ref: '#/components/parameters/Subtopic'
- $ref: '#/components/parameters/RuleID'
- $ref: '#/components/parameters/Status'
- $ref: '#/components/parameters/AssigneeID'
- $ref: '#/components/parameters/Severity'
- $ref: '#/components/parameters/UpdatedBy'
- $ref: '#/components/parameters/AssignedBy'
- $ref: '#/components/parameters/AcknowledgedBy'
- $ref: '#/components/parameters/ResolvedBy'
- $ref: '#/components/parameters/CreatedFrom'
- $ref: '#/components/parameters/CreatedTo'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/AlarmsPageRes'
'400':
description: Failed due to malformed query parameters
'401':
description: Missing or invalid access token
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/alarms/{alarmID}:
get:
operationId: viewAlarm
summary: View Alarm
description: Retrieves an alarm by ID
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/AlarmRes'
'400':
description: Missing or invalid alarm ID
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
put:
operationId: updateAlarm
summary: Update Alarm
description: Updates an existing alarm
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
requestBody:
$ref: '#/components/requestBodies/AlarmUpdateReq'
responses:
'200':
$ref: '#/components/responses/AlarmRes'
'400':
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'415':
description: Missing or invalid content type
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
delete:
operationId: deleteAlarm
summary: Delete Alarm
description: Deletes an alarm
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
responses:
'204':
description: Alarm deleted successfully
'400':
description: Failed due to malformed alarm ID
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
/health:
get:
summary: Retrieves service health check info
tags:
- health
security: []
responses:
'200':
$ref: '#/components/responses/HealthRes'
'500':
$ref: '#/components/responses/ServiceError'
components:
schemas:
Alarm:
type: object
properties:
id:
type: string
description: Unique alarm identifier
readOnly: true
rule_id:
type: string
description: Rule ID that triggered this alarm
domain_id:
type: string
description: Domain ID this alarm belongs to
channel_id:
type: string
description: Channel ID where the alarm was triggered
client_id:
type: string
description: Client ID that triggered the alarm
subtopic:
type: string
description: Subtopic associated with the alarm
status:
type: string
description: Alarm status
enum: [active, cleared]
measurement:
type: string
description: Measurement that triggered the alarm
value:
type: string
description: Value that triggered the alarm
unit:
type: string
description: Unit of measurement
threshold:
type: string
description: Threshold value that was exceeded
cause:
type: string
description: Cause or description of the alarm
severity:
type: integer
description: Severity level (0-100)
minimum: 0
maximum: 100
assignee_id:
type: string
description: ID of the user assigned to this alarm
created_at:
type: string
format: date-time
description: Creation timestamp
readOnly: true
updated_at:
type: string
format: date-time
description: Last update timestamp
readOnly: true
updated_by:
type: string
description: User who last updated the alarm
readOnly: true
assigned_at:
type: string
format: date-time
description: When the alarm was assigned
readOnly: true
assigned_by:
type: string
description: User who assigned the alarm
readOnly: true
acknowledged_at:
type: string
format: date-time
description: When the alarm was acknowledged
readOnly: true
acknowledged_by:
type: string
description: User who acknowledged the alarm
readOnly: true
resolved_at:
type: string
format: date-time
description: When the alarm was resolved
readOnly: true
resolved_by:
type: string
description: User who resolved the alarm
readOnly: true
metadata:
type: object
description: Custom metadata
additionalProperties: true
AlarmsPage:
type: object
properties:
offset:
type: integer
description: Number of items to skip during retrieval
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve
minimum: 1
maximum: 1000
default: 10
total:
type: integer
description: Total number of results
minimum: 0
alarms:
type: array
minItems: 0
items:
$ref: '#/components/schemas/Alarm'
required:
- alarms
- total
- offset
- limit
parameters:
DomainID:
name: domainID
description: Domain ID
in: path
required: true
schema:
type: string
AlarmID:
name: alarmID
description: Alarm ID
in: path
required: true
schema:
type: string
Offset:
name: offset
description: Number of items to skip
in: query
required: false
schema:
type: integer
default: 0
minimum: 0
Limit:
name: limit
description: Size of the subset to retrieve
in: query
required: false
schema:
type: integer
default: 10
minimum: 1
maximum: 1000
Order:
name: order
description: Order by field
in: query
required: false
schema:
type: string
enum: [created_at, updated_at]
default: created_at
Dir:
name: dir
description: Sort direction
in: query
required: false
schema:
type: string
enum: [asc, desc]
default: desc
ChannelID:
name: channel_id
description: Filter by channel ID
in: query
required: false
schema:
type: string
ClientID:
name: client_id
description: Filter by client ID
in: query
required: false
schema:
type: string
Subtopic:
name: subtopic
description: Filter by subtopic
in: query
required: false
schema:
type: string
RuleID:
name: rule_id
description: Filter by rule ID
in: query
required: false
schema:
type: string
Status:
name: status
description: Filter by alarm status
in: query
required: false
schema:
type: string
enum: [active, cleared, all]
default: all
AssigneeID:
name: assignee_id
description: Filter by assignee ID
in: query
required: false
schema:
type: string
Severity:
name: severity
description: Filter by severity level
in: query
required: false
schema:
type: integer
minimum: 0
maximum: 100
UpdatedBy:
name: updated_by
description: Filter by user who updated
in: query
required: false
schema:
type: string
AssignedBy:
name: assigned_by
description: Filter by user who assigned
in: query
required: false
schema:
type: string
AcknowledgedBy:
name: acknowledged_by
description: Filter by user who acknowledged
in: query
required: false
schema:
type: string
ResolvedBy:
name: resolved_by
description: Filter by user who resolved
in: query
required: false
schema:
type: string
CreatedFrom:
name: created_from
description: Filter alarms created after this time (RFC3339 format)
in: query
required: false
schema:
type: string
format: date-time
CreatedTo:
name: created_to
description: Filter alarms created before this time (RFC3339 format)
in: query
required: false
schema:
type: string
format: date-time
requestBodies:
AlarmUpdateReq:
description: JSON-formatted document describing the alarm update
required: true
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: Alarm status
enum: [active, cleared]
assignee_id:
type: string
description: ID of the user assigned to this alarm
severity:
type: integer
description: Severity level (0-100)
minimum: 0
maximum: 100
metadata:
type: object
description: Custom metadata
additionalProperties: true
responses:
AlarmRes:
description: Alarm data retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/Alarm'
links:
update:
operationId: updateAlarm
parameters:
alarmID: $response.body#/id
domainID: $response.body#/domain_id
delete:
operationId: deleteAlarm
parameters:
alarmID: $response.body#/id
domainID: $response.body#/domain_id
AlarmsPageRes:
description: Alarms page retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/AlarmsPage'
ServiceError:
description: Unexpected server-side error occurred
HealthRes:
description: Service Health Check
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
-16
View File
@@ -1,16 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mainflux
// Response contains HTTP response specific methods.
type Response interface {
// Code returns HTTP response code.
Code() int
// Headers returns map of HTTP headers with their values.
Headers() map[string]string
// Empty indicates if HTTP response has content.
Empty() bool
}
+851
View File
@@ -0,0 +1,851 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Magistrala Auth Service
description: |
This is the Auth Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform users. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.0
servers:
- url: http://localhost:9001
- url: https://localhost:9001
tags:
- name: Keys
description: Everything about your Keys.
externalDocs:
description: Find out more about keys
url: https://magistrala.absmach.eu/docs/
- name: PATs
description: Everything about your Personal Access Tokens.
externalDocs:
description: Find out more about Personal Access Tokens
url: https://magistrala.absmach.eu/docs/
- name: Health
description: Service health check endpoint.
externalDocs:
description: Find out more about health check
url: https://magistrala.absmach.eu/docs/
paths:
/keys:
post:
operationId: issueKey
tags:
- Keys
summary: Issue API key
description: |
Generates a new API key. Thew new API key will
be uniquely identified by its ID.
requestBody:
$ref: "#/components/requestBodies/KeyRequest"
responses:
"201":
description: Issued new key.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"409":
description: Failed due to using already existing ID.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/keys/{keyID}:
get:
operationId: getKey
summary: Gets API key details.
description: |
Gets API key details for the given key.
tags:
- Keys
parameters:
- $ref: "#/components/parameters/ApiKeyId"
responses:
"200":
$ref: "#/components/responses/KeyRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: revokeKey
summary: Revoke API key
description: |
Revoke API key identified by the given ID.
tags:
- Keys
parameters:
- $ref: "#/components/parameters/ApiKeyId"
responses:
"204":
description: Key revoked.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/pats:
post:
operationId: createPAT
tags:
- PATs
summary: Create a new Personal Access Token
description: |
Creates a new Personal Access Token (PAT) for the authenticated user.
requestBody:
$ref: "#/components/requestBodies/CreatePATRequest"
responses:
"201":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: listPATs
tags:
- PATs
summary: List all Personal Access Tokens
description: |
Lists all Personal Access Tokens (PATs) for the authenticated user.
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/PATsPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllPATs
tags:
- PATs
summary: Remove all Personal Access Tokens
description: |
Removes all Personal Access Tokens (PATs) for the authenticated user.
responses:
"200":
description: All PATs removed successfully.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}:
get:
operationId: retrievePAT
tags:
- PATs
summary: Retrieve a Personal Access Token
description: |
Retrieves details of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"200":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: deletePAT
tags:
- PATs
summary: Delete a Personal Access Token
description: |
Deletes a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"204":
description: PAT deleted successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/name:
patch:
operationId: updatePATName
tags:
- PATs
summary: Update Personal Access Token name
description: |
Updates the name of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/UpdatePATNameRequest"
responses:
"202":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/description:
patch:
operationId: updatePATDescription
tags:
- PATs
summary: Update Personal Access Token description
description: |
Updates the description of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/UpdatePATDescriptionRequest"
responses:
"202":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/reset:
patch:
operationId: resetPATSecret
tags:
- PATs
summary: Reset Personal Access Token secret
description: |
Resets the secret of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/ResetPATSecretRequest"
responses:
"200":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/revoke:
patch:
operationId: revokePATSecret
tags:
- PATs
summary: Revoke Personal Access Token secret
description: |
Revokes the secret of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"204":
description: PAT secret revoked successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope:
get:
operationId: listScopes
tags:
- PATs
summary: List scopes for a Personal Access Token
description: |
Lists all scopes for a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/ScopesPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllScopes
tags:
- PATs
summary: Remove all scopes from a Personal Access Token
description: |
Removes all scopes from a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"200":
description: All scopes removed successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/add:
patch:
operationId: addScope
tags:
- PATs
summary: Add scope to a Personal Access Token
description: |
Adds a scope to a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/AddScopeRequest"
responses:
"200":
description: Scope added successfully.
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"422":
description: Database cannot process the request.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/remove:
patch:
operationId: removeScope
tags:
- PATs
summary: Remove scope from a Personal Access Token
description: |
Removes a scope from a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/RemoveScopeRequest"
responses:
"200":
description: Scope removed successfully.
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- Health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
PAT:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: Personal Access Token unique identifier
user_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: User ID of the PAT owner
name:
type: string
example: "My PAT"
description: Name of the Personal Access Token
description:
type: string
example: "Token for automation"
description: Description of the Personal Access Token
secret:
type: string
example: "pat_1234567890abcdef"
description: Secret value of the Personal Access Token
issued_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was issued
expires_at:
type: string
format: date-time
example: "2020-11-26T13:31:52Z"
description: Time when the PAT expires
updated_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was last updated
last_used_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was last used
revoked:
type: boolean
example: false
description: Whether the PAT is revoked
revoked_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was revoked
PATsPage:
type: object
properties:
total:
type: integer
example: 10
description: Total number of PATs
offset:
type: integer
example: 0
description: Number of items to skip during retrieval
limit:
type: integer
example: 10
description: Size of the subset to retrieve
pats:
type: array
items:
$ref: "#/components/schemas/PAT"
description: List of Personal Access Tokens
Scope:
type: object
properties:
optional_domain_id:
type: string
format: uuid
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
description: Optional domain ID for the scope
entity_type:
type: string
enum:
[groups, channels, clients, domains, users, dashboards, messages]
example: "groups"
description: Type of entity the scope applies to
entity_id:
type: string
example: "*"
description: ID of the entity the scope applies to. '*' means all entities of the specified type.
operation:
type: string
enum:
[
create,
read,
list,
update,
delete,
share,
unshare,
publish,
subscribe,
]
example: "read"
description: Operation allowed by this scope
ScopesPage:
type: object
properties:
total:
type: integer
example: 10
description: Total number of scopes
offset:
type: integer
example: 0
description: Number of items to skip during retrieval
limit:
type: integer
example: 10
description: Size of the subset to retrieve
scopes:
type: array
items:
$ref: "#/components/schemas/Scope"
description: List of scopes
Key:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: API key unique identifier
issuer_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: In ID of the entity that issued the token.
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
subject:
type: string
format: string
example: "test@example.com"
description: User's email or service identifier of API key subject.
issued_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the key is generated.
expires_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the Key expires. If this field is missing,
that means that Key is valid indefinitely.
parameters:
PatID:
name: patID
description: Personal Access Token ID.
in: path
schema:
type: string
format: uuid
required: true
DomainID:
name: domainID
description: Unique domain identifier.
in: path
schema:
type: string
format: uuid
required: true
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
Status:
name: status
description: Domain status.
in: query
schema:
type: string
default: enabled
required: false
example: enabled
DomainName:
name: name
description: Domain's name.
in: query
schema:
type: string
required: false
example: "domainName"
Permission:
name: permission
description: permission.
in: query
schema:
type: string
required: false
example: "edit"
ApiKeyId:
name: keyID
description: API Key ID.
in: path
schema:
type: string
format: uuid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Metadata:
name: metadata
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
in: query
required: false
schema:
type: object
additionalProperties: {}
Type:
name: type
description: The type of the API Key.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Subject:
name: subject
description: The subject of an API Key
in: query
schema:
type: string
required: false
requestBodies:
CreatePATRequest:
description: JSON-formatted document describing PAT creation request.
required: true
content:
application/json:
schema:
type: object
required:
- name
- duration
properties:
name:
type: string
minLength: 1
example: "My PAT"
description: Name of the Personal Access Token
description:
type: string
example: "Token for automation"
description: Description of the Personal Access Token
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the PAT is valid. Format is a duration string (e.g. "30d", "24h", "1y").
UpdatePATNameRequest:
description: JSON-formatted document describing PAT name update request.
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
example: "New PAT Name"
description: New name for the Personal Access Token
UpdatePATDescriptionRequest:
description: JSON-formatted document describing PAT description update request.
required: true
content:
application/json:
schema:
type: object
required:
- description
properties:
description:
type: string
example: "New PAT Description"
description: New description for the Personal Access Token
ResetPATSecretRequest:
description: JSON-formatted document describing PAT secret reset request.
required: true
content:
application/json:
schema:
type: object
required:
- duration
properties:
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the new PAT secret is valid. Format is a duration string (e.g. "30d", "24h", "1y").
AddScopeRequest:
description: JSON-formatted document describing add scope request.
required: true
content:
application/json:
schema:
type: object
required:
- scopes
properties:
scopes:
type: array
items:
$ref: "#/components/schemas/Scope"
description: List of scopes to add
RemoveScopeRequest:
description: JSON-formatted document describing remove scope request.
required: true
content:
application/json:
schema:
type: object
required:
- scopes_id
properties:
scopes_id:
type: array
items:
type: string
format: uuid
description: List of scope IDs to remove
KeyRequest:
description: JSON-formatted document describing key request.
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
duration:
type: number
format: integer
example: 23456
description: Number of seconds issued token is valid for.
responses:
PATRes:
description: Personal Access Token data.
content:
application/json:
schema:
$ref: "#/components/schemas/PAT"
PATsPageRes:
description: Page of Personal Access Tokens.
content:
application/json:
schema:
$ref: "#/components/schemas/PATsPage"
ScopesPageRes:
description: Page of scopes.
content:
application/json:
schema:
$ref: "#/components/schemas/ScopesPage"
ServiceError:
description: Unexpected server-side error occurred.
KeyRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Key"
links:
revoke:
operationId: revokeKey
parameters:
keyID: $response.body#/id
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
security:
- bearerAuth: []
-1666
View File
File diff suppressed because it is too large Load Diff
-49
View File
@@ -1,49 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package mainflux;
import "google/protobuf/empty.proto";
service ThingsService {
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
rpc Identify(Token) returns (ThingID) {}
}
service AuthNService {
rpc Issue(IssueReq) returns (Token) {}
rpc Identify(Token) returns (UserID) {}
}
message AccessByKeyReq {
string token = 1;
string chanID = 2;
}
message ThingID {
string value = 1;
}
message AccessByIDReq {
string thingID = 1;
string chanID = 2;
}
// If a token is not carrying any information itself, the type
// field can be used to determine how to validate the token.
// Also, different tokens can be encoded in different ways.
message Token {
string value = 1;
}
message UserID {
string value = 1;
}
message IssueReq {
string issuer = 1;
uint32 type = 2;
}
-106
View File
@@ -1,106 +0,0 @@
# Authentication service
Authentication service provides an API for managing authentication keys.
There are *three types of authentication keys*:
- user key - keys issued to the user upon login request
- API key - keys issued upon the user request
- recovery key - password recovery key
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user. API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. Recovery key is the password recovery key. It's short-lived token used for password recovery process.
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
The following actions are supported:
- create (all key types)
- verify (all key types)
- obtain (API keys only; secret is never obtained)
- revoke (API keys only)
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|---------------------------|--------------------------------------------------------------------------|---------------|
| MF_AUTHN_LOG_LEVEL | Service level (debug, info, warn, error) | error |
| MF_AUTHN_DB_HOST | Database host address | localhost |
| MF_AUTHN_DB_PORT | Database host port | 5432 |
| MF_AUTHN_DB_USER | Database user | mainflux |
| MF_AUTHN_DB_PASSWORD | Database password | mainflux |
| MF_AUTHN_DB | Name of the database used by the service | auth |
| MF_AUTHN_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| MF_AUTHN_DB_SSL_CERT | Path to the PEM encoded certificate file | |
| MF_AUTHN_DB_SSL_KEY | Path to the PEM encoded key file | |
| MF_AUTHN_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
| MF_AUTHN_HTTP_PORT | Authn service HTTP port | 8180 |
| MF_AUTHN_GRPC_PORT | Authn service gRPC port | 8181 |
| MF_AUTHN_SERVER_CERT | Path to server certificate in pem format | |
| MF_AUTHN_SERVER_KEY | Path to server key in pem format | |
| MF_AUTHN_SECRET | String used for signing tokens | auth |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
services:
authn:
image: mainflux/authn:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_AUTHN_LOG_LEVEL: [Service log level]
MF_AUTHN_DB_HOST: [Database host address]
MF_AUTHN_DB_PORT: [Database host port]
MF_AUTHN_DB_USER: [Database user]
MF_AUTHN_DB_PASS: [Database password]
MF_AUTHN_DB: [Name of the database used by the service]
MF_AUTHN_DB_SSL_MODE: [SSL mode to connect to the database with]
MF_AUTHN_DB_SSL_CERT: [Path to the PEM encoded certificate file]
MF_AUTHN_DB_SSL_KEY: [Path to the PEM encoded key file]
MF_AUTHN_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
MF_AUTHN_HTTP_PORT: [Service HTTP port]
MF_AUTHN_GRPC_PORT: [Service gRPC port]
MF_AUTHN_SECRET: [String used for signing tokens]
MF_AUTHN_SERVER_CERT: [String path to server certificate in pem format]
MF_AUTHN_SERVER_KEY: [String path to server key in pem format]
MF_JAEGER_URL: [Jaeger server URL]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the service
make authn
# copy binary to bin
make install
# set the environment variables and run the service
MF_AUTHN_LOG_LEVEL=[Service log level] MF_AUTHN_DB_HOST=[Database host address] MF_AUTHN_DB_PORT=[Database host port] MF_AUTHN_DB_USER=[Database user] MF_AUTHN_DB_PASS=[Database password] MF_AUTHN_DB=[Name of the database used by the service] MF_AUTHN_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTHN_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTHN_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTHN_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTHN_HTTP_PORT=[Service HTTP port] MF_AUTHN_GRPC_PORT=[Service gRPC port] MF_AUTHN_SECRET=[String used for signing tokens] MF_AUTHN_SERVER_CERT=[Path to server certificate] MF_AUTHN_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-authn
```
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package api contains implementation of AuthN service HTTP API.
package api
-94
View File
@@ -1,94 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"time"
"github.com/go-kit/kit/endpoint"
kitot "github.com/go-kit/kit/tracing/opentracing"
kitgrpc "github.com/go-kit/kit/transport/grpc"
opentracing "github.com/opentracing/opentracing-go"
"github.com/mainflux/mainflux"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var _ mainflux.AuthNServiceClient = (*grpcClient)(nil)
type grpcClient struct {
issue endpoint.Endpoint
identify endpoint.Endpoint
timeout time.Duration
}
// NewClient returns new gRPC client instance.
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthNServiceClient {
return &grpcClient{
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
conn,
"mainflux.AuthNService",
"Issue",
encodeIssueRequest,
decodeIssueResponse,
mainflux.UserID{},
).Endpoint()),
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
conn,
"mainflux.AuthNService",
"Identify",
encodeIdentifyRequest,
decodeIdentifyResponse,
mainflux.UserID{},
).Endpoint()),
timeout: timeout,
}
}
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.issue(ctx, issueReq{issuer: req.GetIssuer(), keyType: req.Type})
if err != nil {
return nil, err
}
ir := res.(identityRes)
return &mainflux.Token{Value: ir.id}, ir.err
}
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(issueReq)
return &mainflux.IssueReq{Issuer: req.issuer, Type: req.keyType}, nil
}
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.UserID)
return identityRes{res.GetValue(), nil}, nil
}
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserID, error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
if err != nil {
return nil, err
}
ir := res.(identityRes)
return &mainflux.UserID{Value: ir.id}, ir.err
}
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(identityReq)
return &mainflux.Token{Value: req.token}, nil
}
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.UserID)
return identityRes{res.GetValue(), nil}, nil
}
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package grpc contains implementation of AuthN service gRPC API.
package grpc
-50
View File
@@ -1,50 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"time"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/authn"
context "golang.org/x/net/context"
)
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(issueReq)
if err := req.validate(); err != nil {
return nil, err
}
now := time.Now().UTC()
key := authn.Key{
Type: req.keyType,
IssuedAt: now,
}
k, err := svc.Issue(ctx, req.issuer, key)
if err != nil {
return identityRes{}, err
}
return identityRes{k.Secret, nil}, nil
}
}
func identifyEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(identityReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.Identify(ctx, req.token)
if err != nil {
return identityRes{}, err
}
return identityRes{id, nil}, nil
}
}
-166
View File
@@ -1,166 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc_test
import (
"fmt"
"net"
"testing"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/authn/mocks"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = 8081
secret = "secret"
email = "test@example.com"
)
var svc authn.Service
func newService() authn.Service {
repo := mocks.NewKeyRepository()
idp := mocks.NewIdentityProvider()
t := jwt.New(secret)
return authn.New(repo, idp, t)
}
func startGRPCServer(svc authn.Service, port int) {
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
server := grpc.NewServer()
mainflux.RegisterAuthNServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
go server.Serve(listener)
}
func TestIssue(t *testing.T) {
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
cases := []struct {
desc string
id string
kind uint32
err error
code codes.Code
}{
{
desc: "issue for user with valid token",
id: email,
kind: authn.UserKey,
err: nil,
code: codes.OK,
},
{
desc: "issue recovery key",
id: email,
kind: authn.RecoveryKey,
err: nil,
code: codes.OK,
},
{
desc: "issue API key",
id: userKey.Secret,
kind: authn.APIKey,
err: nil,
code: codes.OK,
},
{
desc: "issue for invalid key type",
id: email,
kind: 32,
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
},
{
desc: "issue for user that exist",
id: "",
kind: authn.APIKey,
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
},
}
for _, tc := range cases {
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Issuer: tc.id, Type: tc.kind})
e, ok := status.FromError(err)
assert.True(t, ok, "gRPC status can't be extracted from the error")
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
}
}
func TestIdentify(t *testing.T) {
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
apiKey, err := svc.Issue(context.Background(), userKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
cases := []struct {
desc string
token string
id string
err error
code codes.Code
}{
{
desc: "identify user with recovery token",
token: recoveryKey.Secret,
id: email,
err: nil,
code: codes.OK,
},
{
desc: "identify user with API token",
token: apiKey.Secret,
id: email,
err: nil,
code: codes.OK,
},
{
desc: "identify user with invalid user token",
token: "invalid",
id: "",
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
},
{
desc: "identify user that doesn't exist",
token: "",
id: "",
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
},
}
for _, tc := range cases {
id, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.id, id.GetValue()))
e, ok := status.FromError(err)
assert.True(t, ok, "gRPC status can't be extracted from the error")
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
}
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import "github.com/mainflux/mainflux/authn"
type identityReq struct {
token string
kind uint32
}
func (req identityReq) validate() error {
if req.token == "" {
return authn.ErrMalformedEntity
}
if req.kind != authn.UserKey &&
req.kind != authn.APIKey &&
req.kind != authn.RecoveryKey {
return authn.ErrMalformedEntity
}
return nil
}
type issueReq struct {
issuer string
keyType uint32
}
func (req issueReq) validate() error {
if req.issuer == "" {
return authn.ErrUnauthorizedAccess
}
if req.keyType != authn.UserKey &&
req.keyType != authn.APIKey &&
req.keyType != authn.RecoveryKey {
return authn.ErrMalformedEntity
}
return nil
}
-9
View File
@@ -1,9 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
type identityRes struct {
id string
err error
}
-90
View File
@@ -1,90 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
kitot "github.com/go-kit/kit/tracing/opentracing"
kitgrpc "github.com/go-kit/kit/transport/grpc"
mainflux "github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/errors"
opentracing "github.com/opentracing/opentracing-go"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ mainflux.AuthNServiceServer = (*grpcServer)(nil)
type grpcServer struct {
issue kitgrpc.Handler
identify kitgrpc.Handler
}
// NewServer returns new AuthnServiceServer instance.
func NewServer(tracer opentracing.Tracer, svc authn.Service) mainflux.AuthNServiceServer {
return &grpcServer{
issue: kitgrpc.NewServer(
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
decodeIssueRequest,
encodeIssueResponse,
),
identify: kitgrpc.NewServer(
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
decodeIdentifyRequest,
encodeIdentifyResponse,
),
}
}
func (s *grpcServer) Issue(ctx context.Context, req *mainflux.IssueReq) (*mainflux.Token, error) {
_, res, err := s.issue.ServeGRPC(ctx, req)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.Token), nil
}
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.UserID, error) {
_, res, err := s.identify.ServeGRPC(ctx, token)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.UserID), nil
}
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.IssueReq)
return issueReq{issuer: req.GetIssuer(), keyType: req.GetType()}, nil
}
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(identityRes)
return &mainflux.Token{Value: res.id}, encodeError(res.err)
}
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.Token)
return identityReq{token: req.GetValue()}, nil
}
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(identityRes)
return &mainflux.UserID{Value: res.id}, encodeError(res.err)
}
func encodeError(err error) error {
switch {
case errors.Contains(err, nil):
return nil
case errors.Contains(err, authn.ErrMalformedEntity):
return status.Error(codes.InvalidArgument, "received invalid token request")
case errors.Contains(err, authn.ErrUnauthorizedAccess):
return status.Error(codes.Unauthenticated, err.Error())
case errors.Contains(err, authn.ErrKeyExpired):
return status.Error(codes.Unauthenticated, err.Error())
default:
return status.Error(codes.Internal, "internal server error")
}
}
-18
View File
@@ -1,18 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc_test
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
svc = newService()
startGRPCServer(svc, port)
code := m.Run()
os.Exit(code)
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/authn"
)
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(issueKeyReq)
if err := req.validate(); err != nil {
return nil, err
}
now := time.Now().UTC()
newKey := authn.Key{
Issuer: req.issuer,
IssuedAt: now,
Type: req.Type,
}
duration := time.Duration(req.Duration * time.Second)
if duration != 0 {
exp := now.Add(duration)
newKey.ExpiresAt = exp
}
key, err := svc.Issue(ctx, req.issuer, newKey)
if err != nil {
return nil, err
}
res := issueKeyRes{
ID: key.ID,
Value: key.Secret,
IssuedAt: key.IssuedAt,
}
if !key.ExpiresAt.IsZero() {
res.ExpiresAt = &key.ExpiresAt
}
return res, nil
}
}
func revokeEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(keyReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.Revoke(ctx, req.issuer, req.id); err != nil {
return nil, err
}
return revokeKeyRes{}, nil
}
}
func retrieveEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(keyReq)
if err := req.validate(); err != nil {
return nil, err
}
key, err := svc.Retrieve(ctx, req.issuer, req.id)
if err != nil {
return nil, err
}
return key, nil
}
}
-288
View File
@@ -1,288 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
authn "github.com/mainflux/mainflux/authn"
httpapi "github.com/mainflux/mainflux/authn/api/http"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/authn/mocks"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
)
const (
secret = "secret"
contentType = "application/json"
invalidEmail = "userexample.com"
wrongID = "123e4567-e89b-12d3-a456-000000000042"
id = "123e4567-e89b-12d3-a456-000000000001"
email = "user@example.com"
)
type issueRequest struct {
Duration time.Duration `json:"duration,omitempty"`
Type uint32 `json:"type,omitempty"`
}
type testRequest struct {
client *http.Client
method string
url string
contentType string
token string
body io.Reader
}
func (tr testRequest) make() (*http.Response, error) {
req, err := http.NewRequest(tr.method, tr.url, tr.body)
if err != nil {
return nil, err
}
if tr.token != "" {
req.Header.Set("Authorization", tr.token)
}
if tr.contentType != "" {
req.Header.Set("Content-Type", tr.contentType)
}
req.Header.Set("Referer", "http://localhost")
return tr.client.Do(req)
}
func newService() authn.Service {
repo := mocks.NewKeyRepository()
idp := mocks.NewIdentityProvider()
t := jwt.New(secret)
return authn.New(repo, idp, t)
}
func newServer(svc authn.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc, mocktracer.New())
return httptest.NewServer(mux)
}
func toJSON(data interface{}) string {
jsonData, _ := json.Marshal(data)
return string(jsonData)
}
func TestIssue(t *testing.T) {
svc := newService()
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
uk := issueRequest{Type: authn.UserKey}
ak := issueRequest{Type: authn.APIKey, Duration: time.Hour}
rk := issueRequest{Type: authn.RecoveryKey}
cases := []struct {
desc string
req string
ct string
token string
status int
}{
{
desc: "issue user key",
req: toJSON(uk),
ct: contentType,
token: "",
status: http.StatusCreated,
},
{
desc: "issue API key",
req: toJSON(ak),
ct: contentType,
token: userKey.Secret,
status: http.StatusCreated,
},
{
desc: "issue recovery key",
req: toJSON(rk),
ct: contentType,
token: userKey.Secret,
status: http.StatusBadRequest,
},
{
desc: "issue user key wrong content type",
req: toJSON(uk),
ct: "", token: userKey.Secret,
status: http.StatusUnsupportedMediaType,
},
{
desc: "issue key wrong content type",
req: toJSON(rk),
ct: "",
token: userKey.Secret,
status: http.StatusUnsupportedMediaType,
},
{
desc: "issue key unauthorized",
req: toJSON(ak),
ct: contentType,
token: "wrong",
status: http.StatusForbidden,
},
{
desc: "issue recovery key with empty token",
req: toJSON(rk),
ct: contentType,
token: "",
status: http.StatusBadRequest,
},
{
desc: "issue key with invalid request",
req: "{",
ct: contentType,
token: "",
status: http.StatusBadRequest,
},
{
desc: "issue key with invalid JSON",
req: "{invalid}",
ct: contentType,
token: "",
status: http.StatusBadRequest,
},
{
desc: "issue key with invalid JSON content",
req: `{"Type":{"key":"value"}}`,
ct: contentType,
token: "",
status: http.StatusBadRequest,
},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/keys", ts.URL),
contentType: tc.ct,
token: tc.token,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestRetrieve(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
k, err := svc.Issue(context.Background(), loginKey.Secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
cases := []struct {
desc string
id string
token string
status int
}{
{
desc: "retrieve an existing key",
id: k.ID,
token: loginKey.Secret,
status: http.StatusOK,
},
{
desc: "retrieve a non-existing key",
id: "non-existing",
token: loginKey.Secret,
status: http.StatusNotFound,
},
{
desc: "retrieve a key unauthorized",
id: k.ID,
token: "wrong",
status: http.StatusForbidden,
},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodGet,
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
token: tc.token,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestRevoke(t *testing.T) {
svc := newService()
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
k, err := svc.Issue(context.Background(), userKey.Secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
cases := []struct {
desc string
id string
token string
status int
}{
{
desc: "revoke an existing key",
id: k.ID,
token: userKey.Secret,
status: http.StatusNoContent,
},
{
desc: "revoke a non-existing key",
id: "non-existing",
token: userKey.Secret,
status: http.StatusNoContent,
},
{
desc: "revoke a key unauthorized",
id: k.ID,
token: "wrong",
status: http.StatusForbidden},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodDelete,
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
token: tc.token,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
-39
View File
@@ -1,39 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
import (
"time"
"github.com/mainflux/mainflux/authn"
)
type issueKeyReq struct {
issuer string
Type uint32 `json:"type,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
}
// It is not possible to issue Reset key using HTTP API.
func (req issueKeyReq) validate() error {
if req.Type == authn.UserKey {
return nil
}
if req.issuer == "" || (req.Type != authn.APIKey) {
return authn.ErrMalformedEntity
}
return nil
}
type keyReq struct {
issuer string
id string
}
func (req keyReq) validate() error {
if req.issuer == "" || req.id == "" {
return authn.ErrMalformedEntity
}
return nil
}
-54
View File
@@ -1,54 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
import (
"net/http"
"time"
"github.com/mainflux/mainflux"
)
var (
_ mainflux.Response = (*issueKeyRes)(nil)
_ mainflux.Response = (*revokeKeyRes)(nil)
)
type issueKeyRes struct {
ID string `json:"id,omitempty"`
Value string `json:"value,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
func (res issueKeyRes) Code() int {
return http.StatusCreated
}
func (res issueKeyRes) Headers() map[string]string {
return map[string]string{}
}
func (res issueKeyRes) Empty() bool {
return res.Value == ""
}
type revokeKeyRes struct {
}
func (res revokeKeyRes) Code() int {
return http.StatusNoContent
}
func (res revokeKeyRes) Headers() map[string]string {
return map[string]string{}
}
func (res revokeKeyRes) Empty() bool {
return true
}
type errorRes struct {
Err string `json:"error"`
}
-128
View File
@@ -1,128 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
kitot "github.com/go-kit/kit/tracing/opentracing"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/errors"
"github.com/opentracing/opentracing-go"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const contentType = "application/json"
var errUnsupportedContentType = errors.New("unsupported content type")
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc authn.Service, tracer opentracing.Tracer) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
mux := bone.New()
mux.Post("/keys", kithttp.NewServer(
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
decodeIssue,
encodeResponse,
opts...,
))
mux.Get("/keys/:id", kithttp.NewServer(
kitot.TraceServer(tracer, "retrieve")(retrieveEndpoint(svc)),
decodeKeyReq,
encodeResponse,
opts...,
))
mux.Delete("/keys/:id", kithttp.NewServer(
kitot.TraceServer(tracer, "revoke")(revokeEndpoint(svc)),
decodeKeyReq,
encodeResponse,
opts...,
))
mux.GetFunc("/version", mainflux.Version("auth"))
mux.Handle("/metrics", promhttp.Handler())
return mux
}
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := issueKeyReq{
issuer: r.Header.Get("Authorization"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(authn.ErrMalformedEntity, err)
}
return req, nil
}
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
req := keyReq{
issuer: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
}
return req, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", contentType)
if ar, ok := response.(mainflux.Response); ok {
for k, v := range ar.Headers() {
w.Header().Set(k, v)
}
w.WriteHeader(ar.Code())
if ar.Empty() {
return nil
}
}
return json.NewEncoder(w).Encode(response)
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
switch {
case errors.Contains(err, authn.ErrMalformedEntity):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, authn.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, authn.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, authn.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, io.EOF):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, io.ErrUnexpectedEOF):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, errUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
default:
w.WriteHeader(http.StatusInternalServerError)
}
errorVal, ok := err.(errors.Error)
if ok {
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
w.Header().Set("Content-Type", contentType)
w.WriteHeader(http.StatusInternalServerError)
}
}
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
package api
import (
"context"
"fmt"
"time"
"github.com/mainflux/mainflux/authn"
log "github.com/mainflux/mainflux/logger"
)
var _ authn.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc authn.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc authn.Service, logger log.Logger) authn.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Issue(ctx context.Context, issuer string, newKey authn.Key) (key authn.Key, err error) {
defer func(begin time.Time) {
d := "infinite duration"
if !key.ExpiresAt.IsZero() {
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
}
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Issue(ctx, issuer, newKey)
}
func (lm *loggingMiddleware) Revoke(ctx context.Context, owner, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Revoke(ctx, owner, id)
}
func (lm *loggingMiddleware) Retrieve(ctx context.Context, owner, id string) (key authn.Key, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Retrieve(ctx, owner, id)
}
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Identify(ctx, key)
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/authn"
)
var _ authn.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc authn.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc authn.Service, counter metrics.Counter, latency metrics.Histogram) authn.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) Issue(ctx context.Context, issuer string, key authn.Key) (authn.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "issue").Add(1)
ms.latency.With("method", "issue").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Issue(ctx, issuer, key)
}
func (ms *metricsMiddleware) Revoke(ctx context.Context, issuer, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "revoke").Add(1)
ms.latency.With("method", "revoke").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Revoke(ctx, issuer, id)
}
func (ms *metricsMiddleware) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "retrieve").Add(1)
ms.latency.With("method", "retrieve").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Retrieve(ctx, issuer, id)
}
func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "identify").Add(1)
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Identify(ctx, key)
}
-10
View File
@@ -1,10 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
// IdentityProvider specifies an API for generating unique identifiers.
type IdentityProvider interface {
// ID generates the unique identifier.
ID() (string, error)
}
-111
View File
@@ -1,111 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package jwt_test
import (
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const secret = "test"
func key() authn.Key {
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
return authn.Key{
ID: "id",
Type: authn.UserKey,
Issuer: "user@email.com",
Secret: "",
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
ExpiresAt: exp,
}
}
func TestIssue(t *testing.T) {
tokenizer := jwt.New(secret)
emptyIssuer := key()
emptyIssuer.Issuer = ""
cases := []struct {
desc string
key authn.Key
err error
}{
{
desc: "issue new token",
key: key(),
err: nil,
},
}
for _, tc := range cases {
_, err := tokenizer.Issue(tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
}
}
func TestParse(t *testing.T) {
tokenizer := jwt.New(secret)
token, err := tokenizer.Issue(key())
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
userKey := key()
userKey.Type = authn.APIKey
userKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
userToken, err := tokenizer.Issue(userKey)
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
expKey := key()
expKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
expToken, err := tokenizer.Issue(expKey)
require.Nil(t, err, fmt.Sprintf("issuing expired key expected to succeed: %s", err))
cases := []struct {
desc string
key authn.Key
token string
err error
}{
{
desc: "parse valid key",
key: key(),
token: token,
err: nil,
},
{
desc: "parse ivalid key",
key: authn.Key{},
token: "invalid",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "parse expired key",
key: authn.Key{},
token: expToken,
err: authn.ErrKeyExpired,
},
{
desc: "parse expired user key",
key: userKey,
token: userToken,
err: nil,
},
}
for _, tc := range cases {
key, err := tokenizer.Parse(tc.token)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
}
}
-97
View File
@@ -1,97 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package jwt
import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/errors"
)
type claims struct {
jwt.StandardClaims
Type *uint32 `json:"type,omitempty"`
}
func (c claims) Valid() error {
if c.Type == nil || *c.Type > authn.APIKey {
return authn.ErrMalformedEntity
}
return c.StandardClaims.Valid()
}
type tokenizer struct {
secret string
}
// New returns new JWT Tokenizer.
func New(secret string) authn.Tokenizer {
return tokenizer{secret: secret}
}
func (svc tokenizer) Issue(key authn.Key) (string, error) {
claims := claims{
StandardClaims: jwt.StandardClaims{
Issuer: key.Issuer,
Subject: key.Secret,
IssuedAt: key.IssuedAt.UTC().Unix(),
},
Type: &key.Type,
}
if !key.ExpiresAt.IsZero() {
claims.ExpiresAt = key.ExpiresAt.UTC().Unix()
}
if key.ID != "" {
claims.Id = key.ID
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(svc.secret))
}
func (svc tokenizer) Parse(token string) (authn.Key, error) {
c := claims{}
_, err := jwt.ParseWithClaims(token, &c, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, authn.ErrUnauthorizedAccess
}
return []byte(svc.secret), nil
})
if err != nil {
if e, ok := err.(*jwt.ValidationError); ok && e.Errors == jwt.ValidationErrorExpired {
// Expired User key needs to be revoked.
if c.Type != nil && *c.Type == authn.APIKey {
return c.toKey(), nil
}
return authn.Key{}, errors.Wrap(authn.ErrKeyExpired, err)
}
return authn.Key{}, errors.Wrap(authn.ErrUnauthorizedAccess, err)
}
return c.toKey(), nil
}
func (c claims) toKey() authn.Key {
key := authn.Key{
ID: c.Id,
Issuer: c.Issuer,
Secret: c.Subject,
IssuedAt: time.Unix(c.IssuedAt, 0).UTC(),
}
if c.ExpiresAt != 0 {
key.ExpiresAt = time.Unix(c.ExpiresAt, 0).UTC()
}
// Default type is 0.
if c.Type != nil {
key.Type = *c.Type
}
return key
}
-58
View File
@@ -1,58 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
import (
"context"
"errors"
"time"
)
var (
// ErrInvalidKeyIssuedAt indicates that the Key is being used before it's issued.
ErrInvalidKeyIssuedAt = errors.New("invalid issue time")
// ErrKeyExpired indicates that the Key is expired.
ErrKeyExpired = errors.New("use of expired key")
)
const (
// UserKey is temporary User key received on successfull login.
UserKey uint32 = iota
// RecoveryKey represents a key for resseting password.
RecoveryKey
// APIKey enables the one to act on behalf of the user.
APIKey
)
// Key represents API key.
type Key struct {
ID string
Type uint32
Issuer string
Secret string
IssuedAt time.Time
ExpiresAt time.Time
}
// Expired verifies if the key is expired.
func (k Key) Expired() bool {
if k.Type == APIKey && k.ExpiresAt.IsZero() {
return false
}
return k.ExpiresAt.UTC().Before(time.Now().UTC())
}
// KeyRepository specifies Key persistence API.
type KeyRepository interface {
// Save persists the Key. A non-nil error is returned to indicate
// operation failure
Save(context.Context, Key) (string, error)
// Retrieve retrieves Key by its unique identifier.
Retrieve(context.Context, string, string) (Key, error)
// Remove removes Key with provided ID.
Remove(context.Context, string, string) error
}
-60
View File
@@ -1,60 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn_test
import (
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/stretchr/testify/assert"
)
func TestExpired(t *testing.T) {
exp := time.Now().Add(5 * time.Minute)
exp1 := time.Now()
cases := []struct {
desc string
key authn.Key
expired bool
}{
{
desc: "not expired key",
key: authn.Key{
IssuedAt: time.Now(),
ExpiresAt: exp,
},
expired: false,
},
{
desc: "expired key",
key: authn.Key{
IssuedAt: time.Now().UTC().Add(2 * time.Minute),
ExpiresAt: exp1,
},
expired: true,
},
{
desc: "user key with no expiration date",
key: authn.Key{
IssuedAt: time.Now(),
},
expired: true,
},
{
desc: "API key with no expiration date",
key: authn.Key{
IssuedAt: time.Now(),
Type: authn.APIKey,
},
expired: false,
},
}
for _, tc := range cases {
res := tc.key.Expired()
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
}
}
-32
View File
@@ -1,32 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"fmt"
"sync"
"github.com/mainflux/mainflux/authn"
)
var _ authn.IdentityProvider = (*identityProviderMock)(nil)
type identityProviderMock struct {
mu sync.Mutex
counter int
}
func (idp *identityProviderMock) ID() (string, error) {
idp.mu.Lock()
defer idp.mu.Unlock()
idp.counter++
return fmt.Sprintf("%s%012d", "123e4567-e89b-12d3-a456-", idp.counter), nil
}
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
// token will hold value provided by the caller.
func NewIdentityProvider() authn.IdentityProvider {
return &identityProviderMock{}
}
-55
View File
@@ -1,55 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"sync"
"github.com/mainflux/mainflux/authn"
)
var _ authn.KeyRepository = (*keyRepositoryMock)(nil)
type keyRepositoryMock struct {
mu sync.Mutex
keys map[string]authn.Key
}
// NewKeyRepository creates in-memory user repository
func NewKeyRepository() authn.KeyRepository {
return &keyRepositoryMock{
keys: make(map[string]authn.Key),
}
}
func (krm *keyRepositoryMock) Save(ctx context.Context, key authn.Key) (string, error) {
krm.mu.Lock()
defer krm.mu.Unlock()
if _, ok := krm.keys[key.ID]; ok {
return "", authn.ErrConflict
}
krm.keys[key.ID] = key
return key.ID, nil
}
func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
krm.mu.Lock()
defer krm.mu.Unlock()
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
return key, nil
}
return authn.Key{}, authn.ErrNotFound
}
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuer, id string) error {
krm.mu.Lock()
defer krm.mu.Unlock()
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
delete(krm.keys, id)
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package postgres contains Key repository implementations using
// PostgreSQL as the underlying database.
package postgres
-65
View File
@@ -1,65 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"fmt"
"github.com/jmoiron/sqlx"
migrate "github.com/rubenv/sql-migrate"
)
// Config defines the options that are used when connecting to a PostgreSQL instance
type Config struct {
Host string
Port string
User string
Pass string
Name string
SSLMode string
SSLCert string
SSLKey string
SSLRootCert string
}
// Connect creates a connection to the PostgreSQL instance and applies any
// unapplied database migrations. A non-nil error is returned to indicate
// failure.
func Connect(cfg Config) (*sqlx.DB, error) {
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
db, err := sqlx.Open("postgres", url)
if err != nil {
return nil, err
}
if err := migrateDB(db); err != nil {
return nil, err
}
return db, nil
}
func migrateDB(db *sqlx.DB) error {
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "authn",
Up: []string{
`CREATE TABLE IF NOT EXISTS keys (
id UUID NOT NULL,
type SMALLINT,
issuer VARCHAR(254) NOT NULL,
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP,
PRIMARY KEY (id, issuer)
)`,
},
Down: []string{"DROP TABLE IF EXISTS keys"},
},
},
}
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
return err
}
-119
View File
@@ -1,119 +0,0 @@
package postgres
import (
"context"
"database/sql"
"time"
"github.com/lib/pq"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/errors"
)
var (
errSave = errors.New("failed to save key in database")
errRetrieve = errors.New("failed to retrieve key from database")
errDelete = errors.New("failed to delete key from database")
)
var _ authn.KeyRepository = (*repo)(nil)
const (
errDuplicate = "unique_violation"
errInvalid = "invalid_text_representation"
)
type repo struct {
db Database
}
// New instantiates a PostgreSQL implementation of key repository.
func New(db Database) authn.KeyRepository {
return &repo{
db: db,
}
}
func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
q := `INSERT INTO keys (id, type, issuer, issued_at, expires_at)
VALUES (:id, :type, :issuer, :issued_at, :expires_at)`
dbKey := toDBKey(key)
if _, err := kr.db.NamedExecContext(ctx, q, dbKey); err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
if pqErr.Code.Name() == errDuplicate {
return "", errors.Wrap(authn.ErrConflict, pqErr)
}
}
return "", errors.Wrap(errSave, err)
}
return dbKey.ID, nil
}
func (kr repo) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
q := `SELECT id, type, issuer, issued_at, expires_at FROM keys WHERE issuer = $1 AND id = $2`
key := dbKey{}
if err := kr.db.QueryRowxContext(ctx, q, issuer, id).StructScan(&key); err != nil {
pqErr, ok := err.(*pq.Error)
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
return authn.Key{}, errors.Wrap(authn.ErrNotFound, err)
}
return authn.Key{}, errors.Wrap(errRetrieve, err)
}
return toKey(key), nil
}
func (kr repo) Remove(ctx context.Context, issuer, id string) error {
q := `DELETE FROM keys WHERE issuer = :issuer AND id = :id`
key := dbKey{
ID: id,
Issuer: issuer,
}
if _, err := kr.db.NamedExecContext(ctx, q, key); err != nil {
return errors.Wrap(errDelete, err)
}
return nil
}
type dbKey struct {
ID string `db:"id"`
Type uint32 `db:"type"`
Issuer string `db:"issuer"`
Revoked bool `db:"revoked"`
IssuedAt time.Time `db:"issued_at"`
ExpiresAt sql.NullTime `db:"expires_at"`
}
func toDBKey(key authn.Key) dbKey {
ret := dbKey{
ID: key.ID,
Type: key.Type,
Issuer: key.Issuer,
IssuedAt: key.IssuedAt,
}
if !key.ExpiresAt.IsZero() {
ret.ExpiresAt = sql.NullTime{Time: key.ExpiresAt, Valid: true}
}
return ret
}
func toKey(key dbKey) authn.Key {
ret := authn.Key{
ID: key.ID,
Type: key.Type,
Issuer: key.Issuer,
IssuedAt: key.IssuedAt,
}
if key.ExpiresAt.Valid {
ret.ExpiresAt = key.ExpiresAt.Time
}
return ret
}
-149
View File
@@ -1,149 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/authn/postgres"
"github.com/mainflux/mainflux/authn/uuid"
"github.com/mainflux/mainflux/errors"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
)
func TestKeySave(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
email := "user-save@example.com"
expTime := time.Now().Add(5 * time.Minute)
idp := uuid.New()
id, _ := idp.ID()
cases := []struct {
desc string
key authn.Key
err error
}{
{
desc: "save a new key",
key: authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
},
err: nil,
},
{
desc: "save with duplicate id",
key: authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
},
err: authn.ErrConflict,
},
}
for _, tc := range cases {
_, err := repo.Save(context.Background(), tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestKeyRetrieve(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
email := "user-save@example.com"
expTime := time.Now().Add(5 * time.Minute)
idp := uuid.New()
id, _ := idp.ID()
key := authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
}
_, err := repo.Save(context.Background(), key)
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "retrieve an existing key",
id: key.ID,
issuer: key.Issuer,
err: nil,
},
{
desc: "retrieve unauthorized",
id: key.ID,
issuer: "",
err: authn.ErrNotFound,
},
{
desc: "retrieve unknown key",
id: "",
issuer: key.Issuer,
err: authn.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.Retrieve(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestKeyRemove(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
email := "user-save@example.com"
expTime := time.Now().Add(5 * time.Minute)
idp := uuid.New()
id, _ := idp.ID()
key := authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
}
_, err := repo.Save(opentracing.ContextWithSpan(context.Background(), opentracing.StartSpan("")), key)
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "remove an existing key",
id: key.ID,
issuer: key.Issuer,
err: nil,
},
{
desc: "remove key that does not exist",
id: key.ID,
issuer: key.Issuer,
err: nil,
},
}
for _, tc := range cases {
err := repo.Remove(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
-78
View File
@@ -1,78 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package postgres_test contains tests for PostgreSQL repository
// implementations.
package postgres_test
import (
"database/sql"
"fmt"
"log"
"os"
"testing"
"github.com/jmoiron/sqlx"
"github.com/mainflux/mainflux/authn/postgres"
dockertest "github.com/ory/dockertest/v3"
)
const wrong string = "wrong-value"
var db *sqlx.DB
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
cfg := []string{
"POSTGRES_USER=test",
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.2-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
port := container.GetPort("5432/tcp")
if err := pool.Retry(func() error {
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
db, err := sql.Open("postgres", url)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
dbConfig := postgres.Config{
Host: "localhost",
Port: port,
User: "test",
Pass: "test",
Name: "test",
SSLMode: "disable",
SSLCert: "",
SSLKey: "",
SSLRootCert: "",
}
if db, err = postgres.Connect(dbConfig); err != nil {
log.Fatalf("Could not setup test DB connection: %s", err)
}
code := m.Run()
// defers will not be run when using os.Exit
db.Close()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}
-51
View File
@@ -1,51 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/opentracing/opentracing-go"
)
var _ Database = (*database)(nil)
type database struct {
db *sqlx.DB
}
// Database provides a database interface
type Database interface {
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
}
// NewDatabase creates a ThingDatabase instance
func NewDatabase(db *sqlx.DB) Database {
return &database{
db: db,
}
}
func (d database) NamedExecContext(ctx context.Context, query string, args interface{}) (sql.Result, error) {
addSpanTags(ctx, query)
return d.db.NamedExecContext(ctx, query, args)
}
func (d database) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row {
addSpanTags(ctx, query)
return d.db.QueryRowxContext(ctx, query, args...)
}
func addSpanTags(ctx context.Context, query string) {
span := opentracing.SpanFromContext(ctx)
if span != nil {
span.SetTag("sql.statement", query)
span.SetTag("span.kind", "client")
span.SetTag("peer.service", "postgres")
span.SetTag("db.type", "sql")
}
}
-192
View File
@@ -1,192 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
import (
"context"
"time"
"github.com/mainflux/mainflux/errors"
)
const (
loginDuration = 10 * time.Hour
recoveryDuration = 5 * time.Minute
issuerName = "mainflux.authn"
)
var (
// ErrUnauthorizedAccess represents unauthorized access.
ErrUnauthorizedAccess = errors.New("unauthorized access")
// ErrMalformedEntity indicates malformed entity specification (e.g.
// invalid owner or ID).
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrNotFound indicates a non-existing entity request.
ErrNotFound = errors.New("entity not found")
// ErrConflict indicates that entity already exists.
ErrConflict = errors.New("entity already exists")
errIssueUser = errors.New("failed to issue new user key")
errIssueTmp = errors.New("failed to issue new temporary key")
errRevoke = errors.New("failed to remove key")
errRetrieve = errors.New("failed to retrieve key data")
errIdentify = errors.New("failed to validate token")
)
// Service specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// Issue issues a new Key.
Issue(context.Context, string, Key) (Key, error)
// Revoke removes the Key with the provided id that is
// issued by the user identified by the provided key.
Revoke(context.Context, string, string) error
// Retrieve retrieves data for the Key identified by the provided
// ID, that is issued by the user identified by the provided key.
Retrieve(context.Context, string, string) (Key, error)
// Identify validates token token. If token is valid, content
// is returned. If token is invalid, or invocation failed for some
// other reason, non-nil error value is returned in response.
Identify(context.Context, string) (string, error)
}
var _ Service = (*service)(nil)
type service struct {
keys KeyRepository
idp IdentityProvider
tokenizer Tokenizer
}
// New instantiates the auth service implementation.
func New(keys KeyRepository, idp IdentityProvider, tokenizer Tokenizer) Service {
return &service{
tokenizer: tokenizer,
keys: keys,
idp: idp,
}
}
func (svc service) Issue(ctx context.Context, issuer string, key Key) (Key, error) {
if key.IssuedAt.IsZero() {
return Key{}, ErrInvalidKeyIssuedAt
}
switch key.Type {
case APIKey:
return svc.userKey(ctx, issuer, key)
case RecoveryKey:
return svc.tmpKey(issuer, recoveryDuration, key)
default:
return svc.tmpKey(issuer, loginDuration, key)
}
}
func (svc service) Revoke(ctx context.Context, issuer, id string) error {
email, err := svc.login(issuer)
if err != nil {
return errors.Wrap(errRevoke, err)
}
if err := svc.keys.Remove(ctx, email, id); err != nil {
return errors.Wrap(errRevoke, err)
}
return nil
}
func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error) {
email, err := svc.login(issuer)
if err != nil {
return Key{}, errors.Wrap(errRetrieve, err)
}
return svc.keys.Retrieve(ctx, email, id)
}
func (svc service) Identify(ctx context.Context, token string) (string, error) {
c, err := svc.tokenizer.Parse(token)
if err != nil {
return "", errors.Wrap(errIdentify, err)
}
switch c.Type {
case APIKey:
k, err := svc.keys.Retrieve(ctx, c.Issuer, c.ID)
if err != nil {
return "", err
}
// Auto revoke expired key.
if k.Expired() {
svc.keys.Remove(ctx, c.Issuer, c.ID)
return "", ErrKeyExpired
}
return c.Issuer, nil
case RecoveryKey, UserKey:
if c.Issuer != issuerName {
return "", ErrUnauthorizedAccess
}
return c.Secret, nil
default:
return "", ErrUnauthorizedAccess
}
}
func (svc service) tmpKey(issuer string, duration time.Duration, key Key) (Key, error) {
key.Secret = issuer
key.Issuer = issuerName
key.ExpiresAt = key.IssuedAt.Add(duration)
val, err := svc.tokenizer.Issue(key)
if err != nil {
return Key{}, errors.Wrap(errIssueTmp, err)
}
key.Secret = val
return key, nil
}
func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, error) {
email, err := svc.login(issuer)
if err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
key.Issuer = email
id, err := svc.idp.ID()
if err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
key.ID = id
value, err := svc.tokenizer.Issue(key)
if err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
key.Secret = value
if _, err := svc.keys.Save(ctx, key); err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
return key, nil
}
func (svc service) login(token string) (string, error) {
c, err := svc.tokenizer.Parse(token)
if err != nil {
return "", err
}
// Only user key token is valid for login.
if c.Type != UserKey {
return "", ErrUnauthorizedAccess
}
if c.Secret == "" {
return "", ErrUnauthorizedAccess
}
return c.Secret, nil
}
-279
View File
@@ -1,279 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/authn/mocks"
"github.com/mainflux/mainflux/errors"
"github.com/stretchr/testify/assert"
)
const (
secret = "secret"
email = "test@example.com"
)
func newService() authn.Service {
repo := mocks.NewKeyRepository()
idp := mocks.NewIdentityProvider()
t := jwt.New(secret)
return authn.New(repo, idp, t)
}
func TestIssue(t *testing.T) {
svc := newService()
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
cases := []struct {
desc string
key authn.Key
issuer string
err error
}{
{
desc: "issue user key",
key: authn.Key{
Type: authn.UserKey,
IssuedAt: time.Now(),
},
issuer: email,
err: nil,
},
{
desc: "issue user key no issue time",
key: authn.Key{
Type: authn.UserKey,
},
issuer: email,
err: authn.ErrInvalidKeyIssuedAt,
},
{
desc: "issue API key",
key: authn.Key{
Type: authn.APIKey,
IssuedAt: time.Now(),
},
issuer: userKey.Secret,
err: nil,
},
{
desc: "issue API key unauthorized",
key: authn.Key{
Type: authn.APIKey,
IssuedAt: time.Now(),
},
issuer: "",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "issue API key no issue time",
key: authn.Key{
Type: authn.APIKey,
},
issuer: userKey.Secret,
err: authn.ErrInvalidKeyIssuedAt,
},
{
desc: "issue recovery key",
key: authn.Key{
Type: authn.RecoveryKey,
IssuedAt: time.Now(),
},
issuer: userKey.Secret,
err: nil,
},
{
desc: "issue recovery key no issue time",
key: authn.Key{
Type: authn.RecoveryKey,
},
issuer: userKey.Secret,
err: authn.ErrInvalidKeyIssuedAt,
},
}
for _, tc := range cases {
_, err := svc.Issue(context.Background(), tc.issuer, tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRevoke(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := authn.Key{
Type: authn.APIKey,
IssuedAt: time.Now(),
}
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "revoke user key",
id: newKey.ID,
issuer: loginKey.Secret,
err: nil,
},
{
desc: "revoke non-existing user key",
id: newKey.ID,
issuer: loginKey.Secret,
err: nil,
},
{
desc: "revoke unauthorized",
id: newKey.ID,
issuer: "",
err: authn.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.Revoke(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieve(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := authn.Key{
ID: "id",
Type: authn.APIKey,
IssuedAt: time.Now(),
}
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
resetKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "retrieve user key",
id: newKey.ID,
issuer: loginKey.Secret,
err: nil,
},
{
desc: "retrieve non-existing user key",
id: "invalid",
issuer: loginKey.Secret,
err: authn.ErrNotFound,
},
{
desc: "retrieve unauthorized",
id: newKey.ID,
issuer: "wrong",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "retrieve with user key",
id: newKey.ID,
issuer: userKey.Secret,
err: authn.ErrUnauthorizedAccess,
},
{
desc: "retrieve with reset key",
id: newKey.ID,
issuer: resetKey.Secret,
err: authn.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
_, err := svc.Retrieve(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestIdentify(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
exp1 := time.Now().Add(-2 * time.Second)
expKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
assert.Nil(t, err, fmt.Sprintf("Issuing expired user key expected to succeed: %s", err))
invalidKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: 22, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "identify login key",
key: loginKey.Secret,
id: email,
err: nil,
},
{
desc: "identify recovery key",
key: recoveryKey.Secret,
id: email,
err: nil,
},
{
desc: "identify user key",
key: userKey.Secret,
id: email,
err: nil,
},
{
desc: "identify expired user key",
key: expKey.Secret,
id: "",
err: authn.ErrKeyExpired,
},
{
desc: "identify expired key",
key: invalidKey.Secret,
id: "",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "identify invalid key",
key: "invalid",
id: "",
err: authn.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
id, err := svc.Identify(context.Background(), tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.id, id, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.id, id))
}
}
-142
View File
@@ -1,142 +0,0 @@
swagger: "2.0"
info:
title: Mainflux authentication service
description: HTTP API for managing platform API keys.
version: "1.0.0"
consumes:
- "application/json"
produces:
- "application/json"
paths:
/keys:
post:
summary: Issue API key
description: |
Generates a new API key. Thew new API key will
be uniquely identified by its ID.
tags:
- authn
parameters:
- name: key
description: JSON-formatted document describing the new key.
in: body
schema:
$ref: "#/definitions/KeyRequest"
required: true
responses:
201:
description: Issued new key.
400:
description: Failed due to malformed JSON.
409:
description: Failed due to using already existing ID.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
get:
summary: Gets API key details.
description: |
Gets API key details for the given key.
tags:
- authn
parameters:
- $ref: "#/parameters/Authorization"
- name: id
description: API Key id.
in: path
type: string
required: true
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/Key"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Revoke API key
description: |
Revoke API key identified by the given ID.
tags:
- authn
parameters:
- $ref: "#/parameters/Authorization"
- name: id
description: API Key id.
in: path
type: string
required: true
responses:
204:
description: Key revoked.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
definitions:
Key:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: API key unique identifier
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently
issuer:
type: string
format: string
example: "test@example.com"
description: User's email or service identifier of API key issuer
secret:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
description: API Key value.
issued_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the key is generated
expires_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the Key expires
KeyRequest:
type: object
properties:
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently
issuer:
type: string
format: e-mail
example: "test@example.com"
description: User's email or service identifier of API key issuer
duration:
type: number
format: integer
example: 23456
description: Number of seconds issued token is valid for.
parameters:
Authorization:
name: Authorization
description: Login key secret (User's access token).
in: header
type: string
required: true
responses:
ServiceError:
description: Unexpected server-side error occurred.
-13
View File
@@ -1,13 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
// Tokenizer specifies API for encoding and decoding between string and Key.
type Tokenizer interface {
// Issue converts API Key to its string representation.
Issue(Key) (string, error)
// Parse extracts API Key data from string token.
Parse(string) (Key, error)
}
-72
View File
@@ -1,72 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package tracing contains middlewares that will add spans
// to existing traces.
package tracing
import (
"context"
"github.com/mainflux/mainflux/authn"
opentracing "github.com/opentracing/opentracing-go"
)
const (
saveOp = "save"
retrieveOp = "retrieve_by_id"
revokeOp = "remove"
)
var _ authn.KeyRepository = (*keyRepositoryMiddleware)(nil)
// keyRepositoryMiddleware tracks request and their latency, and adds spans
// to context.
type keyRepositoryMiddleware struct {
tracer opentracing.Tracer
repo authn.KeyRepository
}
// New tracks request and their latency, and adds spans
// to context.
func New(repo authn.KeyRepository, tracer opentracing.Tracer) authn.KeyRepository {
return keyRepositoryMiddleware{
tracer: tracer,
repo: repo,
}
}
func (krm keyRepositoryMiddleware) Save(ctx context.Context, key authn.Key) (string, error) {
span := createSpan(ctx, krm.tracer, saveOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return krm.repo.Save(ctx, key)
}
func (krm keyRepositoryMiddleware) Retrieve(ctx context.Context, owner, id string) (authn.Key, error) {
span := createSpan(ctx, krm.tracer, retrieveOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return krm.repo.Retrieve(ctx, owner, id)
}
func (krm keyRepositoryMiddleware) Remove(ctx context.Context, owner, id string) error {
span := createSpan(ctx, krm.tracer, revokeOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return krm.repo.Remove(ctx, owner, id)
}
func createSpan(ctx context.Context, tracer opentracing.Tracer, opName string) opentracing.Span {
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
return tracer.StartSpan(
opName,
opentracing.ChildOf(parentSpan.Context()),
)
}
return tracer.StartSpan(opName)
}
-32
View File
@@ -1,32 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package uuid provides a UUID identity provider.
package uuid
import (
"github.com/gofrs/uuid"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/errors"
)
// errGeneratingID indicates error in generating UUID
var errGeneratingID = errors.New("failed to generate uuid")
var _ authn.IdentityProvider = (*uuidIdentityProvider)(nil)
type uuidIdentityProvider struct{}
// New instantiates a UUID identity provider.
func New() authn.IdentityProvider {
return &uuidIdentityProvider{}
}
func (idp *uuidIdentityProvider) ID() (string, error) {
id, err := uuid.NewV4()
if err != nil {
return "", errors.Wrap(errGeneratingID, err)
}
return id.String(), nil
}
+1151
View File
File diff suppressed because it is too large Load Diff
-162
View File
@@ -1,162 +0,0 @@
# BOOTSTRAP SERVICE
New devices need to be configured properly and connected to the Mainflux. Bootstrap service is used in order to accomplish that. This service provides the following features:
1) Creating new Mainflux Things
2) Providing basic configuration for the newly created Things
3) Enabling/disabling Things
Pre-provisioning a new Thing is as simple as sending Configuration data to the Bootstrap service. Once the Thing is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Things. Only enabled Things can exchange messages over Mainflux. Bootstrapping does not implicitly enable Things, it has to be done manually.
In order to bootstrap successfully, the Thing needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Thing is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Thing will be saved so that it can be provisioned later.
## Thing Configuration Entity
Thing Configuration consists of two logical parts: the custom configuration that can be interpreted by the Thing itself and Mainflux-related configuration. Mainflux config contains:
1) corresponding Mainflux Thing ID
2) corresponding Mainflux Thing key
3) list of the Mainflux channels the Thing is connected to
>Note: list of channels contains IDs of the Mainflux channels. These channels are _pre-provisioned_ on the Mainflux side and, unlike corresponding Mainflux Thing, Bootstrap service is not able to create Mainflux Channels.
Enabling and disabling Thing (adding Thing to/from whitelist) is as simple as connecting corresponding Mainflux Thing to the given list of Channels. Configuration keeps _state_ of the Thing:
| State | What it means |
|----------|--------------------------------------------------------|
| Inactive | Thing is created, but isn't enabled |
| Active | Thing is able to communicate using Mainflux |
Switching between states `Active` and `Inactive` enables and disables Thing, respectively.
Thing configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Thing. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
## Configuration
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
| Variable | Description | Default |
|-------------------------------|-------------------------------------------------------------------------|----------------------- |
| MF_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | error |
| MF_BOOTSTRAP_DB_HOST | Database host address | localhost |
| MF_BOOTSTRAP_DB_PORT | Database host port | 5432 |
| MF_BOOTSTRAP_DB_USER | Database user | mainflux |
| MF_BOOTSTRAP_DB_PASS | Database password | mainflux |
| MF_BOOTSTRAP_DB | Name of the database used by the service | bootstrap |
| MF_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| MF_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | |
| MF_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | |
| MF_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
| MF_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
| MF_BOOTSTRAP_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
| MF_BOOTSTRAP_CA_CERTS | Path to trusted CAs in PEM format | |
| MF_BOOTSTRAP_PORT | Bootstrap service HTTP port | 8180 |
| MF_BOOTSTRAP_SERVER_CERT | Path to server certificate in pem format | |
| MF_BOOTSTRAP_SERVER_KEY | Path to server key in pem format | |
| MF_SDK_BASE_URL | Base url for Mainflux SDK | http://localhost |
| MF_SDK_THINGS_PREFIX | SDK prefix for Things service | |
| MF_THINGS_ES_URL | Things service event source URL | localhost:6379 |
| MF_THINGS_ES_PASS | Things service event source password | |
| MF_THINGS_ES_DB | Things service event source database | 0 |
| MF_BOOTSTRAP_ES_URL | Bootstrap service event source URL | localhost:6379 |
| MF_BOOTSTRAP_ES_PASS | Bootstrap service event source password | |
| MF_BOOTSTRAP_ES_DB | Bootstrap service event source database | 0 |
| MF_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
| MF_AUTHN_GRPC_URL | AuthN service gRPC URL | localhost:8181 |
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1 |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
bootstrap:
image: mainflux/bootstrap:latest
container_name: mainflux-bootstrap
depends_on:
- bootstrap-db
restart: on-failure
ports:
- 8200:8200
environment:
MF_BOOTSTRAP_LOG_LEVEL: [Bootstrap log level]
MF_BOOTSTRAP_DB_HOST: [Database host address]
MF_BOOTSTRAP_DB_PORT: [Database host port]
MF_BOOTSTRAP_DB_USER: [Database user]
MF_BOOTSTRAP_DB_PASS: [Database password]
MF_BOOTSTRAP_DB: [Name of the database used by the service]
MF_BOOTSTRAP_DB_SSL_MODE: [SSL mode to connect to the database with]
MF_BOOTSTRAP_DB_SSL_CERT: [Path to the PEM encoded certificate file]
MF_BOOTSTRAP_DB_SSL_KEY: [Path to the PEM encoded key file]
MF_BOOTSTRAP_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
MF_BOOTSTRAP_ENCRYPT_KEY: [Hex-encoded encryption key used for secure bootstrap]
MF_BOOTSTRAP_CLIENT_TLS: [Boolean value to enable/disable client TLS]
MF_BOOTSTRAP_CA_CERTS: [Path to trusted CAs in PEM format]
MF_BOOTSTRAP_PORT: 8200
MF_BOOTSTRAP_SERVER_CERT: [String path to server cert in pem format]
MF_BOOTSTRAP_SERVER_KEY: [String path to server key in pem format]
MF_SDK_BASE_URL: [Base SDK URL for the Mainflux services]
MF_SDK_THINGS_PREFIX: [SDK prefix for Things service]
MF_THINGS_ES_URL: [Things service event source URL]
MF_THINGS_ES_PASS: [Things service event source password]
MF_THINGS_ES_DB: [Things service event source database]
MF_BOOTSTRAP_ES_URL: [Bootstrap service event source URL]
MF_BOOTSTRAP_ES_PASS: [Bootstrap service event source password]
MF_BOOTSTRAP_ES_DB: [Bootstrap service event source database]
MF_BOOTSTRAP_EVENT_CONSUMER: [Bootstrap service event source consumer name]
MF_JAEGER_URL: [Jaeger server URL]
MF_AUTHN_GRPC_URL: [AuthN service gRPC URL]
MF_AUTHN_GRPC_TIMEOUT: [AuthN service gRPC request timeout in seconds]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
git clone https://github.com/mainflux/mainflux
cd mainflux
# compile the service
make bootstrap
# copy binary to bin
make install
# set the environment variables and run the service
MF_BOOTSTRAP_LOG_LEVEL=[Bootstrap log level] \
MF_BOOTSTRAP_DB_HOST=[Database host address] \
MF_BOOTSTRAP_DB_PORT=[Database host port] \
MF_BOOTSTRAP_DB_USER=[Database user] \
MF_BOOTSTRAP_DB_PASS=[Database password] \
MF_BOOTSTRAP_DB=[Name of the database used by the service] \
MF_BOOTSTRAP_DB_SSL_MODE=[SSL mode to connect to the database with] \
MF_BOOTSTRAP_DB_SSL_CERT=[Path to the PEM encoded certificate file] \
MF_BOOTSTRAP_DB_SSL_KEY=[Path to the PEM encoded key file] \
MF_BOOTSTRAP_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] \
MF_BOOTSTRAP_ENCRYPT_KEY=[Hex-encoded encryption key used for secure bootstrap] \
MF_BOOTSTRAP_CLIENT_TLS=[Boolean value to enable/disable client TLS] \
MF_BOOTSTRAP_CA_CERTS=[Path to trusted CAs in PEM format] \
MF_BOOTSTRAP_PORT=[Service HTTP port] \
MF_BOOTSTRAP_SERVER_CERT=[Path to server certificate] \
MF_BOOTSTRAP_SERVER_KEY=[Path to server key] \
MF_SDK_BASE_URL=[Base SDK URL for the Mainflux services] \
MF_SDK_THINGS_PREFIX=[SDK prefix for Things service] \
MF_JAEGER_URL=[Jaeger server URL] \
MF_AUTHN_GRPC_URL=[AuthN service gRPC URL] \
MF_AUTHN_GRPC_TIMEOUT=[AuthN service gRPC request timeout in seconds] \
$GOBIN/mainflux-bootstrap
```
Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. This will enable TLS against the Users gRPC endpoint trusting only those CAs that are provided.
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yml).
[doc]: http://mainflux.readthedocs.io
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package api contains implementation of bootstrap service HTTP API.
package api
-257
View File
@@ -1,257 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/bootstrap"
)
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(addReq)
if err := req.validate(); err != nil {
return nil, err
}
channels := []bootstrap.Channel{}
for _, c := range req.Channels {
channels = append(channels, bootstrap.Channel{ID: c})
}
config := bootstrap.Config{
MFThing: req.ThingID,
ExternalID: req.ExternalID,
ExternalKey: req.ExternalKey,
MFChannels: channels,
Name: req.Name,
ClientCert: req.ClientCert,
ClientKey: req.ClientKey,
CACert: req.CACert,
Content: req.Content,
}
saved, err := svc.Add(req.token, config)
if err != nil {
return nil, err
}
res := configRes{
id: saved.MFThing,
created: true,
}
return res, nil
}
}
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateCertReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.UpdateCert(req.key, req.thingID, req.ClientCert, req.ClientKey, req.CACert); err != nil {
return nil, err
}
res := configRes{}
return res, nil
}
}
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
if err := req.validate(); err != nil {
return nil, err
}
config, err := svc.View(req.key, req.id)
if err != nil {
return nil, err
}
var channels []channelRes
for _, ch := range config.MFChannels {
channels = append(channels, channelRes{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
res := viewRes{
MFThing: config.MFThing,
MFKey: config.MFKey,
Channels: channels,
ExternalID: config.ExternalID,
ExternalKey: config.ExternalKey,
Name: config.Name,
Content: config.Content,
State: config.State,
}
return res, nil
}
}
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateReq)
if err := req.validate(); err != nil {
return nil, err
}
config := bootstrap.Config{
MFThing: req.id,
Name: req.Name,
Content: req.Content,
}
if err := svc.Update(req.key, config); err != nil {
return nil, err
}
res := configRes{
id: config.MFThing,
created: false,
}
return res, nil
}
}
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateConnReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.UpdateConnections(req.key, req.id, req.Channels); err != nil {
return nil, err
}
res := configRes{
id: req.id,
created: false,
}
return res, nil
}
}
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listReq)
if err := req.validate(); err != nil {
return nil, err
}
page, err := svc.List(req.key, req.filter, req.offset, req.limit)
if err != nil {
return nil, err
}
switch {
case req.filter.Unknown:
res := listUnknownRes{}
for _, cfg := range page.Configs {
res.Configs = append(res.Configs, unknownRes{
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
})
}
return res, nil
default:
res := listRes{
Total: page.Total,
Offset: page.Offset,
Limit: page.Limit,
Configs: []viewRes{},
}
for _, cfg := range page.Configs {
var channels []channelRes
for _, ch := range cfg.MFChannels {
channels = append(channels, channelRes{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
view := viewRes{
MFThing: cfg.MFThing,
MFKey: cfg.MFKey,
Channels: channels,
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Name: cfg.Name,
Content: cfg.Content,
State: cfg.State,
}
res.Configs = append(res.Configs, view)
}
return res, nil
}
}
}
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
if err := req.validate(); err != nil {
return removeRes{}, err
}
if err := svc.Remove(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(bootstrapReq)
if err := req.validate(); err != nil {
return nil, err
}
cfg, err := svc.Bootstrap(req.key, req.id, secure)
if err != nil {
return nil, err
}
return reader.ReadConfig(cfg, secure)
}
}
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(changeStateReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.ChangeState(req.key, req.id, req.State); err != nil {
return nil, err
}
return stateRes{}, nil
}
}
File diff suppressed because it is too large Load Diff
-195
View File
@@ -1,195 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux/bootstrap"
log "github.com/mainflux/mainflux/logger"
)
var _ bootstrap.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc bootstrap.Service
}
// NewLoggingMiddleware adds logging facilities to the core service.
func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method add for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Add(token, cfg)
}
func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method view for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.View(token, id)
}
func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update for token %s and thing %s took %s to complete", token, cfg.MFThing, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Update(token, cfg)
}
func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, caCert string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_cert for thing with id %s took %s to complete", thingID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateCert(token, thingID, clientCert, clientKey, caCert)
}
func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_connections for token %s and thing %s took %s to complete", token, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateConnections(token, id, connections)
}
func (lm *loggingMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list for token %s and offset %d and limit %d took %s to complete", token, offset, limit, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.List(token, filter, offset, limit)
}
func (lm *loggingMiddleware) Remove(token, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove for token %s and thing %s took %s to complete", token, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Remove(token, id)
}
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method bootstrap for thing with external id %s took %s to complete", externalID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Bootstrap(externalKey, externalID, secure)
}
func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method change_state for token %s and thing %s took %s to complete", token, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ChangeState(token, id, state)
}
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_channel_handler for channel %s took %s to complete", channel.ID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateChannelHandler(channel)
}
func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_config_handler for config %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RemoveConfigHandler(id)
}
func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_channel_handler for channel %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RemoveChannelHandler(id)
}
func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method disconnect_thing_handler for channel %s and thing %s took %s to complete", channelID, thingID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.DisconnectThingHandler(channelID, thingID)
}
-148
View File
@@ -1,148 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/bootstrap"
)
var _ bootstrap.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc bootstrap.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (mm *metricsMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "add").Add(1)
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Add(token, cfg)
}
func (mm *metricsMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "view").Add(1)
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.View(token, id)
}
func (mm *metricsMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update").Add(1)
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Update(token, cfg)
}
func (mm *metricsMiddleware) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_cert").Add(1)
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
}
func (mm *metricsMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_connections").Add(1)
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateConnections(token, id, connections)
}
func (mm *metricsMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "list").Add(1)
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.List(token, filter, offset, limit)
}
func (mm *metricsMiddleware) Remove(token, id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove").Add(1)
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Remove(token, id)
}
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "bootstrap").Add(1)
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Bootstrap(externalKey, externalID, secure)
}
func (mm *metricsMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "change_state").Add(1)
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ChangeState(token, id, state)
}
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_channel").Add(1)
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateChannelHandler(channel)
}
func (mm *metricsMiddleware) RemoveConfigHandler(id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove_config").Add(1)
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveConfigHandler(id)
}
func (mm *metricsMiddleware) RemoveChannelHandler(id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove_channel").Add(1)
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveChannelHandler(id)
}
func (mm *metricsMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "disconnect_thing_handler").Add(1)
mm.latency.With("method", "disconnect_thing_handler").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.DisconnectThingHandler(channelID, thingID)
}
-168
View File
@@ -1,168 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import "github.com/mainflux/mainflux/bootstrap"
type apiReq interface {
validate() error
}
type addReq struct {
token string
ThingID string `json:"thing_id"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
Channels []string `json:"channels"`
Name string `json:"name"`
Content string `json:"content"`
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
}
func (req addReq) validate() error {
if req.token == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.ExternalID == "" || req.ExternalKey == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type entityReq struct {
key string
id string
}
func (req entityReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type updateReq struct {
key string
id string
Name string `json:"name"`
Content string `json:"content"`
}
func (req updateReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type updateCertReq struct {
key string
thingID string
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
}
func (req updateCertReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.thingID == "" {
return bootstrap.ErrNotFound
}
return nil
}
type updateConnReq struct {
key string
id string
Channels []string `json:"channels"`
}
func (req updateConnReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type listReq struct {
key string
filter bootstrap.Filter
offset uint64
limit uint64
}
func (req listReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.limit == 0 || req.limit > maxLimit {
return bootstrap.ErrMalformedEntity
}
return nil
}
type bootstrapReq struct {
key string
id string
}
func (req bootstrapReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type changeStateReq struct {
key string
id string
State bootstrap.State `json:"state"`
}
func (req changeStateReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
if req.State != bootstrap.Inactive &&
req.State != bootstrap.Active {
return bootstrap.ErrMalformedEntity
}
return nil
}
-297
View File
@@ -1,297 +0,0 @@
package api
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/bootstrap"
"github.com/stretchr/testify/assert"
)
func TestAddReqValidation(t *testing.T) {
cases := []struct {
desc string
token string
externalID string
externalKey string
err error
}{
{
desc: "empty key",
token: "",
externalID: "external-id",
externalKey: "external-key",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty external ID",
token: "token",
externalID: "",
externalKey: "external-key",
err: bootstrap.ErrMalformedEntity,
},
{
desc: "empty external key",
token: "token",
externalID: "external-id",
externalKey: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := addReq{
token: tc.token,
ExternalID: tc.externalID,
ExternalKey: tc.externalKey,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestEntityReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "empty key",
key: "",
id: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := entityReq{
key: tc.key,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "empty key",
key: "",
id: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := updateReq{
key: tc.key,
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateCertReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
thingID string
err error
}{
{
desc: "empty key",
key: "",
thingID: "thingID",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty thing key",
key: "key",
thingID: "",
err: bootstrap.ErrNotFound,
},
}
for _, tc := range cases {
req := updateCertReq{
key: tc.key,
thingID: tc.thingID,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "empty key",
key: "",
id: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := updateReq{
key: tc.key,
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListReqValidation(t *testing.T) {
cases := []struct {
desc string
offset uint64
key string
limit uint64
err error
}{
{
desc: "empty key",
key: "",
offset: 0,
limit: 1,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "too large limit",
key: "key",
offset: 0,
limit: maxLimit + 1,
err: bootstrap.ErrMalformedEntity,
},
{
desc: "zero limit",
key: "key",
offset: 0,
limit: 0,
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := listReq{
key: tc.key,
offset: tc.offset,
limit: tc.limit,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestBootstrapReqValidation(t *testing.T) {
cases := []struct {
desc string
externKey string
externID string
err error
}{
{
desc: "empty external key",
externKey: "",
externID: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty external id",
externKey: "key",
externID: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := bootstrapReq{
id: tc.externID,
key: tc.externKey,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChangeStateReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
state bootstrap.State
err error
}{
{
desc: "empty key",
key: "",
id: "id",
state: bootstrap.State(1),
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
state: bootstrap.State(0),
err: bootstrap.ErrMalformedEntity,
},
{
desc: "invalid state",
key: "key",
id: "id",
state: bootstrap.State(14),
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := changeStateReq{
key: tc.key,
id: tc.id,
State: tc.state,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
-148
View File
@@ -1,148 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"fmt"
"net/http"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
)
var (
_ mainflux.Response = (*removeRes)(nil)
_ mainflux.Response = (*configRes)(nil)
_ mainflux.Response = (*stateRes)(nil)
_ mainflux.Response = (*viewRes)(nil)
_ mainflux.Response = (*listRes)(nil)
)
type removeRes struct{}
func (res removeRes) Code() int {
return http.StatusNoContent
}
func (res removeRes) Headers() map[string]string {
return map[string]string{}
}
func (res removeRes) Empty() bool {
return true
}
type configRes struct {
id string
created bool
}
func (res configRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res configRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/things/configs/%s", res.id),
}
}
return map[string]string{}
}
func (res configRes) Empty() bool {
return true
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
type viewRes struct {
MFThing string `json:"mainflux_id,omitempty"`
MFKey string `json:"mainflux_key,omitempty"`
Channels []channelRes `json:"mainflux_channels,omitempty"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key,omitempty"`
Content string `json:"content,omitempty"`
Name string `json:"name,omitempty"`
State bootstrap.State `json:"state"`
}
func (res viewRes) Code() int {
return http.StatusOK
}
func (res viewRes) Headers() map[string]string {
return map[string]string{}
}
func (res viewRes) Empty() bool {
return false
}
type unknownRes struct {
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key,omitempty"`
}
type listUnknownRes struct {
Configs []unknownRes `json:"configs"`
}
func (res listUnknownRes) Code() int {
return http.StatusOK
}
func (res listUnknownRes) Headers() map[string]string {
return map[string]string{}
}
func (res listUnknownRes) Empty() bool {
return false
}
type listRes struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Configs []viewRes `json:"configs"`
}
func (res listRes) Code() int {
return http.StatusOK
}
func (res listRes) Headers() map[string]string {
return map[string]string{}
}
func (res listRes) Empty() bool {
return false
}
type stateRes struct{}
func (res stateRes) Code() int {
return http.StatusOK
}
func (res stateRes) Headers() map[string]string {
return map[string]string{}
}
func (res stateRes) Empty() bool {
return true
}
type errorRes struct {
Err string `json:"error"`
}
-376
View File
@@ -1,376 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
contentType = "application/json"
maxLimit = 100
defaultLimit = 10
)
var (
errUnsupportedContentType = errors.New("unsupported content type")
errInvalidQueryParams = errors.New("invalid query params")
errInvalidLimitParam = errors.New("invalid limit query param")
errInvalidOffsetParam = errors.New("invalid offset query param")
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
partialMatch = []string{"name"}
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
r := bone.New()
r.Post("/things/configs", kithttp.NewServer(
addEndpoint(svc),
decodeAddRequest,
encodeResponse,
opts...))
r.Get("/things/configs/:id", kithttp.NewServer(
viewEndpoint(svc),
decodeEntityRequest,
encodeResponse,
opts...))
r.Put("/things/configs/:id", kithttp.NewServer(
updateEndpoint(svc),
decodeUpdateRequest,
encodeResponse,
opts...))
r.Patch("/things/configs/certs/:id", kithttp.NewServer(
updateCertEndpoint(svc),
decodeUpdateCertRequest,
encodeResponse,
opts...))
r.Put("/things/configs/connections/:id", kithttp.NewServer(
updateConnEndpoint(svc),
decodeUpdateConnRequest,
encodeResponse,
opts...))
r.Get("/things/configs", kithttp.NewServer(
listEndpoint(svc),
decodeListRequest,
encodeResponse,
opts...))
r.Get("/things/unknown/configs", kithttp.NewServer(
listEndpoint(svc),
decodeUnknownRequest,
encodeResponse,
opts...))
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
bootstrapEndpoint(svc, reader, false),
decodeBootstrapRequest,
encodeResponse,
opts...))
r.Get("/things/bootstrap/secure/:external_id", kithttp.NewServer(
bootstrapEndpoint(svc, reader, true),
decodeBootstrapRequest,
encodeSecureRes,
opts...))
r.Put("/things/state/:id", kithttp.NewServer(
stateEndpoint(svc),
decodeStateRequest,
encodeResponse,
opts...))
r.Delete("/things/configs/:id", kithttp.NewServer(
removeEndpoint(svc),
decodeEntityRequest,
encodeResponse,
opts...))
r.GetFunc("/version", mainflux.Version("bootstrap"))
r.Handle("/metrics", promhttp.Handler())
return r
}
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := addReq{token: r.Header.Get("Authorization")}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
}
return req, nil
}
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := updateReq{key: r.Header.Get("Authorization")}
req.id = bone.GetValue(r, "id")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
}
return req, nil
}
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := updateCertReq{
key: r.Header.Get("Authorization"),
thingID: bone.GetValue(r, "id"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
}
return req, nil
}
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := updateConnReq{key: r.Header.Get("Authorization")}
req.id = bone.GetValue(r, "id")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
}
return req, nil
}
func decodeUnknownRequest(_ context.Context, r *http.Request) (interface{}, error) {
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errInvalidQueryParams
}
offset, limit, err := parsePagePrams(q)
if err != nil {
return nil, err
}
req := listReq{
key: r.Header.Get("Authorization"),
filter: bootstrap.Filter{Unknown: true},
offset: offset,
limit: limit,
}
return req, nil
}
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errInvalidQueryParams
}
offset, limit, err := parsePagePrams(q)
if err != nil {
return nil, err
}
filter := parseFilter(q)
req := listReq{
key: r.Header.Get("Authorization"),
filter: filter,
offset: offset,
limit: limit,
}
return req, nil
}
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := bootstrapReq{
id: bone.GetValue(r, "external_id"),
key: r.Header.Get("Authorization"),
}
return req, nil
}
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := changeStateReq{key: r.Header.Get("Authorization")}
req.id = bone.GetValue(r, "id")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
}
return req, nil
}
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := entityReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
}
return req, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", contentType)
if ar, ok := response.(mainflux.Response); ok {
for k, v := range ar.Headers() {
w.Header().Set(k, v)
}
w.WriteHeader(ar.Code())
if ar.Empty() {
return nil
}
}
return json.NewEncoder(w).Encode(response)
}
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", contentType)
w.WriteHeader(http.StatusOK)
if b, ok := response.([]byte); ok {
if _, err := w.Write(b); err != nil {
return err
}
}
return nil
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
switch errorVal := err.(type) {
case errors.Error:
w.Header().Set("Content-Type", contentType)
switch {
case errors.Contains(errorVal, errUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case errors.Contains(errorVal, errInvalidQueryParams):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, bootstrap.ErrMalformedEntity):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, bootstrap.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(errorVal, bootstrap.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(errorVal, bootstrap.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(errorVal, bootstrap.ErrThings):
w.WriteHeader(http.StatusServiceUnavailable)
case errors.Contains(errorVal, io.EOF):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, io.ErrUnexpectedEOF):
w.WriteHeader(http.StatusBadRequest)
default:
w.WriteHeader(http.StatusInternalServerError)
}
if errorVal.Msg() != "" {
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
default:
w.WriteHeader(http.StatusInternalServerError)
}
}
func parseUint(s string) (uint64, error) {
if s == "" {
return 0, nil
}
ret, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, errInvalidQueryParams
}
return ret, nil
}
func parsePagePrams(q url.Values) (uint64, uint64, error) {
offset, err := parseUint(q.Get("offset"))
q.Del("offset")
if err != nil {
return 0, 0, errors.Wrap(errInvalidOffsetParam, err)
}
limit, err := parseUint(q.Get("limit"))
q.Del("limit")
if err != nil {
return 0, 0, errors.Wrap(errInvalidLimitParam, err)
}
if limit > maxLimit {
limit = maxLimit
}
if limit == 0 {
limit = defaultLimit
}
return offset, limit, nil
}
func parseFilter(values url.Values) bootstrap.Filter {
ret := bootstrap.Filter{
FullMatch: make(map[string]string),
PartialMatch: make(map[string]string),
}
for k := range values {
if contains(fullMatch, k) {
ret.FullMatch[k] = values.Get(k)
}
if contains(partialMatch, k) {
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
}
}
return ret
}
func contains(l []string, s string) bool {
for _, v := range l {
if v == s {
return true
}
}
return false
}
-109
View File
@@ -1,109 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package bootstrap
// Config represents Configuration entity. It wraps information about external entity
// as well as info about corresponding Mainflux entities.
// MFThing represents corresponding Mainflux Thing ID.
// MFKey is key of corresponding Mainflux Thing.
// MFChannels is a list of Mainflux Channels corresponding Mainflux Thing connects to.
type Config struct {
MFThing string
Owner string
Name string
ClientCert string
ClientKey string
CACert string
MFKey string
MFChannels []Channel
ExternalID string
ExternalKey string
Content string
State State
}
// Channel represents Mainflux channel corresponding Mainflux Thing is connected to.
type Channel struct {
ID string
Name string
Metadata map[string]interface{}
}
// Filter is used for the search filters.
type Filter struct {
Unknown bool
FullMatch map[string]string
PartialMatch map[string]string
}
// ConfigsPage contains page related metadata as well as list of Configs that
// belong to this page.
type ConfigsPage struct {
Total uint64
Offset uint64
Limit uint64
Configs []Config
}
// ConfigRepository specifies a Config persistence API.
type ConfigRepository interface {
// Save persists the Config. Successful operation is indicated by non-nil
// error response.
Save(cfg Config, chsConnIDs []string) (string, error)
// RetrieveByID retrieves the Config having the provided identifier, that is owned
// by the specified user.
RetrieveByID(owner, id string) (Config, error)
// RetrieveAll retrieves a subset of Configs that are owned
// by the specific user, with given filter parameters.
RetrieveAll(owner string, filter Filter, offset, limit uint64) ConfigsPage
// RetrieveByExternalID returns Config for given external ID.
RetrieveByExternalID(externalID string) (Config, error)
// Update updates an existing Config. A non-nil error is returned
// to indicate operation failure.
Update(cfg Config) error
// UpdateCerts updates an existing Config certificate and owner.
// A non-nil error is returned to indicate operation failure.
UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error
// UpdateConnections updates a list of Channels the Config is connected to
// adding new Channels if needed.
UpdateConnections(owner, id string, channels []Channel, connections []string) error
// Remove removes the Config having the provided identifier, that is owned
// by the specified user.
Remove(owner, id string) error
// ChangeState changes of the Config, that is owned by the specific user.
ChangeState(owner, id string, state State) error
// ListExisting retrieves those channels from the given list that exist in DB.
ListExisting(owner string, ids []string) ([]Channel, error)
// SaveUnknown saves Thing which unsuccessfully bootstrapped.
SaveUnknown(owner, id string) error
// RetrieveUnknown returns a subset of unsuccessfully bootstrapped Things.
RetrieveUnknown(offset, limit uint64) ConfigsPage
// Methods RemoveThing, UpdateChannel, and RemoveChannel are related to
// event sourcing. That's why these methods surpass ownership check.
// RemoveThing removes Config of the Thing with the given ID.
RemoveThing(id string) error
// UpdateChannel updates channel with the given ID.
UpdateChannel(c Channel) error
// RemoveChannel removes channel with the given ID.
RemoveChannel(id string) error
// DisconnectHandler changes state of the Config when the corresponding Thing is
// disconnected from the Channel.
DisconnectThing(channelID, thingID string) error
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package bootstrap contains the domain concept definitions needed to support
// Mainflux bootstrap service functionality.
package bootstrap
-364
View File
@@ -1,364 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"sort"
"strconv"
"strings"
"sync"
"github.com/mainflux/mainflux/bootstrap"
)
const (
emptyState = -1
notFoundIdx = -1
)
var _ bootstrap.ConfigRepository = (*configRepositoryMock)(nil)
type configRepositoryMock struct {
mu sync.Mutex
counter uint64
configs map[string]bootstrap.Config
channels map[string]bootstrap.Channel
unknown map[string]string
}
// NewConfigsRepository creates in-memory config repository.
func NewConfigsRepository(unknown map[string]string) bootstrap.ConfigRepository {
return &configRepositoryMock{
configs: make(map[string]bootstrap.Config),
channels: make(map[string]bootstrap.Channel),
unknown: unknown,
}
}
func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []string) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
for _, v := range crm.configs {
if v.MFThing == config.MFThing || v.ExternalID == config.ExternalID {
return "", bootstrap.ErrConflict
}
}
crm.counter++
config.MFThing = strconv.FormatUint(crm.counter, 10)
crm.configs[config.MFThing] = config
for _, ch := range config.MFChannels {
crm.channels[ch.ID] = ch
}
config.MFChannels = []bootstrap.Channel{}
for _, ch := range connections {
config.MFChannels = append(config.MFChannels, crm.channels[ch])
}
crm.configs[config.MFThing] = config
delete(crm.unknown, config.ExternalID)
return config.MFThing, nil
}
func (crm *configRepositoryMock) RetrieveByID(token, id string) (bootstrap.Config, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
c, ok := crm.configs[id]
if !ok {
return bootstrap.Config{}, bootstrap.ErrNotFound
}
if c.Owner != token {
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
}
return c, nil
}
func (crm *configRepositoryMock) RetrieveAll(token string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
crm.mu.Lock()
defer crm.mu.Unlock()
configs := make([]bootstrap.Config, 0)
if offset < 0 || limit <= 0 {
return bootstrap.ConfigsPage{}
}
first := uint64(offset) + 1
last := first + uint64(limit)
var state bootstrap.State = emptyState
var name string
if s, ok := filter.FullMatch["state"]; ok {
val, _ := strconv.Atoi(s)
state = bootstrap.State(val)
}
if s, ok := filter.PartialMatch["name"]; ok {
name = strings.ToLower(s)
}
var total uint64
for _, v := range crm.configs {
id, _ := strconv.ParseUint(v.MFThing, 10, 64)
if (state == emptyState || v.State == state) &&
(name == "" || strings.Index(strings.ToLower(v.Name), name) != notFoundIdx) &&
v.Owner == token {
if id >= first && id < last {
configs = append(configs, v)
}
total++
}
}
sort.SliceStable(configs, func(i, j int) bool {
return configs[i].MFThing < configs[j].MFThing
})
return bootstrap.ConfigsPage{
Total: total,
Offset: offset,
Limit: limit,
Configs: configs,
}
}
func (crm *configRepositoryMock) RetrieveByExternalID(externalID string) (bootstrap.Config, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
for _, cfg := range crm.configs {
if cfg.ExternalID == externalID {
return cfg, nil
}
}
return bootstrap.Config{}, bootstrap.ErrNotFound
}
func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
crm.mu.Lock()
defer crm.mu.Unlock()
cfg, ok := crm.configs[config.MFThing]
if !ok || cfg.Owner != config.Owner {
return bootstrap.ErrNotFound
}
cfg.Name = config.Name
cfg.Content = config.Content
crm.configs[config.MFThing] = cfg
return nil
}
func (crm *configRepositoryMock) UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
var forUpdate bootstrap.Config
for _, v := range crm.configs {
if v.MFThing == thingID && v.Owner == owner {
forUpdate = v
break
}
}
if _, ok := crm.configs[forUpdate.MFThing]; !ok {
return bootstrap.ErrNotFound
}
forUpdate.ClientCert = clientCert
forUpdate.ClientKey = clientKey
forUpdate.CACert = caCert
crm.configs[forUpdate.MFThing] = forUpdate
return nil
}
func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []bootstrap.Channel, connections []string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
config, ok := crm.configs[id]
if !ok {
return bootstrap.ErrNotFound
}
for _, ch := range channels {
crm.channels[ch.ID] = ch
}
config.MFChannels = []bootstrap.Channel{}
for _, conn := range connections {
ch, ok := crm.channels[conn]
if !ok {
return bootstrap.ErrNotFound
}
config.MFChannels = append(config.MFChannels, ch)
}
crm.configs[id] = config
return nil
}
func (crm *configRepositoryMock) Remove(token, id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
for k, v := range crm.configs {
if v.Owner == token && k == id {
delete(crm.configs, k)
break
}
}
return nil
}
func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.State) error {
crm.mu.Lock()
defer crm.mu.Unlock()
config, ok := crm.configs[id]
if !ok {
return bootstrap.ErrNotFound
}
if config.Owner != token {
return bootstrap.ErrUnauthorizedAccess
}
config.State = state
crm.configs[id] = config
return nil
}
func (crm *configRepositoryMock) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
crm.mu.Lock()
defer crm.mu.Unlock()
configs := []bootstrap.Config{}
i := uint64(0)
l := int(limit)
var keys []string
for k := range crm.unknown {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if i >= offset && len(configs) < l {
configs = append(configs, bootstrap.Config{
ExternalID: k,
ExternalKey: crm.unknown[k],
})
}
i++
}
return bootstrap.ConfigsPage{
Total: uint64(len(crm.unknown)),
Offset: offset,
Limit: limit,
Configs: configs,
}
}
func (crm *configRepositoryMock) RemoveUnknown(token, id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
for k, v := range crm.unknown {
if k == id && v == token {
delete(crm.unknown, k)
return nil
}
}
return nil
}
func (crm *configRepositoryMock) SaveUnknown(token, id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.unknown[id] = token
return nil
}
func (crm *configRepositoryMock) ListExisting(token string, connections []string) ([]bootstrap.Channel, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
var ret []bootstrap.Channel
for k, v := range crm.channels {
for _, conn := range connections {
if conn == k {
ret = append(ret, v)
break
}
}
}
return ret, nil
}
func (crm *configRepositoryMock) RemoveThing(id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
delete(crm.configs, id)
return nil
}
func (crm *configRepositoryMock) UpdateChannel(ch bootstrap.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
channel, ok := crm.channels[ch.ID]
if !ok {
return nil
}
channel.Name = ch.Name
channel.Metadata = ch.Metadata
crm.channels[ch.ID] = channel
return nil
}
func (crm *configRepositoryMock) RemoveChannel(id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
delete(crm.channels, id)
return nil
}
func (crm *configRepositoryMock) DisconnectThing(channelID, thingID string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
idx := -1
if config, ok := crm.configs[thingID]; ok {
for i, ch := range config.MFChannels {
if ch.ID == channelID {
idx = i
break
}
}
if idx != -1 {
config.MFChannels = append(config.MFChannels[0:idx], config.MFChannels[idx:]...)
}
crm.configs[thingID] = config
}
delete(crm.channels, channelID)
return nil
}
-232
View File
@@ -1,232 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"strconv"
"sync"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/things"
)
var _ things.Service = (*mainfluxThings)(nil)
type mainfluxThings struct {
mu sync.Mutex
counter uint64
things map[string]things.Thing
channels map[string]things.Channel
auth mainflux.AuthNServiceClient
connections map[string][]string
}
// NewThingsService returns Mainflux Things service mock.
// Only methods used by SDK are mocked.
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, authn mainflux.AuthNServiceClient) things.Service {
return &mainfluxThings{
things: things,
channels: channels,
auth: authn,
connections: make(map[string][]string),
}
}
func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...things.Thing) ([]things.Thing, error) {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return []things.Thing{}, things.ErrUnauthorizedAccess
}
for i := range ths {
svc.counter++
ths[i].Owner = userID.Value
ths[i].ID = strconv.FormatUint(svc.counter, 10)
ths[i].Key = ths[i].ID
svc.things[ths[i].ID] = ths[i]
}
return ths, nil
}
func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (things.Thing, error) {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.Thing{}, things.ErrUnauthorizedAccess
}
if t, ok := svc.things[id]; ok && t.Owner == userID.Value {
return t, nil
}
return things.Thing{}, things.ErrNotFound
}
func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs []string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.ErrUnauthorizedAccess
}
for _, chID := range chIDs {
if svc.channels[chID].Owner != userID.Value {
return things.ErrUnauthorizedAccess
}
for _, thID := range thIDs {
svc.connections[chID] = append(svc.connections[chID], thID)
}
}
return nil
}
func (svc *mainfluxThings) Disconnect(_ context.Context, owner, chanID, thingID string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil || svc.channels[chanID].Owner != userID.Value {
return things.ErrUnauthorizedAccess
}
ids := svc.connections[chanID]
i := 0
for _, t := range ids {
if t == thingID {
break
}
i++
}
if i == len(ids) {
return things.ErrNotFound
}
var tmp []string
if i != len(ids)-2 {
tmp = ids[i+1:]
}
ids = append(ids[:i], tmp...)
svc.connections[chanID] = ids
return nil
}
func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.ErrUnauthorizedAccess
}
if t, ok := svc.things[id]; !ok || t.Owner != userID.Value {
return things.ErrNotFound
}
delete(svc.things, id)
conns := make(map[string][]string)
for k, v := range svc.connections {
i := findIndex(v, id)
if i != -1 {
var tmp []string
if i != len(v)-2 {
tmp = v[i+1:]
}
conns[k] = append(v[:i], tmp...)
}
}
svc.connections = conns
return nil
}
func (svc *mainfluxThings) ViewChannel(_ context.Context, owner, id string) (things.Channel, error) {
if c, ok := svc.channels[id]; ok {
return c, nil
}
return things.Channel{}, things.ErrNotFound
}
func (svc *mainfluxThings) UpdateThing(context.Context, string, things.Thing) error {
panic("not implemented")
}
func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) error {
panic("not implemented")
}
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string, things.Metadata) (things.Page, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64) (things.ChannelsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64) (things.Page, error) {
panic("not implemented")
}
func (svc *mainfluxThings) CreateChannels(_ context.Context, owner string, chs ...things.Channel) ([]things.Channel, error) {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return []things.Channel{}, things.ErrUnauthorizedAccess
}
for i := range chs {
svc.counter++
chs[i].Owner = userID.Value
chs[i].ID = strconv.FormatUint(svc.counter, 10)
svc.channels[chs[i].ID] = chs[i]
}
return chs, nil
}
func (svc *mainfluxThings) UpdateChannel(context.Context, string, things.Channel) error {
panic("not implemented")
}
func (svc *mainfluxThings) ListChannels(context.Context, string, uint64, uint64, string, things.Metadata) (things.ChannelsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) RemoveChannel(context.Context, string, string) error {
panic("not implemented")
}
func (svc *mainfluxThings) CanAccessByKey(context.Context, string, string) (string, error) {
panic("not implemented")
}
func (svc *mainfluxThings) CanAccessByID(context.Context, string, string) error {
panic("not implemented")
}
func (svc *mainfluxThings) Identify(context.Context, string) (string, error) {
panic("not implemented")
}
func findIndex(list []string, val string) int {
for i, v := range list {
if v == val {
return i
}
}
return -1
}
-40
View File
@@ -1,40 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/users"
"google.golang.org/grpc"
)
var _ mainflux.AuthNServiceClient = (*serviceMock)(nil)
type serviceMock struct {
users map[string]string
}
// NewUsersService creates mock of users service.
func NewUsersService(users map[string]string) mainflux.AuthNServiceClient {
return &serviceMock{users}
}
func (svc serviceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserID, error) {
if id, ok := svc.users[in.Value]; ok {
return &mainflux.UserID{Value: id}, nil
}
return nil, users.ErrUnauthorizedAccess
}
func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ...grpc.CallOption) (*mainflux.Token, error) {
if id, ok := svc.users[in.GetIssuer()]; ok {
switch in.Type {
default:
return &mainflux.Token{Value: id}, nil
}
}
return nil, users.ErrUnauthorizedAccess
}
-731
View File
@@ -1,731 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/errors"
"github.com/mainflux/mainflux/logger"
)
const (
duplicateErr = "unique_violation"
uuidErr = "invalid input syntax for type uuid"
connConstraintErr = "connections_config_id_fkey"
fkViolation = "foreign_key_violation"
configFieldsNum = 8
chanFieldsNum = 3
connFieldsNum = 2
cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
SELECT channel_id FROM connections c WHERE ch.mainflux_channel = c.channel_id);`
)
var (
errSaveDB = errors.New("failed to save bootstrap configuration to database")
errMarshalChannel = errors.New("failed to marshal channel into json")
errUnmarshalChannel = errors.New("failed to unmarshal json to channel")
errSaveChannels = errors.New("failed to insert channels to database")
errSaveConnections = errors.New("failed to insert connections to database")
errRemoveUnknown = errors.New("failed to remove from uknown configurations in database")
errSaveUnknown = errors.New("failed to insert into uknown configurations in database")
errRetrieve = errors.New("failed to retreive bootstrap configuration from database")
errUpdate = errors.New("failed to update bootstrap configuration in database")
errRemove = errors.New("failed to remove bootstrap configuration from database")
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
)
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
type configRepository struct {
db *sqlx.DB
log logger.Logger
}
// NewConfigRepository instantiates a PostgreSQL implementation of config
// repository.
func NewConfigRepository(db *sqlx.DB, log logger.Logger) bootstrap.ConfigRepository {
return &configRepository{db: db, log: log}
}
func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (string, error) {
q := `INSERT INTO configs (mainflux_thing, owner, name, client_cert, client_key, ca_cert, mainflux_key, external_id, external_key, content, state)
VALUES (:mainflux_thing, :owner, :name, :client_cert, :client_key, :ca_cert, :mainflux_key, :external_id, :external_key, :content, :state)`
tx, err := cr.db.Beginx()
if err != nil {
return "", errors.Wrap(errSaveDB, err)
}
dbcfg := toDBConfig(cfg)
if _, err := tx.NamedExec(q, dbcfg); err != nil {
e := err
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
e = bootstrap.ErrConflict
}
cr.rollback("Failed to insert a Config", tx, err)
return "", errors.Wrap(errSaveDB, e)
}
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
cr.rollback("Failed to insert Channels", tx, err)
return "", errors.Wrap(errSaveChannels, err)
}
if err := insertConnections(cfg, chsConnIDs, tx); err != nil {
cr.rollback("Failed to insert connections", tx, err)
return "", errors.Wrap(errSaveConnections, err)
}
q = "DELETE FROM unknown_configs WHERE external_id = :external_id AND external_key = :external_key"
if _, err := tx.NamedExec(q, dbcfg); err != nil {
cr.rollback("Failed to remove from unknown", tx, err)
return "", errors.Wrap(errRemoveUnknown, err)
}
if err := tx.Commit(); err != nil {
cr.rollback("Failed to commit Config save", tx, err)
}
return cfg.MFThing, nil
}
func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, error) {
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
FROM configs
WHERE mainflux_thing = $1 AND owner = $2`
dbcfg := dbConfig{
MFThing: id,
Owner: owner,
}
if err := cr.db.QueryRowx(q, id, owner).StructScan(&dbcfg); err != nil {
empty := bootstrap.Config{}
if err == sql.ErrNoRows {
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
}
return empty, errors.Wrap(errRetrieve, err)
}
q = `SELECT mainflux_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
WHERE conn.config_id = :mainflux_thing AND conn.config_owner = :owner`
rows, err := cr.db.NamedQuery(q, dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
}
defer rows.Close()
chans := []bootstrap.Channel{}
for rows.Next() {
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
}
dbch.Owner = nullString(dbcfg.Owner)
ch, err := toChannel(dbch)
if err != nil {
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
}
chans = append(chans, ch)
}
cfg := toConfig(dbcfg)
cfg.MFChannels = chans
return cfg, nil
}
func (cr configRepository) RetrieveAll(owner string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
search, params := cr.retrieveAll(owner, filter)
n := len(params)
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
FROM configs %s ORDER BY mainflux_thing LIMIT $%d OFFSET $%d`
q = fmt.Sprintf(q, search, n+1, n+2)
rows, err := cr.db.Query(q, append(params, limit, offset)...)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
return bootstrap.ConfigsPage{}
}
defer rows.Close()
var name, content sql.NullString
configs := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{Owner: owner}
if err := rows.Scan(&c.MFThing, &c.MFKey, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
c.Name = name.String
c.Content = content.String
configs = append(configs, c)
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
var total uint64
if err := cr.db.QueryRow(q, params...).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
return bootstrap.ConfigsPage{}
}
return bootstrap.ConfigsPage{
Total: total,
Limit: limit,
Offset: offset,
Configs: configs,
}
}
func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Config, error) {
q := `SELECT mainflux_thing, mainflux_key, external_key, owner, name, client_cert, client_key, ca_cert, content, state
FROM configs
WHERE external_id = $1`
dbcfg := dbConfig{
ExternalID: externalID,
}
if err := cr.db.QueryRowx(q, externalID).StructScan(&dbcfg); err != nil {
empty := bootstrap.Config{}
if err == sql.ErrNoRows {
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
}
return empty, errors.Wrap(errRetrieve, err)
}
q = `SELECT mainflux_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
WHERE conn.config_id = :mainflux_thing AND conn.config_owner = :owner`
rows, err := cr.db.NamedQuery(q, dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
}
defer rows.Close()
channels := []bootstrap.Channel{}
for rows.Next() {
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
}
channels = append(channels, ch)
}
cfg := toConfig(dbcfg)
cfg.MFChannels = channels
return cfg, nil
}
func (cr configRepository) Update(cfg bootstrap.Config) error {
q := `UPDATE configs SET name = $1, content = $2 WHERE mainflux_thing = $3 AND owner = $4`
content := nullString(cfg.Content)
name := nullString(cfg.Name)
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
if err != nil {
return errors.Wrap(errUpdate, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(errUpdate, err)
}
if cnt == 0 {
return bootstrap.ErrNotFound
}
return nil
}
func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error {
q := `UPDATE configs SET client_cert = $1, client_key = $2, ca_cert = $3 WHERE mainflux_thing = $4 AND owner = $5`
res, err := cr.db.Exec(q, clientCert, clientKey, caCert, thingID, owner)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return bootstrap.ErrNotFound
}
return nil
}
func (cr configRepository) UpdateConnections(owner, id string, channels []bootstrap.Channel, connections []string) error {
tx, err := cr.db.Beginx()
if err != nil {
return err
}
if err := insertChannels(owner, channels, tx); err != nil {
cr.rollback("Failed to insert Channels during the update", tx, err)
return err
}
if err := updateConnections(owner, id, connections, tx); err != nil {
if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
return bootstrap.ErrNotFound
}
}
cr.rollback("Failed to update connections during the update", tx, err)
return err
}
if err := tx.Commit(); err != nil {
cr.rollback("Failed to commit Config update", tx, err)
}
return nil
}
func (cr configRepository) Remove(owner, id string) error {
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
if _, err := cr.db.Exec(q, id, owner); err != nil {
return errors.Wrap(errRemove, err)
}
if _, err := cr.db.Exec(cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
return nil
}
func (cr configRepository) ChangeState(owner, id string, state bootstrap.State) error {
q := `UPDATE configs SET state = $1 WHERE mainflux_thing = $2 AND owner = $3;`
res, err := cr.db.Exec(q, state, id, owner)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return bootstrap.ErrNotFound
}
return nil
}
func (cr configRepository) ListExisting(owner string, ids []string) ([]bootstrap.Channel, error) {
var channels []bootstrap.Channel
if len(ids) == 0 {
return channels, nil
}
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
rows, err := cr.db.Queryx(q, owner, pq.Array(ids))
if err != nil {
return []bootstrap.Channel{}, err
}
for rows.Next() {
var dbch dbChannel
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
return []bootstrap.Channel{}, err
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return []bootstrap.Channel{}, err
}
channels = append(channels, ch)
}
return channels, nil
}
func (cr configRepository) SaveUnknown(owner, id string) error {
q := `INSERT INTO unknown_configs (external_id, external_key) VALUES ($1, $2)`
if _, err := cr.db.Exec(q, id, owner); err != nil {
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
return nil
}
return errors.Wrap(errSaveUnknown, err)
}
return nil
}
func (cr configRepository) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
q := `SELECT external_id, external_key FROM unknown_configs LIMIT $1 OFFSET $2`
rows, err := cr.db.Query(q, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve config due to %s", err))
return bootstrap.ConfigsPage{}
}
defer rows.Close()
items := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{}
if err := rows.Scan(&c.ExternalID, &c.ExternalKey); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
items = append(items, c)
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM unknown_configs`)
var total uint64
if err := cr.db.QueryRow(q).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count unknown configs due to %s", err))
return bootstrap.ConfigsPage{}
}
return bootstrap.ConfigsPage{
Total: total,
Offset: offset,
Limit: limit,
Configs: items,
}
}
func (cr configRepository) RemoveThing(id string) error {
q := `DELETE FROM configs WHERE mainflux_thing = $1`
_, err := cr.db.Exec(q, id)
if _, err := cr.db.Exec(cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
if err != nil {
return errors.Wrap(errRemove, err)
}
return nil
}
func (cr configRepository) UpdateChannel(c bootstrap.Channel) error {
dbch, err := toDBChannel("", c)
if err != nil {
return err
}
q := `UPDATE channels SET name = :name, metadata = :metadata WHERE mainflux_channel = :mainflux_channel`
if _, err = cr.db.NamedExec(q, dbch); err != nil {
return errors.Wrap(errUpdateChannels, err)
}
return nil
}
func (cr configRepository) RemoveChannel(id string) error {
q := `DELETE FROM channels WHERE mainflux_channel = $1`
if _, err := cr.db.Exec(q, id); err != nil {
return errors.Wrap(errRemoveChannels, err)
}
return nil
}
func (cr configRepository) DisconnectThing(channelID, thingID string) error {
q := `UPDATE configs SET state = $1 WHERE EXISTS (
SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
if _, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID); err != nil {
return errors.Wrap(errDisconnectThing, err)
}
return nil
}
func (cr configRepository) retrieveAll(owner string, filter bootstrap.Filter) (string, []interface{}) {
template := `WHERE owner = $1 %s`
params := []interface{}{owner}
// One empty string so that strings Join works if only one filter is applied.
queries := []string{""}
// Since owner is the first param, start from 2.
counter := 2
for k, v := range filter.FullMatch {
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
params = append(params, v)
counter++
}
for k, v := range filter.PartialMatch {
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
params = append(params, v)
counter++
}
f := strings.Join(queries, " AND ")
return fmt.Sprintf(template, f), params
}
func (cr configRepository) rollback(content string, tx *sqlx.Tx, err error) {
cr.log.Error(fmt.Sprintf("%s %s", content, err))
if err := tx.Rollback(); err != nil {
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
}
}
func insertChannels(owner string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
if len(channels) == 0 {
return nil
}
var chans []dbChannel
for _, ch := range channels {
dbch, err := toDBChannel(owner, ch)
if err != nil {
return err
}
chans = append(chans, dbch)
}
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata)
VALUES (:mainflux_channel, :owner, :name, :metadata)`
if _, err := tx.NamedExec(q, chans); err != nil {
e := err
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
e = bootstrap.ErrConflict
}
return e
}
return nil
}
func insertConnections(cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
if len(connections) == 0 {
return nil
}
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
conns := []dbConnection{}
for _, conn := range connections {
dbconn := dbConnection{
Config: cfg.MFThing,
Channel: conn,
ConfigOwner: cfg.Owner,
ChannelOwner: cfg.Owner,
}
conns = append(conns, dbconn)
}
_, err := tx.NamedExec(q, conns)
return err
}
func updateConnections(owner, id string, connections []string, tx *sqlx.Tx) error {
if len(connections) == 0 {
return nil
}
q := `DELETE FROM connections
WHERE config_id = $1 AND config_owner = $2 AND channel_owner = $2
AND channel_id NOT IN ($3)`
res, err := tx.Exec(q, id, owner, pq.Array(connections))
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
conns := []dbConnection{}
for _, conn := range connections {
dbconn := dbConnection{
Config: id,
Channel: conn,
ConfigOwner: owner,
ChannelOwner: owner,
}
conns = append(conns, dbconn)
}
if _, err := tx.NamedExec(q, conns); err != nil {
return err
}
if cnt == 0 {
return nil
}
_, err = tx.Exec(cleanupQuery)
return err
}
func nullString(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{
String: s,
Valid: true,
}
}
type dbConfig struct {
MFThing string `db:"mainflux_thing"`
Owner string `db:"owner"`
Name sql.NullString `db:"name"`
ClientCert sql.NullString `db:"client_cert"`
ClientKey sql.NullString `db:"client_key"`
CaCert sql.NullString `db:"ca_cert"`
MFKey string `db:"mainflux_key"`
ExternalID string `db:"external_id"`
ExternalKey string `db:"external_key"`
Content sql.NullString `db:"content"`
State bootstrap.State `db:"state"`
}
func toDBConfig(cfg bootstrap.Config) dbConfig {
return dbConfig{
MFThing: cfg.MFThing,
Owner: cfg.Owner,
Name: nullString(cfg.Name),
ClientCert: nullString(cfg.ClientCert),
ClientKey: nullString(cfg.ClientKey),
CaCert: nullString(cfg.CACert),
MFKey: cfg.MFKey,
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Content: nullString(cfg.Content),
State: cfg.State,
}
}
func toConfig(dbcfg dbConfig) bootstrap.Config {
cfg := bootstrap.Config{
MFThing: dbcfg.MFThing,
Owner: dbcfg.Owner,
MFKey: dbcfg.MFKey,
ExternalID: dbcfg.ExternalID,
ExternalKey: dbcfg.ExternalKey,
State: dbcfg.State,
}
if dbcfg.Name.Valid {
cfg.Name = dbcfg.Name.String
}
if dbcfg.Content.Valid {
cfg.Content = dbcfg.Content.String
}
if dbcfg.ClientCert.Valid {
cfg.ClientCert = dbcfg.ClientCert.String
}
if dbcfg.ClientKey.Valid {
cfg.ClientKey = dbcfg.ClientKey.String
}
if dbcfg.CaCert.Valid {
cfg.CACert = dbcfg.CaCert.String
}
return cfg
}
type dbChannel struct {
ID string `db:"mainflux_channel"`
Name sql.NullString `db:"name"`
Owner sql.NullString `db:"owner"`
Metadata string `db:"metadata"`
}
func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
dbch := dbChannel{
ID: ch.ID,
Name: nullString(ch.Name),
Owner: nullString(owner),
}
metadata, err := json.Marshal(ch.Metadata)
if err != nil {
return dbChannel{}, errors.Wrap(errMarshalChannel, err)
}
dbch.Metadata = string(metadata)
return dbch, nil
}
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
ch := bootstrap.Channel{
ID: dbch.ID,
}
if dbch.Name.Valid {
ch.Name = dbch.Name.String
}
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
return bootstrap.Channel{}, errors.Wrap(errUnmarshalChannel, err)
}
return ch, nil
}
type dbConnection struct {
Config string `db:"config_id"`
Channel string `db:"channel_id"`
ConfigOwner string `db:"config_owner"`
ChannelOwner string `db:"channel_owner"`
}
-760
View File
@@ -1,760 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"fmt"
"strconv"
"testing"
"github.com/gofrs/uuid"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/bootstrap/postgres"
"github.com/mainflux/mainflux/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const numConfigs = 10
var (
config = bootstrap.Config{
MFThing: "mf-thing",
MFKey: "mf-key",
ExternalID: "external-id",
ExternalKey: "external-key",
Owner: "user@email.com",
MFChannels: []bootstrap.Channel{
bootstrap.Channel{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
bootstrap.Channel{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
},
Content: "content",
State: bootstrap.Inactive,
}
channels = []string{"1", "2"}
)
func TestSave(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
diff := "different"
duplicateThing := config
duplicateThing.ExternalID = diff
duplicateThing.MFKey = diff
duplicateThing.MFChannels = []bootstrap.Channel{}
duplicateExternal := config
duplicateExternal.MFThing = diff
duplicateExternal.MFKey = diff
duplicateExternal.MFChannels = []bootstrap.Channel{}
duplicateChannels := config
duplicateChannels.ExternalID = diff
duplicateChannels.MFKey = diff
duplicateChannels.MFThing = diff
cases := []struct {
desc string
config bootstrap.Config
connections []string
err error
}{
{
desc: "save a config",
config: config,
connections: channels,
err: nil,
},
{
desc: "save config with same Thing ID",
config: duplicateThing,
connections: nil,
err: bootstrap.ErrConflict,
},
{
desc: "save config with same external ID",
config: duplicateExternal,
connections: nil,
err: bootstrap.ErrConflict,
},
{
desc: "save config with same Channels",
config: duplicateChannels,
connections: channels,
err: bootstrap.ErrConflict,
},
}
for _, tc := range cases {
_, err := repo.Save(tc.config, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveByID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
id, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
nonexistentConfID, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
cases := []struct {
desc string
owner string
id string
err error
}{
{
desc: "retrieve config",
owner: c.Owner,
id: id,
err: nil,
},
{
desc: "retrieve config with wrong owner",
owner: "2",
id: id,
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve a non-existing config",
owner: c.Owner,
id: nonexistentConfID.String(),
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve a config with invalid ID",
owner: c.Owner,
id: "invalid",
err: bootstrap.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByID(tc.owner, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveAll(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
for i := 0; i < numConfigs; i++ {
c := config
// Use UUID to prevent conflict errors.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ExternalID = uid.String()
c.Name = fmt.Sprintf("name %d", i)
c.MFThing = uid.String()
c.MFKey = uid.String()
if i%2 == 0 {
c.State = bootstrap.Active
}
if i > 0 {
c.MFChannels = nil
}
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
cases := []struct {
desc string
owner string
offset uint64
limit uint64
filter bootstrap.Filter
size int
}{
{
desc: "retrieve all",
owner: config.Owner,
offset: 0,
limit: uint64(numConfigs),
size: numConfigs,
},
{
desc: "retrieve subset",
owner: config.Owner,
offset: 5,
limit: uint64(numConfigs - 5),
size: numConfigs - 5,
},
{
desc: "retrieve wrong owner",
owner: "2",
offset: 0,
limit: uint64(numConfigs),
size: 0,
},
{
desc: "retrieve all active",
owner: config.Owner,
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
size: numConfigs / 2,
},
{
desc: "retrieve search by name",
owner: config.Owner,
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
size: 1,
},
}
for _, tc := range cases {
ret := repo.RetrieveAll(tc.owner, tc.filter, tc.offset, tc.limit)
size := len(ret.Configs)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
}
}
func TestRetrieveByExternalID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
externalID string
err error
}{
{
desc: "retrieve with invalid external ID",
externalID: strconv.Itoa(numConfigs + 1),
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve with external key",
externalID: c.ExternalID,
err: nil,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByExternalID(tc.externalID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdate(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
c.Name = "new name"
wrongOwner := c
wrongOwner.Owner = "3"
cases := []struct {
desc string
id string
config bootstrap.Config
err error
}{
{
desc: "update with wrong owner",
config: wrongOwner,
err: bootstrap.ErrNotFound,
},
{
desc: "update a config",
config: c,
err: nil,
},
}
for _, tc := range cases {
err := repo.Update(tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateCert(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
c.Name = "new name"
wrongOwner := c
wrongOwner.Owner = "3"
cases := []struct {
desc string
thingID string
owner string
cert string
certKey string
ca string
err error
}{
{
desc: "update with wrong owner",
thingID: "",
cert: "cert",
certKey: "certKey",
ca: "",
owner: "wrong",
err: bootstrap.ErrNotFound,
},
{
desc: "update a config",
thingID: c.MFThing,
cert: "cert",
certKey: "certKey",
ca: "ca",
owner: c.Owner,
err: nil,
},
}
for _, tc := range cases {
err := repo.UpdateCert(tc.owner, tc.thingID, tc.cert, tc.certKey, tc.ca)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnections(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Use UUID to prevent conflicts.
uid, err = uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.MFChannels = []bootstrap.Channel{}
c2, err := repo.Save(c, []string{channels[0]})
require.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
cases := []struct {
desc string
owner string
id string
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "update connections of non-existing config",
owner: config.Owner,
id: "unknown",
channels: nil,
connections: []string{channels[1]},
err: bootstrap.ErrNotFound,
},
{
desc: "update connections",
owner: config.Owner,
id: c.MFThing,
channels: nil,
connections: []string{channels[1]},
err: nil,
},
{
desc: "update connections with existing channels",
owner: config.Owner,
id: c2,
channels: nil,
connections: channels,
err: nil,
},
{
desc: "update connections no channels",
owner: config.Owner,
id: c.MFThing,
channels: nil,
connections: nil,
err: nil,
},
}
for _, tc := range cases {
err := repo.UpdateConnections(tc.owner, tc.id, tc.channels, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemove(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
id, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Removal works the same for both existing and non-existing
// (removed) config
for i := 0; i < 2; i++ {
err := repo.Remove(c.Owner, id)
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
_, err = repo.RetrieveByID(c.Owner, id)
require.True(t, errors.Contains(err, bootstrap.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
}
}
func TestChangeState(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
owner string
id string
state bootstrap.State
err error
}{
{
desc: "change state with wrong owner",
id: saved,
owner: "2",
err: bootstrap.ErrNotFound,
},
{
desc: "change state with wrong id",
id: "wrong",
owner: c.Owner,
err: bootstrap.ErrNotFound,
},
{
desc: "change state to Active",
id: saved,
owner: c.Owner,
state: bootstrap.Active,
err: nil,
},
{
desc: "change state to Inactive",
id: saved,
owner: c.Owner,
state: bootstrap.Inactive,
err: nil,
},
}
for _, tc := range cases {
err := repo.ChangeState(tc.owner, tc.id, tc.state)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListExisting(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
var chs []bootstrap.Channel
for _, ch := range config.MFChannels {
chs = append(chs, ch)
}
cases := []struct {
desc string
owner string
connections []string
existing []bootstrap.Channel
}{
{
desc: "list all existing channels",
owner: c.Owner,
connections: channels,
existing: chs,
},
{
desc: "list a subset of existing channels",
owner: c.Owner,
connections: []string{channels[0], "5"},
existing: []bootstrap.Channel{chs[0]},
},
{
desc: "list a subset of existing channels empty",
owner: c.Owner,
connections: []string{"5", "6"},
existing: []bootstrap.Channel{},
},
}
for _, tc := range cases {
existing, err := repo.ListExisting(tc.owner, tc.connections)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.existing, existing))
}
}
func TestSaveUnknown(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
cases := []struct {
desc string
externalID string
externalKey string
err error
}{
{
desc: "save unknown",
externalID: "unknown",
externalKey: "unknown",
err: nil,
},
{
desc: "save invalid unknown",
externalID: "unknown",
externalKey: "",
err: nil,
},
}
for _, tc := range cases {
err := repo.SaveUnknown(tc.externalKey, tc.externalID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveUnknown(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
for i := 0; i < numConfigs; i++ {
id, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
repo.SaveUnknown(id.String(), id.String())
}
cases := []struct {
desc string
offset uint64
limit uint64
size int
}{
{
desc: "retrieve all",
offset: 0,
limit: uint64(numConfigs),
size: numConfigs,
},
{
desc: "retrieve a subset",
offset: 5,
limit: uint64(numConfigs - 5),
size: numConfigs - 5,
},
}
for _, tc := range cases {
ret := repo.RetrieveUnknown(tc.offset, tc.limit)
size := len(ret.Configs)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
}
}
func TestRemoveThing(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
for i := 0; i < 2; i++ {
err := repo.RemoveThing(saved)
assert.Nil(t, err, fmt.Sprintf("an unexpected error occured: %s\n", err))
}
}
func TestUpdateChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
id := c.MFChannels[0].ID
update := bootstrap.Channel{
ID: id,
Name: "update name",
Metadata: map[string]interface{}{"update": "metadata update"},
}
err = repo.UpdateChannel(update)
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
var retreved bootstrap.Channel
for _, c := range cfg.MFChannels {
if c.ID == id {
retreved = c
break
}
}
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
}
func TestRemoveChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = repo.RemoveChannel(c.MFChannels[0].ID)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.NotContains(t, cfg.MFChannels, c.MFChannels[0], fmt.Sprintf("expected to remove channel %s from %s", c.MFChannels[0], cfg.MFChannels))
}
func TestDisconnectThing(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.MFKey = uid.String()
c.MFThing = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = repo.DisconnectThing(c.MFChannels[0].ID, saved)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected ti be inactive when a connection is removed from %s", cfg))
}
func deleteChannels(repo bootstrap.ConfigRepository) error {
for _, ch := range channels {
if err := repo.RemoveChannel(ch); err != nil {
return err
}
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package postgres contains repository implementations using PostgreSQL as
// the underlying database.
package postgres
-100
View File
@@ -1,100 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"fmt"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq" // required for SQL access
migrate "github.com/rubenv/sql-migrate"
)
// Config defines the options that are used when connecting to a PostgreSQL instance
type Config struct {
Host string
Port string
User string
Pass string
Name string
SSLMode string
SSLCert string
SSLKey string
SSLRootCert string
}
// Connect creates a connection to the PostgreSQL instance and applies any
// unapplied database migrations. A non-nil error is returned to indicate
// failure.
func Connect(cfg Config) (*sqlx.DB, error) {
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
db, err := sqlx.Open("postgres", url)
if err != nil {
return nil, err
}
if err := migrateDB(db); err != nil {
return nil, err
}
return db, nil
}
func migrateDB(db *sqlx.DB) error {
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "configs_1",
Up: []string{
`CREATE TABLE IF NOT EXISTS configs (
mainflux_thing TEXT UNIQUE NOT NULL,
owner VARCHAR(254),
name TEXT,
mainflux_key CHAR(36) UNIQUE NOT NULL,
external_id TEXT UNIQUE NOT NULL,
external_key TEXT NOT NULL,
content TEXT,
client_cert TEXT,
client_key TEXT,
ca_cert TEXT,
state BIGINT NOT NULL,
PRIMARY KEY (mainflux_thing, owner)
)`,
`CREATE TABLE IF NOT EXISTS unknown_configs (
external_id TEXT UNIQUE NOT NULL,
external_key TEXT NOT NULL,
PRIMARY KEY (external_id, external_key)
)`,
`CREATE TABLE IF NOT EXISTS channels (
mainflux_channel TEXT UNIQUE NOT NULL,
owner VARCHAR(254),
name TEXT,
metadata JSON,
PRIMARY KEY (mainflux_channel, owner)
)`,
`CREATE TABLE IF NOT EXISTS connections (
channel_id TEXT,
channel_owner VARCHAR(256),
config_id TEXT,
config_owner VARCHAR(256),
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_thing, owner) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
)`,
},
Down: []string{
"DROP TABLE connections",
"DROP TABLE configs",
"DROP TABLE channels",
"DROP TABLE unknown_configs",
},
},
},
}
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
return err
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"fmt"
"log"
"os"
"testing"
"github.com/jmoiron/sqlx"
"github.com/mainflux/mainflux/bootstrap/postgres"
"github.com/mainflux/mainflux/logger"
dockertest "github.com/ory/dockertest/v3"
)
const (
wrongID = "0"
wrongValue = "wrong-value"
)
var (
testLog, _ = logger.New(os.Stdout, logger.Info.String())
db *sqlx.DB
)
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
cfg := []string{
"POSTGRES_USER=test",
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.2-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
port := container.GetPort("5432/tcp")
if err := pool.Retry(func() error {
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
db, err = sqlx.Open("postgres", url)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
dbConfig := postgres.Config{
Host: "localhost",
Port: port,
User: "test",
Pass: "test",
Name: "test",
SSLMode: "disable",
SSLCert: "",
SSLKey: "",
SSLRootCert: "",
}
if db, err = postgres.Connect(dbConfig); err != nil {
log.Fatalf("Could not setup test DB connection: %s", err)
}
code := m.Run()
// defers will not be run when using os.Exit
db.Close()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"io"
"net/http"
)
// bootstrapRes represent Mainflux Response to the Bootatrap request.
// This is used as a response from ConfigReader and can easily be
// replace with any other response format.
type bootstrapRes struct {
MFThing string `json:"mainflux_id"`
MFKey string `json:"mainflux_key"`
MFChannels []channelRes `json:"mainflux_channels"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
func (res bootstrapRes) Code() int {
return http.StatusOK
}
func (res bootstrapRes) Headers() map[string]string {
return map[string]string{}
}
func (res bootstrapRes) Empty() bool {
return false
}
type reader struct {
encKey []byte
}
// NewConfigReader return new reader which is used to generate response
// from the config.
func NewConfigReader(encKey []byte) ConfigReader {
return reader{encKey: encKey}
}
func (r reader) ReadConfig(cfg Config, secure bool) (interface{}, error) {
var channels []channelRes
for _, ch := range cfg.MFChannels {
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
}
res := bootstrapRes{
MFKey: cfg.MFKey,
MFThing: cfg.MFThing,
MFChannels: channels,
Content: cfg.Content,
ClientCert: cfg.ClientCert,
ClientKey: cfg.ClientKey,
CACert: cfg.CACert,
}
if secure {
b, err := json.Marshal(res)
if err != nil {
return nil, err
}
return r.encrypt(b)
}
return res, nil
}
func (r reader) encrypt(in []byte) ([]byte, error) {
block, err := aes.NewCipher(r.encKey)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(in))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
return ciphertext, nil
}
-126
View File
@@ -1,126 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package bootstrap_test
import (
"crypto/aes"
"crypto/cipher"
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type readChan struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
type readResp struct {
MFThing string `json:"mainflux_id"`
MFKey string `json:"mainflux_key"`
MFChannels []readChan `json:"mainflux_channels"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
func dec(in []byte) ([]byte, error) {
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
if len(in) < aes.BlockSize {
return nil, bootstrap.ErrMalformedEntity
}
iv := in[:aes.BlockSize]
in = in[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(in, in)
return in, nil
}
func TestReadConfig(t *testing.T) {
cfg := bootstrap.Config{
MFThing: "mf_id",
ClientCert: "client_cert",
ClientKey: "client_key",
CACert: "ca_cert",
MFKey: "mf_key",
MFChannels: []bootstrap.Channel{
bootstrap.Channel{
ID: "mf_id",
Name: "mf_name",
Metadata: map[string]interface{}{"key": "value}"},
},
},
Content: "content",
}
ret := readResp{
MFThing: "mf_id",
MFKey: "mf_key",
MFChannels: []readChan{
{
ID: "mf_id",
Name: "mf_name",
Metadata: map[string]interface{}{"key": "value}"},
},
},
Content: "content",
ClientCert: "client_cert",
ClientKey: "client_key",
CACert: "ca_cert",
}
bin, err := json.Marshal(ret)
require.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
reader := bootstrap.NewConfigReader(encKey)
cases := []struct {
desc string
config bootstrap.Config
enc []byte
secret bool
err error
}{
{
desc: "read a config",
config: cfg,
enc: bin,
secret: false,
},
{
desc: "read encrypted config",
config: cfg,
enc: bin,
secret: true,
},
}
for _, tc := range cases {
res, err := reader.ReadConfig(tc.config, tc.secret)
require.Nil(t, err, fmt.Sprintf("Reading config to succeed: %s.\n", err))
if tc.secret {
d, err := dec(res.([]byte))
require.Nil(t, err, fmt.Sprintf("Decrypting expected to succeed: %s.\n", err))
assert.Equal(t, tc.enc, d, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, d))
continue
}
b, err := json.Marshal(res)
require.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
assert.Equal(t, tc.enc, b, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, b))
resp, ok := res.(mainflux.Response)
require.True(t, ok, fmt.Sprintf("If not encrypted, reader should return response."))
assert.False(t, resp.Empty(), fmt.Sprintf("Response should not be empty %s.", err))
assert.Equal(t, http.StatusOK, resp.Code(), fmt.Sprintf("Default config response code should be 200."))
}
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package consumer contains events consumer for events
// published by Things service.
package consumer
-20
View File
@@ -1,20 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package consumer
type removeEvent struct {
id string
}
type updateChannelEvent struct {
id string
name string
metadata map[string]interface{}
}
// Connection event is either connect or disconnect event.
type disconnectEvent struct {
thingID string
channelID string
}
-158
View File
@@ -1,158 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package consumer
import (
"encoding/json"
"fmt"
"github.com/go-redis/redis"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/logger"
)
const (
stream = "mainflux.things"
group = "mainflux.bootstrap"
thingPrefix = "thing."
thingRemove = thingPrefix + "remove"
thingDisconnect = thingPrefix + "disconnect"
channelPrefix = "channel."
channelUpdate = channelPrefix + "update"
channelRemove = channelPrefix + "remove"
exists = "BUSYGROUP Consumer Group name already exists"
)
// Subscriber represents event source for things and channels provisioning.
type Subscriber interface {
// Subscribes to given subject and receives events.
Subscribe(string) error
}
type eventStore struct {
svc bootstrap.Service
client *redis.Client
consumer string
logger logger.Logger
}
// NewEventStore returns new event store instance.
func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string, log logger.Logger) Subscriber {
return eventStore{
svc: svc,
client: client,
consumer: consumer,
logger: log,
}
}
func (es eventStore) Subscribe(subject string) error {
err := es.client.XGroupCreateMkStream(stream, group, "$").Err()
if err != nil && err.Error() != exists {
return err
}
for {
streams, err := es.client.XReadGroup(&redis.XReadGroupArgs{
Group: group,
Consumer: es.consumer,
Streams: []string{stream, ">"},
Count: 100,
}).Result()
if err != nil || len(streams) == 0 {
continue
}
for _, msg := range streams[0].Messages {
event := msg.Values
var err error
switch event["operation"] {
case thingRemove:
rte := decodeRemoveThing(event)
err = es.handleRemoveThing(rte)
case thingDisconnect:
dte := decodeDisconnectThing(event)
err = es.handleDisconnectThing(dte)
case channelUpdate:
uce := decodeUpdateChannel(event)
err = es.handleUpdateChannel(uce)
case channelRemove:
rce := decodeRemoveChannel(event)
err = es.handleRemoveChannel(rce)
}
if err != nil {
es.logger.Warn(fmt.Sprintf("Failed to handle event sourcing: %s", err.Error()))
break
}
es.client.XAck(stream, group, msg.ID)
}
}
}
func decodeRemoveThing(event map[string]interface{}) removeEvent {
return removeEvent{
id: read(event, "id", ""),
}
}
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
strmeta := read(event, "metadata", "{}")
var metadata map[string]interface{}
if err := json.Unmarshal([]byte(strmeta), metadata); err != nil {
metadata = map[string]interface{}{}
}
return updateChannelEvent{
id: read(event, "id", ""),
name: read(event, "name", ""),
metadata: metadata,
}
}
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
return removeEvent{
id: read(event, "id", ""),
}
}
func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
return disconnectEvent{
channelID: read(event, "chan_id", ""),
thingID: read(event, "thing_id", ""),
}
}
func (es eventStore) handleRemoveThing(rte removeEvent) error {
return es.svc.RemoveConfigHandler(rte.id)
}
func (es eventStore) handleUpdateChannel(uce updateChannelEvent) error {
channel := bootstrap.Channel{
ID: uce.id,
Name: uce.name,
Metadata: uce.metadata,
}
return es.svc.UpdateChannelHandler(channel)
}
func (es eventStore) handleRemoveChannel(rce removeEvent) error {
return es.svc.RemoveChannelHandler(rce.id)
}
func (es eventStore) handleDisconnectThing(dte disconnectEvent) error {
return es.svc.DisconnectThingHandler(dte.channelID, dte.thingID)
}
func read(event map[string]interface{}, key, def string) string {
val, ok := event[key].(string)
if !ok {
return def
}
return val
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package producer contains the domain events needed to support
// event sourcing of Bootstrap service actions.
package producer
-134
View File
@@ -1,134 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package producer
import (
"strings"
"time"
"github.com/mainflux/mainflux/bootstrap"
)
const (
configPrefix = "config."
configCreate = configPrefix + "create"
configUpdate = configPrefix + "update"
configRemove = configPrefix + "remove"
thingPrefix = "thing."
thingBootstrap = thingPrefix + "bootstrap"
thingStateChange = thingPrefix + "state_change"
thingUpdateConnections = thingPrefix + "update_connections"
)
type event interface {
encode() map[string]interface{}
}
var (
_ event = (*createConfigEvent)(nil)
_ event = (*updateConfigEvent)(nil)
_ event = (*removeConfigEvent)(nil)
_ event = (*bootstrapEvent)(nil)
_ event = (*changeStateEvent)(nil)
_ event = (*updateConnectionsEvent)(nil)
)
type createConfigEvent struct {
mfThing string
owner string
name string
mfChannels []string
externalID string
content string
timestamp time.Time
}
func (cce createConfigEvent) encode() map[string]interface{} {
return map[string]interface{}{
"thing_id": cce.mfThing,
"owner": cce.owner,
"name": cce.name,
"channels": strings.Join(cce.mfChannels, ", "),
"external_id": cce.externalID,
"content": cce.content,
"timestamp": cce.timestamp.Unix(),
"operation": configCreate,
}
}
type updateConfigEvent struct {
mfThing string
name string
content string
timestamp time.Time
}
func (uce updateConfigEvent) encode() map[string]interface{} {
return map[string]interface{}{
"thing_id": uce.mfThing,
"name": uce.name,
"content": uce.content,
"timestamp": uce.timestamp.Unix(),
"operation": configUpdate,
}
}
type removeConfigEvent struct {
mfThing string
timestamp time.Time
}
func (rce removeConfigEvent) encode() map[string]interface{} {
return map[string]interface{}{
"thing_id": rce.mfThing,
"timestamp": rce.timestamp.Unix(),
"operation": configRemove,
}
}
type bootstrapEvent struct {
externalID string
success bool
timestamp time.Time
}
func (be bootstrapEvent) encode() map[string]interface{} {
return map[string]interface{}{
"external_id": be.externalID,
"success": be.success,
"timestamp": be.timestamp.Unix(),
"operation": thingBootstrap,
}
}
type changeStateEvent struct {
mfThing string
state bootstrap.State
timestamp time.Time
}
func (cse changeStateEvent) encode() map[string]interface{} {
return map[string]interface{}{
"thing_id": cse.mfThing,
"state": cse.state.String(),
"timestamp": cse.timestamp.Unix(),
"operation": thingStateChange,
}
}
type updateConnectionsEvent struct {
mfThing string
mfChannels []string
timestamp time.Time
}
func (uce updateConnectionsEvent) encode() map[string]interface{} {
return map[string]interface{}{
"thing_id": uce.mfThing,
"channels": strings.Join(uce.mfChannels, ", "),
"timestamp": uce.timestamp.Unix(),
"operation": thingUpdateConnections,
}
}
-53
View File
@@ -1,53 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package producer_test
import (
"fmt"
"log"
"os"
"testing"
"github.com/go-redis/redis"
dockertest "github.com/ory/dockertest/v3"
)
const (
wrongID = 0
wrongValue = "wrong-value"
)
var redisClient *redis.Client
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
container, err := pool.Run("redis", "5.0-alpine", nil)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
if err := pool.Retry(func() error {
redisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("localhost:%s", container.GetPort("6379/tcp")),
Password: "",
DB: 0,
})
return redisClient.Ping().Err()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
code := m.Run()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}
-178
View File
@@ -1,178 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package producer
import (
"time"
"github.com/go-redis/redis"
"github.com/mainflux/mainflux/bootstrap"
)
const (
streamID = "mainflux.bootstrap"
streamLen = 1000
)
var _ bootstrap.Service = (*eventStore)(nil)
type eventStore struct {
svc bootstrap.Service
client *redis.Client
}
// NewEventStoreMiddleware returns wrapper around bootstrap service that sends
// events to event store.
func NewEventStoreMiddleware(svc bootstrap.Service, client *redis.Client) bootstrap.Service {
return eventStore{
svc: svc,
client: client,
}
}
func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config, error) {
saved, err := es.svc.Add(token, cfg)
if err != nil {
return saved, err
}
var channels []string
for _, ch := range saved.MFChannels {
channels = append(channels, ch.ID)
}
ev := createConfigEvent{
mfThing: saved.MFThing,
owner: saved.Owner,
name: saved.Name,
mfChannels: channels,
externalID: saved.ExternalID,
content: saved.Content,
timestamp: time.Now(),
}
es.add(ev)
return saved, err
}
func (es eventStore) View(token, id string) (bootstrap.Config, error) {
return es.svc.View(token, id)
}
func (es eventStore) Update(token string, cfg bootstrap.Config) error {
if err := es.svc.Update(token, cfg); err != nil {
return err
}
ev := updateConfigEvent{
mfThing: cfg.MFThing,
name: cfg.Name,
content: cfg.Content,
timestamp: time.Now(),
}
es.add(ev)
return nil
}
func (es eventStore) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) error {
return es.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
}
func (es eventStore) UpdateConnections(token, id string, connections []string) error {
if err := es.svc.UpdateConnections(token, id, connections); err != nil {
return err
}
ev := updateConnectionsEvent{
mfThing: id,
mfChannels: connections,
timestamp: time.Now(),
}
es.add(ev)
return nil
}
func (es eventStore) List(token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
return es.svc.List(token, filter, offset, limit)
}
func (es eventStore) Remove(token, id string) error {
if err := es.svc.Remove(token, id); err != nil {
return err
}
ev := removeConfigEvent{
mfThing: id,
timestamp: time.Now(),
}
es.add(ev)
return nil
}
func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (bootstrap.Config, error) {
cfg, err := es.svc.Bootstrap(externalKey, externalID, secure)
ev := bootstrapEvent{
externalID: externalID,
timestamp: time.Now(),
success: true,
}
if err != nil {
ev.success = false
}
es.add(ev)
return cfg, err
}
func (es eventStore) ChangeState(token, id string, state bootstrap.State) error {
if err := es.svc.ChangeState(token, id, state); err != nil {
return err
}
ev := changeStateEvent{
mfThing: id,
state: state,
timestamp: time.Now(),
}
es.add(ev)
return nil
}
func (es eventStore) RemoveConfigHandler(id string) error {
return es.svc.RemoveConfigHandler(id)
}
func (es eventStore) RemoveChannelHandler(id string) error {
return es.svc.RemoveChannelHandler(id)
}
func (es eventStore) UpdateChannelHandler(channel bootstrap.Channel) error {
return es.UpdateChannelHandler(channel)
}
func (es eventStore) DisconnectThingHandler(channelID, thingID string) error {
return es.svc.DisconnectThingHandler(channelID, thingID)
}
func (es eventStore) add(ev event) error {
record := &redis.XAddArgs{
Stream: streamID,
MaxLenApprox: streamLen,
Values: ev.encode(),
}
return es.client.XAdd(record).Err()
}
-549
View File
@@ -1,549 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package producer_test
import (
"fmt"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/go-redis/redis"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/errors"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/bootstrap/mocks"
"github.com/mainflux/mainflux/bootstrap/redis/producer"
mfsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/mainflux/mainflux/things"
httpapi "github.com/mainflux/mainflux/things/api/things/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
streamID = "mainflux.bootstrap"
email = "user@example.com"
validToken = "validToken"
unknownID = "1"
unknownKey = "2"
channelsNum = 3
defaultTimout = 5
configPrefix = "config."
configCreate = configPrefix + "create"
configUpdate = configPrefix + "update"
configRemove = configPrefix + "remove"
thingPrefix = "thing."
thingStateChange = thingPrefix + "state_change"
thingBootstrap = thingPrefix + "bootstrap"
thingUpdateConnections = thingPrefix + "update_connections"
)
var (
encKey = []byte("1234567891011121")
channel = bootstrap.Channel{
ID: "1",
Name: "name",
Metadata: map[string]interface{}{"name": "value"},
}
config = bootstrap.Config{
ExternalID: "external_id",
ExternalKey: "external_key",
MFChannels: []bootstrap.Channel{channel},
Content: "config",
}
)
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
configs := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
config := mfsdk.Config{
BaseURL: url,
}
sdk := mfsdk.NewSDK(config)
return bootstrap.New(auth, configs, sdk, encKey)
}
func newThingsService(auth mainflux.AuthNServiceClient) things.Service {
channels := make(map[string]things.Channel, channelsNum)
for i := 0; i < channelsNum; i++ {
id := strconv.Itoa(i + 1)
channels[id] = things.Channel{
ID: id,
Owner: email,
Metadata: map[string]interface{}{"meta": "data"},
}
}
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
}
func newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(mocktracer.New(), svc)
return httptest.NewServer(mux)
}
func TestAdd(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
var channels []string
for _, ch := range config.MFChannels {
channels = append(channels, ch.ID)
}
invalidConfig := config
invalidConfig.MFChannels = []bootstrap.Channel{bootstrap.Channel{ID: "empty"}}
cases := []struct {
desc string
config bootstrap.Config
token string
err error
event map[string]interface{}
}{
{
desc: "create config successfully",
config: config,
token: validToken,
err: nil,
event: map[string]interface{}{
"thing_id": "1",
"owner": email,
"name": config.Name,
"channels": strings.Join(channels, ", "),
"external_id": config.ExternalID,
"content": config.Content,
"timestamp": time.Now().Unix(),
"operation": configCreate,
},
},
{
desc: "create invalid config",
config: invalidConfig,
token: validToken,
err: bootstrap.ErrMalformedEntity,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
_, err := svc.Add(tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
}).Val()
var event map[string]interface{}
if len(streams) > 0 && len(streams[0].Messages) > 0 {
msg := streams[0].Messages[0]
event = msg.Values
lastID = msg.ID
}
test(t, tc.event, event, tc.desc)
}
}
func TestView(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
svcConfig, svcErr := svc.View(validToken, saved.MFThing)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
esConfig, esErr := svc.View(validToken, saved.MFThing)
assert.Equal(t, svcConfig, esConfig, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfig, esConfig))
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
}
func TestUpdate(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
modified := saved
modified.Content = "new-config"
modified.Name = "new name"
nonExisting := config
nonExisting.MFThing = "unknown"
cases := []struct {
desc string
config bootstrap.Config
token string
err error
event map[string]interface{}
}{
{
desc: "update config successfully",
config: modified,
token: validToken,
err: nil,
event: map[string]interface{}{
"thing_id": modified.MFThing,
"name": modified.Name,
"content": modified.Content,
"timestamp": time.Now().Unix(),
"operation": configUpdate,
},
},
{
desc: "update non-existing config",
config: nonExisting,
token: validToken,
err: bootstrap.ErrNotFound,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.Update(tc.token, tc.config)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
}).Val()
var event map[string]interface{}
if len(streams) > 0 && len(streams[0].Messages) > 0 {
msg := streams[0].Messages[0]
event = msg.Values
lastID = msg.ID
}
test(t, tc.event, event, tc.desc)
}
}
func TestUpdateConnections(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
cases := []struct {
desc string
id string
token string
connections []string
err error
event map[string]interface{}
}{
{
desc: "update connections successfully",
id: saved.MFThing,
token: validToken,
connections: []string{"2"},
err: nil,
event: map[string]interface{}{
"thing_id": saved.MFThing,
"channels": "2",
"timestamp": time.Now().Unix(),
"operation": thingUpdateConnections,
},
},
{
desc: "update connections unsuccessfully",
id: saved.MFThing,
token: validToken,
connections: []string{"256"},
err: bootstrap.ErrMalformedEntity,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
}).Val()
var event map[string]interface{}
if len(streams) > 0 && len(streams[0].Messages) > 0 {
msg := streams[0].Messages[0]
event = msg.Values
lastID = msg.ID
}
test(t, tc.event, event, tc.desc)
}
}
func TestList(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
offset := uint64(0)
limit := uint64(10)
svcConfigs, svcErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
esConfigs, esErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
assert.Equal(t, svcConfigs, esConfigs, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfigs, esConfigs))
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
}
func TestRemove(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
cases := []struct {
desc string
id string
token string
err error
event map[string]interface{}
}{
{
desc: "remove config successfully",
id: saved.MFThing,
token: validToken,
err: nil,
event: map[string]interface{}{
"thing_id": saved.MFThing,
"timestamp": time.Now().Unix(),
"operation": configRemove,
},
},
{
desc: "remove config with invalid credentials",
id: saved.MFThing,
token: "",
err: bootstrap.ErrUnauthorizedAccess,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.Remove(tc.token, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
}).Val()
var event map[string]interface{}
if len(streams) > 0 && len(streams[0].Messages) > 0 {
msg := streams[0].Messages[0]
event = msg.Values
lastID = msg.ID
}
test(t, tc.event, event, tc.desc)
}
}
func TestBootstrap(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
cases := []struct {
desc string
externalID string
externalKey string
err error
event map[string]interface{}
}{
{
desc: "bootstrap successfully",
externalID: saved.ExternalID,
externalKey: saved.ExternalKey,
err: nil,
event: map[string]interface{}{
"external_id": saved.ExternalID,
"success": "1",
"timestamp": time.Now().Unix(),
"operation": thingBootstrap,
},
},
{
desc: "bootstrap with an error",
externalID: saved.ExternalID,
externalKey: "external",
err: bootstrap.ErrNotFound,
event: map[string]interface{}{
"external_id": saved.ExternalID,
"success": "0",
"timestamp": time.Now().Unix(),
"operation": thingBootstrap,
},
},
}
lastID := "0"
for _, tc := range cases {
_, err := svc.Bootstrap(tc.externalKey, tc.externalID, false)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
}).Val()
var event map[string]interface{}
if len(streams) > 0 && len(streams[0].Messages) > 0 {
msg := streams[0].Messages[0]
event = msg.Values
lastID = msg.ID
}
test(t, tc.event, event, tc.desc)
}
}
func TestChangeState(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
cases := []struct {
desc string
id string
token string
state bootstrap.State
err error
event map[string]interface{}
}{
{
desc: "change state to active",
id: saved.MFThing,
token: validToken,
state: bootstrap.Active,
err: nil,
event: map[string]interface{}{
"thing_id": saved.MFThing,
"state": bootstrap.Active.String(),
"timestamp": time.Now().Unix(),
"operation": thingStateChange,
},
},
{
desc: "change state invalid credentials",
id: saved.MFThing,
token: "",
state: bootstrap.Inactive,
err: bootstrap.ErrUnauthorizedAccess,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.ChangeState(tc.token, tc.id, tc.state)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
}).Val()
var event map[string]interface{}
if len(streams) > 0 && len(streams[0].Messages) > 0 {
msg := streams[0].Messages[0]
event = msg.Values
lastID = msg.ID
}
test(t, tc.event, event, tc.desc)
}
}
func test(t *testing.T, expected, actual map[string]interface{}, description string) {
if expected != nil && actual != nil {
ts1 := expected["timestamp"].(int64)
ts2, err := strconv.ParseInt(actual["timestamp"].(string), 10, 64)
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid timestamp, got %s", description, err))
val := ts1 == ts2 || ts2 <= ts1+defaultTimout
assert.True(t, val, fmt.Sprintf("%s: timestamp is not in valid range", description))
delete(expected, "timestamp")
delete(actual, "timestamp")
assert.Equal(t, expected, actual, fmt.Sprintf("%s: expected %v got %v\n", description, expected, actual))
}
}
-513
View File
@@ -1,513 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/errors"
mfsdk "github.com/mainflux/mainflux/sdk/go"
)
var (
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
// ErrMalformedEntity indicates malformed entity specification.
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
// ErrConflict indicates that entity with the same ID or external ID already exists.
ErrConflict = errors.New("entity already exists")
// ErrThings indicates failure to communicate with Mainflux Things service.
// It can be due to networking error or invalid/unauthorized request.
ErrThings = errors.New("failed to receive response from Things service")
// ErrExternalKeyNotFound indicates a non-existent bootstrap configuration for given external key
ErrExternalKeyNotFound = errors.New("failed to get bootstrap configuration for given external key")
// ErrSecureBootstrap indicates erron in getting bootstrap configuration for given encrypted external key
ErrSecureBootstrap = errors.New("failed to get bootstrap configuration for given encrypted external key")
errAddBootstrap = errors.New("failed to add bootstrap configuration")
errUpdateConnections = errors.New("failed to update connections")
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
errBootstrap = errors.New("failed to read bootstrap configuration")
errChangeState = errors.New("failed to change state of bootstrap configuration")
errUpdateChannel = errors.New("failed to update channel")
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
errRemoveChannel = errors.New("failed to remove channel")
errCreateThing = errors.New("failed to create thing")
errDisconnectThing = errors.New("failed to disconnect thing")
errThingNotFound = errors.New("thing not found")
errCheckChannels = errors.New("failed to check if channels exists")
errConnectionChannels = errors.New("failed to check channels connections")
errUpdateCert = errors.New("failed to update cert")
)
var _ Service = (*bootstrapService)(nil)
// Service specifies an API that must be fulfilled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// Add adds new Thing Config to the user identified by the provided token.
Add(token string, cfg Config) (Config, error)
// View returns Thing Config with given ID belonging to the user identified by the given token.
View(token, id string) (Config, error)
// Update updates editable fields of the provided Config.
Update(token string, cfg Config) error
// UpdateCert updates an existing Config certificate and token.
// A non-nil error is returned to indicate operation failure.
UpdateCert(token, thingID, clientCert, clientKey, caCert string) error
// UpdateConnections updates list of Channels related to given Config.
UpdateConnections(token, id string, connections []string) error
// List returns subset of Configs with given search params that belong to the
// user identified by the given token.
List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
// Remove removes Config with specified token that belongs to the user identified by the given token.
Remove(token, id string) error
// Bootstrap returns Config to the Thing with provided external ID using external key.
Bootstrap(externalKey, externalID string, secure bool) (Config, error)
// ChangeState changes state of the Thing with given ID and owner.
ChangeState(token, id string, state State) error
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
// handlers for events. That's why these methods surpass ownership check.
// UpdateChannelHandler updates Channel with data received from an event.
UpdateChannelHandler(channel Channel) error
// RemoveConfigHandler removes Configuration with id received from an event.
RemoveConfigHandler(id string) error
// RemoveChannelHandler removes Channel with id received from an event.
RemoveChannelHandler(id string) error
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
DisconnectThingHandler(channelID, thingID string) error
}
// ConfigReader is used to parse Config into format which will be encoded
// as a JSON and consumed from the client side. The purpose of this interface
// is to provide convenient way to generate custom configuration response
// based on the specific Config which will be consumed by the client.
type ConfigReader interface {
ReadConfig(Config, bool) (interface{}, error)
}
type bootstrapService struct {
auth mainflux.AuthNServiceClient
configs ConfigRepository
sdk mfsdk.SDK
encKey []byte
reader ConfigReader
}
// New returns new Bootstrap service.
func New(auth mainflux.AuthNServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
return &bootstrapService{
configs: configs,
sdk: sdk,
auth: auth,
encKey: encKey,
}
}
func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
owner, err := bs.identify(token)
if err != nil {
return Config{}, err
}
toConnect := bs.toIDList(cfg.MFChannels)
// Check if channels exist. This is the way to prevent fetching channels that already exist.
existing, err := bs.configs.ListExisting(owner, toConnect)
if err != nil {
return Config{}, errors.Wrap(errCheckChannels, err)
}
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), token)
if err != nil {
return Config{}, errors.Wrap(errConnectionChannels, err)
}
id := cfg.MFThing
mfThing, err := bs.thing(token, id)
if err != nil {
return Config{}, errors.Wrap(errAddBootstrap, err)
}
cfg.MFThing = mfThing.ID
cfg.Owner = owner
cfg.State = Inactive
cfg.MFKey = mfThing.Key
saved, err := bs.configs.Save(cfg, toConnect)
if err != nil {
if id == "" {
if errT := bs.sdk.DeleteThing(cfg.MFThing, token); errT != nil {
err = errors.Wrap(err, errT)
}
}
return Config{}, errors.Wrap(errAddBootstrap, err)
}
cfg.MFThing = saved
cfg.MFChannels = append(cfg.MFChannels, existing...)
return cfg, nil
}
func (bs bootstrapService) View(token, id string) (Config, error) {
owner, err := bs.identify(token)
if err != nil {
return Config{}, err
}
return bs.configs.RetrieveByID(owner, id)
}
func (bs bootstrapService) Update(token string, cfg Config) error {
owner, err := bs.identify(token)
if err != nil {
return err
}
cfg.Owner = owner
return bs.configs.Update(cfg)
}
func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caCert string) error {
owner, err := bs.identify(token)
if err != nil {
return err
}
if err := bs.configs.UpdateCert(owner, thingID, clientCert, clientKey, caCert); err != nil {
return errors.Wrap(errUpdateCert, err)
}
return nil
}
func (bs bootstrapService) UpdateConnections(token, id string, connections []string) error {
owner, err := bs.identify(token)
if err != nil {
return err
}
cfg, err := bs.configs.RetrieveByID(owner, id)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
add, remove := bs.updateList(cfg, connections)
// Check if channels exist. This is the way to prevent fetching channels that already exist.
existing, err := bs.configs.ListExisting(owner, connections)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), token)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
cfg.MFChannels = channels
var connect, disconnect []string
if cfg.State == Active {
connect = add
disconnect = remove
}
for _, c := range disconnect {
if err := bs.sdk.DisconnectThing(id, c, token); err != nil {
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
continue
}
return ErrThings
}
}
for _, c := range connect {
conIDs := mfsdk.ConnectionIDs{
ChannelIDs: []string{c},
ThingIDs: []string{id},
}
if err := bs.sdk.Connect(conIDs, token); err != nil {
if errors.Contains(err, mfsdk.ErrFailedConnect) {
return ErrMalformedEntity
}
return ErrThings
}
}
return bs.configs.UpdateConnections(owner, id, channels, connections)
}
func (bs bootstrapService) List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
owner, err := bs.identify(token)
if err != nil {
return ConfigsPage{}, err
}
if filter.Unknown {
return bs.configs.RetrieveUnknown(offset, limit), nil
}
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
}
func (bs bootstrapService) Remove(token, id string) error {
owner, err := bs.identify(token)
if err != nil {
return err
}
if err := bs.configs.Remove(owner, id); err != nil {
return errors.Wrap(errRemoveBootstrap, err)
}
return nil
}
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
cfg, err := bs.configs.RetrieveByExternalID(externalID)
if err != nil {
if errors.Contains(err, ErrNotFound) {
bs.configs.SaveUnknown(externalKey, externalID)
return Config{}, ErrNotFound
}
return cfg, errors.Wrap(errBootstrap, err)
}
if secure {
dec, err := bs.dec(externalKey)
if err != nil {
return Config{}, errors.Wrap(ErrSecureBootstrap, err)
}
externalKey = dec
}
if cfg.ExternalKey != externalKey {
return Config{}, errors.Wrap(ErrExternalKeyNotFound, ErrNotFound)
}
return cfg, nil
}
func (bs bootstrapService) ChangeState(token, id string, state State) error {
owner, err := bs.identify(token)
if err != nil {
return err
}
cfg, err := bs.configs.RetrieveByID(owner, id)
if err != nil {
return errors.Wrap(errChangeState, err)
}
if cfg.State == state {
return nil
}
switch state {
case Active:
for _, c := range cfg.MFChannels {
conIDs := mfsdk.ConnectionIDs{
ChannelIDs: []string{c.ID},
ThingIDs: []string{cfg.MFThing},
}
if err := bs.sdk.Connect(conIDs, token); err != nil {
return ErrThings
}
}
case Inactive:
for _, c := range cfg.MFChannels {
if err := bs.sdk.DisconnectThing(cfg.MFThing, c.ID, token); err != nil {
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
continue
}
return ErrThings
}
}
}
if err := bs.configs.ChangeState(owner, id, state); err != nil {
return errors.Wrap(errChangeState, err)
}
return nil
}
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
if err := bs.configs.UpdateChannel(channel); err != nil {
return errors.Wrap(errUpdateChannel, err)
}
return nil
}
func (bs bootstrapService) RemoveConfigHandler(id string) error {
if err := bs.configs.RemoveThing(id); err != nil {
return errors.Wrap(errRemoveConfig, err)
}
return nil
}
func (bs bootstrapService) RemoveChannelHandler(id string) error {
if err := bs.configs.RemoveChannel(id); err != nil {
return errors.Wrap(errRemoveChannel, err)
}
return nil
}
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
if err := bs.configs.DisconnectThing(channelID, thingID); err != nil {
return errors.Wrap(errDisconnectThing, err)
}
return nil
}
func (bs bootstrapService) identify(token string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := bs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return "", ErrUnauthorizedAccess
}
return res.GetValue(), nil
}
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
thingID := id
var err error
if id == "" {
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, token)
if err != nil {
return mfsdk.Thing{}, errors.Wrap(errCreateThing, err)
}
}
thing, err := bs.sdk.Thing(thingID, token)
if err != nil {
if errors.Contains(err, mfsdk.ErrFailedFetch) {
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, ErrNotFound)
}
if id != "" {
if errT := bs.sdk.DeleteThing(thingID, token); errT != nil {
err = errors.Wrap(err, errT)
}
}
return mfsdk.Thing{}, errors.Wrap(ErrThings, err)
}
return thing, nil
}
func (bs bootstrapService) connectionChannels(channels, existing []string, token string) ([]Channel, error) {
add := make(map[string]bool, len(channels))
for _, ch := range channels {
add[ch] = true
}
for _, ch := range existing {
if add[ch] == true {
delete(add, ch)
}
}
var ret []Channel
for id := range add {
ch, err := bs.sdk.Channel(id, token)
if err != nil {
return nil, errors.Wrap(ErrMalformedEntity, err)
}
ret = append(ret, Channel{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
return ret, nil
}
// Method updateList accepts config and channel IDs and returns three lists:
// 1) IDs of Channels to be added
// 2) IDs of Channels to be removed
// 3) IDs of common Channels for these two configs
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
var disconnect map[string]bool
disconnect = make(map[string]bool, len(cfg.MFChannels))
for _, c := range cfg.MFChannels {
disconnect[c.ID] = true
}
for _, c := range connections {
if disconnect[c] {
// Don't disconnect common elements.
delete(disconnect, c)
continue
}
// Connect new elements.
add = append(add, c)
}
for v := range disconnect {
remove = append(remove, v)
}
return
}
func (bs bootstrapService) toIDList(channels []Channel) []string {
var ret []string
for _, ch := range channels {
ret = append(ret, ch.ID)
}
return ret
}
func (bs bootstrapService) dec(in string) (string, error) {
ciphertext, err := hex.DecodeString(in)
if err != nil {
return "", ErrNotFound
}
block, err := aes.NewCipher(bs.encKey)
if err != nil {
return "", err
}
if len(ciphertext) < aes.BlockSize {
return "", ErrMalformedEntity
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return string(ciphertext), nil
}
-807
View File
@@ -1,807 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package bootstrap_test
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http/httptest"
"strconv"
"testing"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/gofrs/uuid"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/bootstrap/mocks"
"github.com/mainflux/mainflux/errors"
mfsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/mainflux/mainflux/things"
httpapi "github.com/mainflux/mainflux/things/api/things/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
validToken = "validToken"
invalidToken = "invalidToken"
email = "test@example.com"
unknown = "unknown"
unknownID = "1"
unknownKey = "2"
channelsNum = 3
)
var (
encKey = []byte("1234567891011121")
channel = bootstrap.Channel{
ID: "1",
Name: "name",
Metadata: map[string]interface{}{"name": "value"},
}
config = bootstrap.Config{
ExternalID: "external_id",
ExternalKey: "external_key",
MFChannels: []bootstrap.Channel{channel},
Content: "config",
}
)
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
config := mfsdk.Config{
BaseURL: url,
}
sdk := mfsdk.NewSDK(config)
return bootstrap.New(auth, things, sdk, encKey)
}
func newThingsService(auth mainflux.AuthNServiceClient) things.Service {
channels := make(map[string]things.Channel, channelsNum)
for i := 0; i < channelsNum; i++ {
id := strconv.Itoa(i + 1)
channels[id] = things.Channel{
ID: id,
Owner: email,
Metadata: map[string]interface{}{"meta": "data"},
}
}
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
}
func newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(mocktracer.New(), svc)
return httptest.NewServer(mux)
}
func enc(in []byte) ([]byte, error) {
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(in))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
return ciphertext, nil
}
func TestAdd(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
neID := config
neID.MFThing = "non-existent"
wrongChannels := config
ch := channel
ch.ID = "invalid"
wrongChannels.MFChannels = append(wrongChannels.MFChannels, ch)
cases := []struct {
desc string
config bootstrap.Config
token string
err error
}{
{
desc: "add a new config",
config: config,
token: validToken,
err: nil,
},
{
desc: "add a config with an invalid ID",
config: neID,
token: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "add a config with wrong credentials",
config: config,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "add a config with invalid list of channels",
config: wrongChannels,
token: validToken,
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
_, err := svc.Add(tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestView(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
token string
err error
}{
{
desc: "view an existing config",
id: saved.MFThing,
token: validToken,
err: nil,
},
{
desc: "view a non-existing config",
id: unknown,
token: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "view a config with wrong credentials",
id: config.MFThing,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
_, err := svc.View(tc.token, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdate(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
c := config
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
modifiedCreated := saved
modifiedCreated.Content = "new-config"
modifiedCreated.Name = "new name"
nonExisting := config
nonExisting.MFThing = unknown
cases := []struct {
desc string
config bootstrap.Config
token string
err error
}{
{
desc: "update a config with state Created",
config: modifiedCreated,
token: validToken,
err: nil,
},
{
desc: "update a non-existing config",
config: nonExisting,
token: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "update a config with wrong credentials",
config: saved,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.Update(tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateCert(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
c := config
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
token string
thingKey string
clientCert string
clientKey string
caCert string
err error
}{
{
desc: "update certs for the valid config",
thingKey: saved.MFKey,
clientCert: "newCert",
clientKey: "newKey",
caCert: "newCert",
token: validToken,
err: nil,
},
{
desc: "update cert for a non-existing config",
thingKey: "empty",
clientCert: "newCert",
clientKey: "newKey",
caCert: "newCert",
token: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "update config cert with wrong credentials",
thingKey: saved.MFKey,
clientCert: "newCert",
clientKey: "newKey",
caCert: "newCert",
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.UpdateCert(tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnections(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
c := config
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
created, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
externalID, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ExternalID = externalID.String()
active, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = svc.ChangeState(validToken, active.MFThing, bootstrap.Active)
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
nonExisting := config
nonExisting.MFThing = unknown
cases := []struct {
desc string
token string
id string
connections []string
err error
}{
{
desc: "update connections for config with state Inactive",
token: validToken,
id: created.MFThing,
connections: []string{"2"},
err: nil,
},
{
desc: "update connections for config with state Active",
token: validToken,
id: active.MFThing,
connections: []string{"3"},
err: nil,
},
{
desc: "update connections for non-existing config",
token: validToken,
id: "",
connections: []string{"3"},
err: bootstrap.ErrNotFound,
},
{
desc: "update connections with invalid channels",
token: validToken,
id: created.MFThing,
connections: []string{"wrong"},
err: bootstrap.ErrMalformedEntity,
},
{
desc: "update connections a config with wrong credentials",
token: invalidToken,
id: created.MFKey,
connections: []string{"2", "3"},
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestList(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
numThings := 101
var saved []bootstrap.Config
for i := 0; i < numThings; i++ {
c := config
id, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ExternalID = id.String()
c.ExternalKey = id.String()
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
s, err := svc.Add(validToken, c)
saved = append(saved, s)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
// Set one Thing to the different state
err := svc.ChangeState(validToken, "42", bootstrap.Active)
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
saved[41].State = bootstrap.Active
unknownConfig := bootstrap.Config{
ExternalID: unknownID,
ExternalKey: unknownKey,
}
cases := []struct {
desc string
config bootstrap.ConfigsPage
filter bootstrap.Filter
offset uint64
limit uint64
token string
err error
}{
{
desc: "list configs",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 0,
Limit: 10,
Configs: saved[0:10],
},
filter: bootstrap.Filter{},
token: validToken,
offset: 0,
limit: 10,
err: nil,
},
{
desc: "list configs with specified name",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 100,
Configs: saved[95:96],
},
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
token: validToken,
offset: 0,
limit: 100,
err: nil,
},
{
desc: "list configs unauthorized",
config: bootstrap.ConfigsPage{},
filter: bootstrap.Filter{},
token: invalidToken,
offset: 0,
limit: 10,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "list last page",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 95,
Limit: 10,
Configs: saved[95:],
},
filter: bootstrap.Filter{},
token: validToken,
offset: 95,
limit: 10,
err: nil,
},
{
desc: "list configs with Active state",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 35,
Limit: 20,
Configs: []bootstrap.Config{saved[41]},
},
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
token: validToken,
offset: 35,
limit: 20,
err: nil,
},
{
desc: "list unknown configs",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 20,
Configs: []bootstrap.Config{unknownConfig},
},
filter: bootstrap.Filter{Unknown: true},
token: validToken,
offset: 0,
limit: 20,
err: nil,
},
}
for _, tc := range cases {
result, err := svc.List(tc.token, tc.filter, tc.offset, tc.limit)
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemove(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
token string
err error
}{
{
desc: "view a config with wrong credentials",
id: saved.MFThing,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "remove an existing config",
id: saved.MFThing,
token: validToken,
err: nil,
},
{
desc: "remove removed config",
id: saved.MFThing,
token: validToken,
err: nil,
},
{
desc: "remove non-existing config",
id: unknown,
token: validToken,
err: nil,
},
}
for _, tc := range cases {
err := svc.Remove(tc.token, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestBootstrap(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
e, err := enc([]byte(saved.ExternalKey))
require.Nil(t, err, fmt.Sprintf("Encrypting external key expected to succeed: %s.\n", err))
cases := []struct {
desc string
config bootstrap.Config
externalKey string
externalID string
err error
encrypted bool
}{
{
desc: "bootstrap using invalid external id",
config: bootstrap.Config{},
externalID: "invalid",
externalKey: saved.ExternalKey,
err: bootstrap.ErrNotFound,
encrypted: false,
},
{
desc: "bootstrap using invalid external key",
config: bootstrap.Config{},
externalID: saved.ExternalID,
externalKey: "invalid",
err: bootstrap.ErrNotFound,
encrypted: false,
},
{
desc: "bootstrap an existing config",
config: saved,
externalID: saved.ExternalID,
externalKey: saved.ExternalKey,
err: nil,
encrypted: false,
},
{
desc: "bootstrap encrypted",
config: saved,
externalID: saved.ExternalID,
externalKey: hex.EncodeToString(e),
err: nil,
encrypted: true,
},
}
for _, tc := range cases {
config, err := svc.Bootstrap(tc.externalKey, tc.externalID, tc.encrypted)
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChangeState(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
state bootstrap.State
id string
token string
err error
}{
{
desc: "change state with wrong credentials",
state: bootstrap.Active,
id: saved.MFThing,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "change state of non-existing config",
state: bootstrap.Active,
id: unknown,
token: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "change state to Active",
state: bootstrap.Active,
id: saved.MFThing,
token: validToken,
err: nil,
},
{
desc: "change state to current state",
state: bootstrap.Active,
id: saved.MFThing,
token: validToken,
err: nil,
},
{
desc: "change state to Inactive",
state: bootstrap.Inactive,
id: saved.MFThing,
token: validToken,
err: nil,
},
}
for _, tc := range cases {
err := svc.ChangeState(tc.token, tc.id, tc.state)
assert.True(t, errors.Contains(err, tc.err), err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateChannelHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
ch := bootstrap.Channel{
ID: channel.ID,
Name: "new name",
Metadata: map[string]interface{}{"meta": "new"},
}
cases := []struct {
desc string
channel bootstrap.Channel
err error
}{
{
desc: "update an existing channel",
channel: ch,
err: nil,
},
{
desc: "update a non-existing channel",
channel: bootstrap.Channel{ID: ""},
err: nil,
},
}
for _, tc := range cases {
err := svc.UpdateChannelHandler(tc.channel)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemoveChannelHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
err error
}{
{
desc: "remove an existing channel",
id: channel.ID,
err: nil,
},
{
desc: "remove a non-existing channel",
id: "unknown",
err: nil,
},
}
for _, tc := range cases {
err := svc.RemoveChannelHandler(tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemoveCoinfigHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
err error
}{
{
desc: "remove an existing config",
id: saved.MFThing,
err: nil,
},
{
desc: "remove a non-existing channel",
id: "unknown",
err: nil,
},
}
for _, tc := range cases {
err := svc.RemoveConfigHandler(tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestDisconnectThingsHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
thingID string
channelID string
err error
}{
{
desc: "disconnect",
channelID: channel.ID,
thingID: saved.MFThing,
err: nil,
},
{
desc: "disconnect disconnected",
channelID: channel.ID,
thingID: saved.MFThing,
err: nil,
},
}
for _, tc := range cases {
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
-26
View File
@@ -1,26 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import "strconv"
const (
// Inactive Thing is created, but not able to exchange messages using Mainflux.
Inactive State = iota
// Active Thing is created, configured, and whitelisted.
Active
)
// State represents corresponding Mainflux Thing state. The possible Config States
// as well as description of what that State represents are given in the table:
// | State | What it means |
// |----------+--------------------------------------------------------------------------------|
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
// | Active | Thing is able to communicate using Mainflux |
type State int
// String returns string representation of State.
func (s State) String() string {
return strconv.Itoa(int(s))
}
-520
View File
@@ -1,520 +0,0 @@
swagger: "2.0"
info:
title: Mainflux Bootstrap service
description: HTTP API for managing platform things configuration.
version: "1.0.0"
consumes:
- "application/json"
produces:
- "application/json"
paths:
/things/configs:
post:
summary: Adds new config
description: |
Adds new config to the list of config owned by user identified using
the provided access token.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- name: config
description: JSON-formatted document describing the new config.
in: body
schema:
$ref: "#/definitions/ConfigReq"
required: true
responses:
201:
description: Config registered.
headers:
Location:
type: string
description: Created configuration's relative URL (i.e. /things/configs/{configId}).
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
get:
summary: Retrieves managed configs
description: |
Retrieves a list of managed configs. Due to performance concerns, data
is retrieved in subsets. The API configs must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
- $ref: "#/parameters/State"
- $ref: "#/parameters/Name"
responses:
200:
description: |
Data retrieved. Configs from this list don't contain channels.
schema:
$ref: "#/definitions/ConfigList"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/things/bootstrap/{externalId}:
get:
summary: Retrieves configuration
description: |
Retrieves a configuration with given external ID and external key.
tags:
- configs
parameters:
- $ref: "#/parameters/ConfigAuth"
- $ref: "#/parameters/ExternalId"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/BootstrapRes"
404:
description: |
Failed to retrieve corresponding config. Thing which attempted
to bootstrap is saved as an unknown Thing and can be listed and
added to the service later.
500:
$ref: "#/responses/ServiceError"
/things/bootstrap/secure/{externalId}:
get:
summary: Retrieves configuration
description: |
Retrieves a configuration with given external ID and encrypted external key.
tags:
- configs
parameters:
- $ref: "#/parameters/EncConfigAuth"
- $ref: "#/parameters/ExternalId"
responses:
200:
description: |
Data retrieved. In this case, Bootstrap response is encrypted using
the secret key, so an actual response is in the binary format.
schema:
$ref: "#/definitions/BootstrapRes"
404:
description: |
Failed to retrieve corresponding config. Thing which attempted
to bootstrap is saved as an unknown Thing and can be listed and
added to the service later.
500:
$ref: "#/responses/ServiceError"
/things/configs/{configId}:
get:
summary: Retrieves config info (with channels)
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/ConfigRes"
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
500:
$ref: "#/responses/ServiceError"
put:
summary: Updates config info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the owner, ID, external ID,
external key, Mainflux Thing ID and key cannot be changed.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: config
description: JSON-formatted document describing the updated thing.
in: body
schema:
$ref: "#/definitions/ConfigUpdateReq"
required: true
responses:
200:
description: Config updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Removes a Config
description: |
Removes a Config. In case of successful removal the service will ensure
that the removed config is disconnected from all of the Mainflux channels.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
responses:
204:
description: Config removed.
400:
description: Failed due to malformed config ID.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/things/configs/certs/{configId}:
patch:
summary: Updates certs
description: |
Update is performed by replacing the current certificate data with values
provided in a request payload.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: config
description: JSON-formatted document describing the updated thing.
in: body
schema:
$ref: "#/definitions/ConfigUpdateCertReq"
required: true
responses:
200:
description: Config updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
/things/configs/connections/{configId}:
put:
summary: Updates channels the thing is connected to
description: |
Update connections performs update of the channel list corresponding
Thing is connected to.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: channels
description: Array if IDs the thing is be connected to.
in: body
schema:
$ref: "#/definitions/ConfigUpdateConnReq"
required: true
responses:
200:
description: Config updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
/things/state/{configId}:
put:
summary: Updates Config state.
description: |
Updating state represents enabling/disabling Config, i.e. connecting
and disconnecting corresponding Mainflux Thing to the list of Channels.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: state
description: New state of the Config.
in: body
schema:
type: object
properties:
state:
type: integer
enum:
- inactive
- active
responses:
204:
description: Config removed.
400:
description: Failed due to malformed config's ID.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/things/unknown/configs:
get:
summary: Get a list of unsuccessfully bootstrapped Things
description: |
Retrieves a list of unknown configs. Due to performance concerns, data
is retrieved in subsets. The API configs must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/ConfigList"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
parameters:
Authorization:
name: Authorization
description: User's access token.
in: header
type: string
required: true
ConfigAuth:
name: configAuthorization
description: Configuration external key.
in: header
type: string
required: true
EncConfigAuth:
name: configAuthorization
description: |
Hex-encoded configuration external key encrypted using
the AES algorithm and SHA256 sum of the external key
itself as an encryption key.
in: header
type: string
required: true
ConfigId:
name: configId
description: Unique Config identifier. It's the ID of the corresponding Thing.
in: path
type: string
required: true
ExternalId:
name: externalId
description: Unique Config identifier provided by external entity.
in: path
type: string
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
type: integer
default: 0
minimum: 0
required: false
State:
name: state
description: A state of items
in: query
type: integer
enum:
- inactive
- active
required: false
Name:
name: name
description: Name of the config. Search by name is partial-match and case-insensitive.
in: query
type: string
required: false
responses:
ServiceError:
description: Unexpected server-side error occurred.
definitions:
ConfigList:
type: object
properties:
total:
type: integer
description: Total number of results.
minimum: 0
offset:
type: integer
description: Number of items to skip during retrieval.
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve.
maximum: 100
default: 10
configs:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/definitions/ConfigRes"
required:
- configs
State:
type: integer
enum:
- active
- inactive
ConfigRes:
type: object
properties:
mainflux_id:
type: string
description: Corresponding Mainflux Thing ID.
mainflux_key:
type: string
description: Corresponding Mainflux Thing key.
mainflux_channels:
type: array
minItems: 0
items:
type: object
properties:
id:
type: string
description: ID of the Channel.
name:
type: string
description: Name of the Channel.
metadata:
type: object
description: Custom metadata related to the Channel.
external_id:
type: string
description: External ID (MAC address or some unique identifier).
external_key:
type: string
description: External key.
content:
type: string
description: Free-form custom configuration.
state:
$ref: '#/definitions/State'
required:
- external_id
- external_key
BootstrapRes:
type: object
properties:
mainflux_id:
type: string
description: Corresponding Mainflux Thing ID.
mainflux_key:
type: string
description: Corresponding Mainflux Thing key.
mainflux_channels:
type: array
minItems: 0
items:
type: string
content:
type: string
description: Free-form custom configuration.
client_cert:
type: string
description: Client certrificate.
client_key:
type: string
description: Key for the client_cert.
ca_cert:
type: string
description: Issuing CA certificate.
required:
- mainflux_id
- mainflux_key
- mainflux_channels
- content
ConfigReq:
type: object
properties:
external_id:
type: string
description: External ID (MAC address or some unique identifier).
external_key:
type: string
description: External key.
thing_id:
type: string
description: ID of the corresponding Mainflux Thing.
channels:
type: array
minItems: 0
items:
type: string
content:
type: string
required:
- external_id
- external_key
ConfigUpdateReq:
type: object
properties:
content:
type: string
name:
type: string
required:
- content
- name
ConfigUpdateConnReq:
type: object
properties:
channels:
type: array
minItems: 0
items:
type: string
ConfigUpdateCertReq:
type: object
properties:
client_cert:
type: string
client_key:
type: string
ca_cert:
type: string
+722
View File
@@ -0,0 +1,722 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Certs Service API
description: |
Certificate management service for issuing, renewing, revoking, and managing X.509 certificates.
This service provides PKI functionality including certificate lifecycle management, OCSP responder,
and CRL generation.
version: 1.0.0
contact:
name: Abstract Machines
license:
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://localhost:9019
description: Development server
tags:
- name: certificates
description: Certificate lifecycle management operations
- name: pki
description: PKI infrastructure operations (OCSP, CRL, CA)
- name: health
description: Service health and monitoring
security:
- BearerAuth: []
paths:
/{domainID}/certs/issue/{entityID}:
post:
tags:
- certificates
summary: Issue a new certificate
description: Issues a new X.509 certificate for the specified entity with custom subject options
operationId: issueCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueCertRequest'
responses:
'201':
description: Certificate successfully issued
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}/renew:
patch:
tags:
- certificates
summary: Renew a certificate
description: Renews an existing certificate with extended TTL and new serial number
operationId: renewCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'200':
description: Certificate successfully renewed
content:
application/json:
schema:
$ref: '#/components/schemas/RenewCertResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}/revoke:
patch:
tags:
- certificates
summary: Revoke a certificate
description: Revokes a certificate by its serial number
operationId: revokeCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'204':
description: Certificate successfully revoked
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{entityID}/delete:
delete:
tags:
- certificates
summary: Delete certificates for an entity
description: Deletes all certificates associated with the specified entity
operationId: deleteCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
responses:
'204':
description: Certificates successfully deleted
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs:
get:
tags:
- certificates
summary: List certificates
description: Retrieves a paginated list of certificates with optional filtering by entity ID
operationId: listCerts
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/EntityIDFilter'
responses:
'200':
description: Certificates successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}:
get:
tags:
- certificates
summary: View certificate details
description: Retrieves detailed information about a specific certificate by serial number
operationId: viewCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'200':
description: Certificate details successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ViewCertResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/csrs/{entityID}:
post:
tags:
- certificates
summary: Issue certificate from CSR
description: Issues a certificate from a Certificate Signing Request (CSR)
operationId: issueFromCSR
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
- $ref: '#/components/parameters/TTL'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRRequest'
responses:
'200':
description: Certificate successfully issued from CSR
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/csrs/{entityID}:
post:
tags:
- certificates
summary: Issue certificate from CSR (Internal)
description: Issues a certificate from a CSR using internal agent authentication
operationId: issueFromCSRInternal
security:
- AgentAuth: []
parameters:
- $ref: '#/components/parameters/EntityID'
- $ref: '#/components/parameters/TTL'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRRequest'
responses:
'200':
description: Certificate successfully issued from CSR
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/ocsp:
post:
tags:
- pki
summary: OCSP responder
description: |
Online Certificate Status Protocol (OCSP) responder endpoint.
Accepts both binary OCSP requests and JSON format requests.
operationId: ocsp
security: []
parameters:
- name: force_status
in: query
description: Force a specific OCSP status for testing
required: false
schema:
type: string
requestBody:
required: true
content:
application/ocsp-request:
schema:
type: string
format: binary
description: DER-encoded OCSP request
application/json:
schema:
$ref: '#/components/schemas/OCSPRequest'
responses:
'200':
description: OCSP response
content:
application/ocsp-response:
schema:
type: string
format: binary
description: DER-encoded OCSP response
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/crl:
get:
tags:
- pki
summary: Generate Certificate Revocation List
description: Generates and returns the current Certificate Revocation List (CRL)
operationId: generateCRL
security: []
responses:
'200':
description: CRL successfully generated
content:
application/json:
schema:
$ref: '#/components/schemas/CRLResponse'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/view-ca:
get:
tags:
- pki
summary: View CA certificate
description: Retrieves the CA certificate chain (root and intermediate certificates)
operationId: viewCA
security: []
responses:
'200':
description: CA certificate successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ViewCertResponse'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/download-ca:
get:
tags:
- pki
summary: Download CA certificate
description: Downloads the CA certificate as a ZIP file
operationId: downloadCA
security: []
responses:
'200':
description: CA certificate ZIP file
content:
application/zip:
schema:
type: string
format: binary
'500':
$ref: '#/components/responses/InternalServerError'
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
'200':
$ref: '#/components/responses/HealthRes'
'500':
$ref: '#/components/responses/InternalServerError'
/metrics:
get:
tags:
- health
summary: Prometheus metrics
description: Returns Prometheus metrics for monitoring
operationId: metrics
security: []
responses:
'200':
description: Metrics successfully retrieved
content:
text/plain:
schema:
type: string
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: User authentication token
AgentAuth:
type: http
scheme: bearer
description: Agent authentication token for internal operations
parameters:
DomainID:
name: domainID
in: path
required: true
description: Domain identifier
schema:
type: string
EntityID:
name: entityID
in: path
required: true
description: Entity identifier for the certificate
schema:
type: string
CertID:
name: id
in: path
required: true
description: Certificate serial number
schema:
type: string
Offset:
name: offset
in: query
description: Number of items to skip
schema:
type: integer
minimum: 0
default: 0
Limit:
name: limit
in: query
description: Maximum number of items to return
schema:
type: integer
minimum: 1
maximum: 100
default: 10
EntityIDFilter:
name: entity_id
in: query
description: Filter certificates by entity ID
schema:
type: string
TTL:
name: ttl
in: query
description: Time to live for the certificate (e.g., "8760h", "365d")
schema:
type: string
schemas:
IssueCertRequest:
type: object
required:
- options
properties:
ttl:
type: string
description: Time to live for the certificate (e.g., "8760h" for 1 year)
example: "8760h"
ip_addresses:
type: array
items:
type: string
description: IP addresses to include in the certificate
example: ["192.168.1.1", "10.0.0.1"]
options:
$ref: '#/components/schemas/SubjectOptions'
SubjectOptions:
type: object
required:
- common_name
properties:
common_name:
type: string
description: Common Name (CN) for the certificate subject
example: "example.com"
organization:
type: array
items:
type: string
description: Organization (O)
example: ["Abstract Machines"]
organizational_unit:
type: array
items:
type: string
description: Organizational Unit (OU)
example: ["Engineering"]
country:
type: array
items:
type: string
description: Country (C)
example: ["US"]
province:
type: array
items:
type: string
description: Province or State (ST)
example: ["California"]
locality:
type: array
items:
type: string
description: Locality or City (L)
example: ["San Francisco"]
street_address:
type: array
items:
type: string
description: Street Address
example: ["123 Main St"]
postal_code:
type: array
items:
type: string
description: Postal Code
example: ["94105"]
dns_names:
type: array
items:
type: string
description: DNS names for Subject Alternative Names
example: ["example.com", "www.example.com"]
ip_addresses:
type: array
items:
type: string
description: IP addresses for Subject Alternative Names
example: ["192.168.1.1"]
CertificateResponse:
type: object
properties:
serial_number:
type: string
description: Unique serial number of the certificate
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
key:
type: string
description: PEM-encoded private key
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
revoked:
type: boolean
description: Whether the certificate is revoked
example: false
expiry_time:
type: string
format: date-time
description: Certificate expiration time
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Entity identifier associated with the certificate
example: "entity-123"
RenewCertResponse:
type: object
properties:
certificate:
$ref: '#/components/schemas/ViewCertResponse'
ViewCertResponse:
type: object
properties:
serial_number:
type: string
description: Certificate serial number
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
key:
type: string
description: PEM-encoded private key
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
revoked:
type: boolean
description: Revocation status
example: false
expiry_time:
type: string
format: date-time
description: Expiration timestamp
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Associated entity identifier
example: "entity-123"
CertificateListResponse:
type: object
properties:
total:
type: integer
format: uint64
description: Total number of certificates
example: 100
offset:
type: integer
format: uint64
description: Current offset
example: 0
limit:
type: integer
format: uint64
description: Current limit
example: 10
certificates:
type: array
items:
$ref: '#/components/schemas/ViewCertResponse'
IssueFromCSRRequest:
type: object
required:
- csr
properties:
csr:
type: string
format: byte
description: PEM-encoded Certificate Signing Request
example: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K..."
IssueFromCSRResponse:
type: object
properties:
serial_number:
type: string
description: Serial number of the issued certificate
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
revoked:
type: boolean
description: Revocation status
example: false
expiry_time:
type: string
format: date-time
description: Expiration timestamp
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Associated entity identifier
example: "entity-123"
OCSPRequest:
type: object
properties:
serial_number:
type: string
description: Certificate serial number to check
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate to check
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
status:
type: string
description: Force a specific status (for testing)
enum: [good, revoked, unknown]
CRLResponse:
type: object
properties:
crl:
type: string
format: byte
description: DER-encoded Certificate Revocation List
Error:
type: object
properties:
error:
type: string
description: Error message
example: "invalid request"
responses:
BadRequest:
description: Bad request - invalid parameters or malformed request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized - invalid or missing authentication token
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
UnprocessableEntity:
description: Unprocessable entity - request cannot be processed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: './schemas/health_info.yaml'
+1149
View File
File diff suppressed because it is too large Load Diff
-204
View File
@@ -1,204 +0,0 @@
# Mainflux CLI
## Build
From the project root:
```bash
make cli
```
## Usage
### Service
#### Get the version of Mainflux services
```bash
mainflux-cli version
```
### Users management
#### Create User
```bash
mainflux-cli users create <user_email> <user_password>
```
#### Login User
```bash
mainflux-cli users token <user_email> <user_password>
```
#### Retrieve User
```bash
mainflux-cli users get <user_auth_token>
```
#### Update User Metadata
```bash
mainflux-cli users update '{"key1":"value1", "key2":"value2"}' <user_auth_token>
```
#### Update User Password
```bash
mainflux-cli users password <old_password> <password> <user_auth_token>
```
### System Provisioning
#### Create Thing
```bash
mainflux-cli things create '{"name":"myThing"}' <user_auth_token>
```
#### Create Thing with metadata
```bash
mainflux-cli things create '{"name":"myThing", "metadata": {\"key1\":\"value1\"}}' <user_auth_token>
```
#### Bulk Provision Things
```bash
mainflux-cli provision things <file> <user_auth_token>
```
* `file` - A CSV or JSON file containing things
* `user_auth_token` - A valid user auth token for the current system
#### Update Thing
```bash
mainflux-cli things update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
```
#### Remove Thing
```bash
mainflux-cli things delete <thing_id> <user_auth_token>
```
#### Retrieve a subset list of provisioned Things
```bash
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Thing By ID
```bash
mainflux-cli things get <thing_id> <user_auth_token>
```
#### Create Channel
```bash
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
```
#### Bulk Provision Channels
```bash
mainflux-cli provision channels <file> <user_auth_token>
```
* `file` - A CSV or JSON file containing channels
* `user_auth_token` - A valid user auth token for the current system
#### Update Channel
```bash
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
```
#### Remove Channel
```bash
mainflux-cli channels delete <channel_id> <user_auth_token>
```
#### Retrieve a subset list of provisioned Channels
```bash
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Channel By ID
```bash
mainflux-cli channels get <channel_id> <user_auth_token>
```
### Access control
#### Connect Thing to Channel
```bash
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
```
#### Bulk Connect Things to Channels
```bash
mainflux-cli provision connect <file> <user_auth_token>
```
* `file` - A CSV or JSON file containing thing and channel ids
* `user_auth_token` - A valid user auth token for the current system
An example CSV file might be
```csv
<thing_id>,<channel_id>
<thing_id>,<channel_id>
```
in which the first column is thing IDs and the second column is channel IDs. A connection will be created for each thing to each channel. This example would result in 4 connections being created.
A comparable JSON file would be
```json
{
"thing_ids": [
"<thing_id>",
"<thing_id>"
],
"channel_ids": [
"<channel_id>",
"<channel_id>"
]
}
```
#### Disconnect Thing from Channel
```bash
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
```
#### Retrieve a subset list of Channels connected to Thing
```bash
mainflux-cli things connections <thing_id> <user_auth_token>
```
#### Retrieve a subset list of Things connected to Channel
```bash
mainflux-cli channels connections <channel_id> <user_auth_token>
```
### Messaging
#### Send a message over HTTP
```bash
mainflux-cli messages send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"hum","v":40}, {"bn":"Dev2", "n":"temp","v":20}, {"n":"hum","v":40}]' <thing_auth_token>
```
#### Read messages over HTTP
```bash
mainflux-cli messages read <channel_id> <thing_auth_token>
```
### Bootstrap
#### Add configuration
```bash
mainflux-cli bootstrap add '{"external_id": "myExtID", "external_key": "myExtKey", "name": "myName", "content": "myContent"}' <user_auth_token>
```
#### View configuration
```bash
mainflux-cli bootstrap view <thing_id> <user_auth_token>
```
#### Update configuration
```bash
mainflux-cli bootstrap update '{"MFThing":"<thing_id>", "name": "newName", "content": "newContent"}' <user_auth_token>
```
#### Remove configuration
```bash
mainflux-cli bootstrap remove <thing_id> <user_auth_token>
```
#### Bootstrap configuration
```bash
mainflux-cli bootstrap bootstrap <external_id> <external_key>
```
-137
View File
@@ -1,137 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var cmdBootstrap = []cobra.Command{
cobra.Command{
Use: "add",
Short: "add <JSON_config> <user_auth_token>",
Long: `Adds new Thing Bootstrap Config to the user identified by the provided key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var cfg mfxsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logError(err)
return
}
id, err := sdk.AddBootstrap(args[1], cfg)
if err != nil {
logError(err)
return
}
logCreated(id)
},
},
cobra.Command{
Use: "view",
Short: "view <thing_id> <user_auth_token>",
Long: `Returns Thing Config with given ID belonging to the user identified by the given key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
c, err := sdk.ViewBootstrap(args[1], args[0])
if err != nil {
logError(err)
return
}
logJSON(c)
},
},
cobra.Command{
Use: "update",
Short: "update <JSON_config> <user_auth_token>",
Long: `Updates editable fields of the provided Config`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var cfg mfxsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logError(err)
return
}
if err := sdk.UpdateBootstrap(args[1], cfg); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "remove",
Short: "remove <thing_id> <user_auth_token>",
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if err := sdk.RemoveBootstrap(args[1], args[0]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "bootstrap",
Short: "bootstrap <external_id> <external_key>",
Long: `Returns Config to the Thing with provided external ID using external key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
c, err := sdk.Bootstrap(args[1], args[0])
if err != nil {
logError(err)
return
}
logJSON(c)
},
},
}
// NewBootstrapCmd returns bootstrap command.
func NewBootstrapCmd() *cobra.Command {
cmd := cobra.Command{
Use: "bootstrap",
Short: "Bootstrap management",
Long: `Bootstrap management: create, get, update or delete Bootstrap config`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("bootstrap [add | view | update | remove | bootstrap]")
},
}
for i := range cmdBootstrap {
cmd.AddCommand(&cmdBootstrap[i])
}
return &cmd
}
-148
View File
@@ -1,148 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var cmdChannels = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <JSON_channel> <user_auth_token>",
Long: `Creates new channel and generates it's UUID`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var channel mfxsdk.Channel
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
logError(err)
return
}
id, err := sdk.CreateChannel(channel, args[1])
if err != nil {
logError(err)
return
}
logCreated(id)
},
},
cobra.Command{
Use: "get",
Short: "get [all | <channel_id>] <user_auth_token>",
Long: `Gets list of all channels or gets channel by id`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if args[0] == "all" {
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit), Name)
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
c, err := sdk.Channel(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(c)
},
},
cobra.Command{
Use: "update",
Short: "update <JSON_string> <user_auth_token>",
Long: `Updates channel record`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var channel mfxsdk.Channel
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
logError(err)
return
}
if err := sdk.UpdateChannel(channel, args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "delete",
Short: "delete <channel_id> <user_auth_token>",
Long: `Delete channel by ID`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if err := sdk.DeleteChannel(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "connections",
Short: "connections <channel_id> <user_auth_token>",
Long: `List of Things connected to Channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(cl)
},
},
}
// NewChannelsCmd returns channels command.
func NewChannelsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "channels",
Short: "Channels management",
Long: `Channels management: create, get, update or delete Channel and get list of Things connected to Channel`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("channels [create | get | update | delete | connections]")
},
}
for i := range cmdChannels {
cmd.AddCommand(&cmdChannels[i])
}
return &cmd
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import "github.com/spf13/cobra"
const contentTypeSenml = "application/senml+json"
var cmdMessages = []cobra.Command{
cobra.Command{
Use: "send",
Short: "send <channel_id>[.<subtopic>...] <JSON_string> <thing_key>",
Long: `Sends message on the channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.SendMessage(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "read",
Short: "read <channel_id>[.<subtopic>...] <thing_key>",
Long: `Reads all channel messages`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
m, err := sdk.ReadMessages(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(m)
},
},
}
// NewMessagesCmd returns messages command.
func NewMessagesCmd() *cobra.Command {
cmd := cobra.Command{
Use: "messages",
Short: "Send or read messages",
Long: `Send or read messages using the http-adapter and the configured database reader`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("messages [send | read]")
},
}
for i := range cmdMessages {
cmd.AddCommand(&cmdMessages[i])
}
return &cmd
}
-345
View File
@@ -1,345 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/docker/docker/pkg/namesgenerator"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var errMalformedCSV = errors.New("malformed CSV")
const jsonExt = ".json"
const csvExt = ".csv"
var cmdProvision = []cobra.Command{
cobra.Command{
Use: "things",
Short: "things <things_file> <user_token>",
Long: `Bulk create things`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
logError(err)
return
}
things, err := thingsFromFile(args[0])
if err != nil {
logError(err)
return
}
things, err = sdk.CreateThings(things, args[1])
if err != nil {
logError(err)
return
}
logJSON(things)
},
},
cobra.Command{
Use: "channels",
Short: "channels <channels_file> <user_token>",
Long: `Bulk create channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
channels, err := channelsFromFile(args[0])
if err != nil {
logError(err)
return
}
channels, err = sdk.CreateChannels(channels, args[1])
if err != nil {
logError(err)
return
}
logJSON(channels)
},
},
cobra.Command{
Use: "connect",
Short: "connect <connections_file> <user_token>",
Long: `Bulk connect things to channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
connIDs, err := connectionsFromFile(args[0])
if err != nil {
logError(err)
return
}
err = sdk.Connect(connIDs, args[1])
if err != nil {
logError(err)
return
}
},
},
cobra.Command{
Use: "test",
Short: "test",
Long: `Provisions test setup: one test user, two things and two channels. \
Connect both things to one of the channels, \
and only on thing to other channel.`,
Run: func(cmd *cobra.Command, args []string) {
numThings := 2
numChan := 2
things := []mfxsdk.Thing{}
channels := []mfxsdk.Channel{}
if len(args) != 0 {
logUsage(cmd.Short)
return
}
un := fmt.Sprintf("%s@email.com", namesgenerator.GetRandomName(0))
// Create test user
user := mfxsdk.User{
Email: un,
Password: "12345678",
}
if err := sdk.CreateUser(user); err != nil {
logError(err)
return
}
ut, err := sdk.CreateToken(user)
if err != nil {
logError(err)
return
}
// Create things
for i := 0; i < numThings; i++ {
n := fmt.Sprintf("d%d", i)
t := mfxsdk.Thing{
Name: n,
}
things = append(things, t)
}
things, err = sdk.CreateThings(things, ut)
if err != nil {
logError(err)
return
}
// Create channels
for i := 0; i < numChan; i++ {
n := fmt.Sprintf("c%d", i)
c := mfxsdk.Channel{
Name: n,
}
channels = append(channels, c)
}
channels, err = sdk.CreateChannels(channels, ut)
if err != nil {
logError(err)
return
}
// Connect things to channels - first thing to both channels, second only to first
conIDs := mfxsdk.ConnectionIDs{
ChannelIDs: []string{channels[0].ID, channels[1].ID},
ThingIDs: []string{things[0].ID},
}
if err := sdk.Connect(conIDs, ut); err != nil {
logError(err)
return
}
conIDs = mfxsdk.ConnectionIDs{
ChannelIDs: []string{channels[0].ID},
ThingIDs: []string{things[1].ID},
}
if err := sdk.Connect(conIDs, ut); err != nil {
logError(err)
return
}
logJSON(user, ut, things, channels)
},
},
}
// NewProvisionCmd returns provision command.
func NewProvisionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "provision",
Short: "Provision things and channels from a config file",
Long: `Provision things and channels: use json or csv file to bulk provision things and channels`,
}
for i := range cmdProvision {
cmd.AddCommand(&cmdProvision[i])
}
return &cmd
}
func thingsFromFile(path string) ([]mfxsdk.Thing, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []mfxsdk.Thing{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []mfxsdk.Thing{}, err
}
defer file.Close()
things := []mfxsdk.Thing{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []mfxsdk.Thing{}, err
}
if len(l) < 1 {
return []mfxsdk.Thing{}, errors.New("empty line found in file")
}
thing := mfxsdk.Thing{
Name: l[0],
}
things = append(things, thing)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&things)
if err != nil {
return []mfxsdk.Thing{}, err
}
default:
return []mfxsdk.Thing{}, err
}
return things, nil
}
func channelsFromFile(path string) ([]mfxsdk.Channel, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []mfxsdk.Channel{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []mfxsdk.Channel{}, err
}
defer file.Close()
channels := []mfxsdk.Channel{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []mfxsdk.Channel{}, err
}
if len(l) < 1 {
return []mfxsdk.Channel{}, errors.New("empty line found in file")
}
channel := mfxsdk.Channel{
Name: l[0],
}
channels = append(channels, channel)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&channels)
if err != nil {
return []mfxsdk.Channel{}, err
}
default:
return []mfxsdk.Channel{}, err
}
return channels, nil
}
func connectionsFromFile(path string) (mfxsdk.ConnectionIDs, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return mfxsdk.ConnectionIDs{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return mfxsdk.ConnectionIDs{}, err
}
defer file.Close()
connections := mfxsdk.ConnectionIDs{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return mfxsdk.ConnectionIDs{}, err
}
if len(l) < 1 {
return mfxsdk.ConnectionIDs{}, errors.New("empty line found in file")
}
connections.ThingIDs = append(connections.ThingIDs, l[0])
connections.ChannelIDs = append(connections.ChannelIDs, l[1])
}
case jsonExt:
err := json.NewDecoder(file).Decode(&connections)
if err != nil {
return mfxsdk.ConnectionIDs{}, err
}
default:
return mfxsdk.ConnectionIDs{}, err
}
return connections, nil
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import mfxsdk "github.com/mainflux/mainflux/sdk/go"
// Keep SDK handle in global var
var sdk mfxsdk.SDK
// SetSDK sets mainflux SDK instance.
func SetSDK(s mfxsdk.SDK) {
sdk = s
}
-187
View File
@@ -1,187 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var cmdThings = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <JSON_thing> <user_auth_token>",
Long: `Create new thing, generate his UUID and store it`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var thing mfxsdk.Thing
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
logError(err)
return
}
id, err := sdk.CreateThing(thing, args[1])
if err != nil {
logError(err)
return
}
logCreated(id)
},
},
cobra.Command{
Use: "get",
Short: "get [all | <thing_id>] <user_auth_token>",
Long: `Get all things or thing by id`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if args[0] == "all" {
l, err := sdk.Things(args[1], uint64(Offset), uint64(Limit), Name)
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
t, err := sdk.Thing(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(t)
},
},
cobra.Command{
Use: "delete",
Short: "delete <thing_id> <user_auth_token>",
Long: `Removes thing from database`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if err := sdk.DeleteThing(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "update",
Short: "update <JSON_string> <user_auth_token>",
Long: `Update thing record`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var thing mfxsdk.Thing
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
logError(err)
return
}
if err := sdk.UpdateThing(thing, args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "connect",
Short: "connect <thing_id> <channel_id> <user_auth_token>",
Long: `Connect thing to the channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
connIDs := mfxsdk.ConnectionIDs{
ChannelIDs: []string{args[1]},
ThingIDs: []string{args[0]},
}
if err := sdk.Connect(connIDs, args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "disconnect",
Short: "disconnect <thing_id> <channel_id> <user_auth_token>",
Long: `Disconnect thing to the channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.DisconnectThing(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "connections",
Short: "connections <thing_id> <user_auth_token>",
Long: `List of Channels connected to Thing`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(cl)
},
},
}
// NewThingsCmd returns things command.
func NewThingsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "things",
Short: "Things management",
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected to Thing`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("things [create | get | update | delete | connect | disconnect | connections]")
},
}
for i := range cmdThings {
cmd.AddCommand(&cmdThings[i])
}
return &cmd
}
-138
View File
@@ -1,138 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var cmdUsers = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <username> <password>",
Long: `Creates new user`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
user := mfxsdk.User{
Email: args[0],
Password: args[1],
}
if err := sdk.CreateUser(user); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "get",
Short: "get <user_auth_token>",
Long: `Returns user object`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
logUsage(cmd.Short)
return
}
u, err := sdk.User(args[0])
if err != nil {
logError(err)
return
}
logJSON(u)
},
},
cobra.Command{
Use: "token",
Short: "token <username> <password>",
Long: `Creates new token`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
user := mfxsdk.User{
Email: args[0],
Password: args[1],
}
token, err := sdk.CreateToken(user)
if err != nil {
logError(err)
return
}
logCreated(token)
},
},
cobra.Command{
Use: "update",
Short: "update <JSON_string> <user_auth_token>",
Long: `Update user metadata`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var user mfxsdk.User
if err := json.Unmarshal([]byte(args[0]), &user.Metadata); err != nil {
logError(err)
return
}
if err := sdk.UpdateUser(user, args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "password",
Short: "password <old_password> <password> <user_auth_token>",
Long: `Update user password`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.UpdatePassword(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
}
// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
cmd := cobra.Command{
Use: "users",
Short: "Users management",
Long: `Users management: create accounts and tokens"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("Usage: users [create | get | update | token | password]")
},
}
for i := range cmdUsers {
cmd.AddCommand(&cmdUsers[i])
}
return &cmd
}

Some files were not shown because too many files have changed in this diff Show More