mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92d8fb99bf | |||
| ad80cf868b | |||
| f0dac5c6ac | |||
| 48d6a95a21 | |||
| c6f7c69798 | |||
| ce46723a8f | |||
| 29f9cbb10a | |||
| 127044efad | |||
| f3ed852b36 | |||
| 0a6b2f135a | |||
| e5278c463f | |||
| cc5d5195ab | |||
| 5ca8495f35 | |||
| 9972d1d1a4 | |||
| 8b94199785 | |||
| d51a79c538 | |||
| 09cbc3f14b | |||
| 655ac838be | |||
| f8ce94e9bb | |||
| 1f8a221c22 | |||
| 9e0947a355 | |||
| e8a51842aa | |||
| 6ad654d7cb | |||
| 4f56986c8e | |||
| 81b2a1a2d3 | |||
| 89061d33e5 | |||
| 2abf9da27e | |||
| 42dd813521 | |||
| bcc8cf7119 | |||
| d86a4dee92 | |||
| 864ad144e6 | |||
| bea09d97dd | |||
| 6483969927 | |||
| aa014c2191 | |||
| dd7d52ef10 | |||
| ad8b7ddf5a | |||
| b18c9e79dc | |||
| 7e9ab453d4 | |||
| 309ef512cb | |||
| db6fab961e | |||
| f99f5d228d | |||
| 5112ef681a | |||
| 31d30b204d | |||
| be3e98f677 | |||
| 87510288c2 | |||
| b78928c998 | |||
| 39133b06a4 | |||
| 3042d6b40b | |||
| c70fb576b6 | |||
| 9f5a319519 | |||
| b570c38ed0 | |||
| dc935858bd | |||
| 90e4561491 | |||
| 30912e5a45 | |||
| 27d4646db4 | |||
| 042ff98509 | |||
| 5e9a91bd03 | |||
| 39ae7bdfa1 | |||
| 94cba4aab2 | |||
| 66d3da0531 | |||
| f4312aef8a | |||
| caa4bda1a1 | |||
| e1a66b3268 | |||
| 7b3c26f60f | |||
| d73a5d53fe | |||
| d6a3830ef4 | |||
| 5ac1203b55 | |||
| 19f0437f57 | |||
| 2cfff01979 | |||
| bb072b8ad2 | |||
| af0162f0df | |||
| 68af0e32b5 | |||
| cddfdf4038 | |||
| 0a89f1dae1 | |||
| d3e34b1662 | |||
| 9e5e50b347 | |||
| 516c02bebe | |||
| 38ca7f761b | |||
| 9ce31b65a6 | |||
| 79af1ba9a8 | |||
| e04d94ecc7 | |||
| e02e9c2387 | |||
| 063f73076d | |||
| 84978c034b | |||
| 0fe9f55bae | |||
| 00dc197fa6 | |||
| cd11728073 | |||
| 6770c19279 | |||
| 4efd25eed5 | |||
| 6b6f18317f | |||
| 0cdcf28683 | |||
| f9f51470b1 | |||
| e87715ba31 | |||
| 9245e2593b | |||
| bcdc6d21f6 | |||
| 0f3a262ca3 | |||
| 74aa93fbb6 | |||
| 30ba38c919 | |||
| a1e18a770a | |||
| 259950b009 | |||
| 530f925c4d | |||
| 6b1f4d54f8 | |||
| 7bcaa323d4 | |||
| e334569d81 | |||
| 0432b2aa03 | |||
| 56d04cda64 | |||
| e01874e557 | |||
| 9e532d2a72 | |||
| 7ba3fdb3bf | |||
| 7834cc48b3 | |||
| f0f60e2d2a | |||
| 24b902d049 | |||
| 13c426c09c | |||
| ca5ff63dfb | |||
| 8b2ae46324 | |||
| 241ccf8fb4 | |||
| 39a649c1bb | |||
| a3dbfecb2d | |||
| 73c175020e | |||
| 1bf485b71b | |||
| 8c87ee8328 | |||
| 0631900d5c | |||
| 4619576e94 | |||
| 31f5bf714a | |||
| bf5ede086a | |||
| 5cfc9305e5 | |||
| 85f73a87b6 | |||
| 0f856f5667 | |||
| 0516fe2fd7 | |||
| a8c652f96c | |||
| 8e5a9cfc9a | |||
| 6b7dc54c8b | |||
| 973ca177ea | |||
| a185855c06 | |||
| ac09815457 | |||
| 3eca6920e1 | |||
| e326494166 | |||
| 47217cb5b9 | |||
| b2ccbaec27 | |||
| cb9985d160 | |||
| 1810cec82b | |||
| d2af0602a0 | |||
| 8aadc0ff40 | |||
| c7d2feb434 | |||
| 18dd8967cc | |||
| 9864b27271 | |||
| 3653e6b5d2 | |||
| fbba7aaa1a | |||
| 20f5290d7a | |||
| 23bc822433 | |||
| 7195cad0f6 | |||
| 663bd4e18d | |||
| c03644524e | |||
| f1aa32d89c | |||
| 3273c30d8b | |||
| 46c675cd5f | |||
| 02db4066b1 | |||
| f6b1ae735c | |||
| 926e9799d6 | |||
| 1c298d8f27 | |||
| 420b598ac7 | |||
| 86fe3f7cdb |
@@ -28,3 +28,13 @@ the following locations:
|
||||
5. In what environment did you encounter the issue?
|
||||
|
||||
6. Additional information you deem important:
|
||||
|
||||
**ENHANCEMENT**
|
||||
1. Describe the enhancement you are requesting. Enhancements include:
|
||||
- tests
|
||||
- code refactor
|
||||
- documentation
|
||||
- research
|
||||
- tooling
|
||||
|
||||
2. Indicate the importance of this enhancement to you (must-have, should-have, nice-to-have).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Swagger ui action
|
||||
id: swagger-ui-action
|
||||
uses: blokovi/swagger-ui-action@main
|
||||
with:
|
||||
dir: './api/openapi'
|
||||
pattern: '*.yml'
|
||||
debug: 'true'
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: api.mainflux.io
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Adopters
|
||||
|
||||
As Mainflux Community grows, we'd like to keep track of Mainflux adopters to grow the community, contact other users, share experiences and best practices.
|
||||
|
||||
To accomplish this, we created a public ledger. The list of organizations and users who consider themselves as Mainflux adopters and that **publicly/officially** shared information and/or details of their adoption journey(optional).
|
||||
Where users themselves directly maintain the list.
|
||||
|
||||
## Adding yourself as an adopter
|
||||
If you are using Mainflux, please consider adding yourself as an adopter with a brief description of your use case by opening a pull request to this file and adding a section describing your adoption of Mainflux technology.
|
||||
|
||||
**Please send PRs to add or remove organizations/users**
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
N: Name of user (company or individual)
|
||||
D: Short Use Case Description (optional)
|
||||
L: Link with further information (optional)
|
||||
T: Type of adaptation: Evaluation, Core Technology, Production Usage (optional)
|
||||
```
|
||||
|
||||
## Requirements
|
||||
* You must represent the user or organization listed. Do NOT add entries on behalf of other organizations or individuals.
|
||||
Pull request commit must be [signed](https://docs.github.com/en/github/authenticating-to-github/signing-commits) and auto-checked with [ Developer Certificate of Origin (DCO)](https://probot.github.io/apps/dco/)
|
||||
* There is no minimum requirement or adaptation size, but we request to list permanent deployments only, i.e., no demo or trial deployments. Commercial or production use is not required. A well-done home lab setup can be equally impressive as a large-scale commercial deployment.
|
||||
|
||||
|
||||
**The list of organizations/users that have publicly shared the usage of Mainflux:**
|
||||
|
||||
**Note**: Several other organizations/users couldn't publicly share their usage details but are active project contributors and Mainflux Community members.
|
||||
|
||||
|
||||
## Adopters list (alphabetical)
|
||||
|
||||
|
||||
**Note:** The list is maintained by the users themselves. If you find yourself on this list, and you think it's inappropriate. Please contact [project maintainers](https://github.com/mainflux/mainflux/blob/master/MAINTAINERS) and you will be permanently removed from the list.
|
||||
+206
@@ -11,6 +11,212 @@ Otherwise, whole log in a similar format can be observed via:
|
||||
git log --pretty=oneline --abbrev-commit
|
||||
```
|
||||
|
||||
## 0.13.0 - 15. APR 2022.
|
||||
### Features and Bugfixes
|
||||
- NOISSUE - Update changelog for release 0.13.0
|
||||
- Update VerneMQ release (#1593)
|
||||
- NOISSUE - Update changelog and readme for release 0.13.0
|
||||
- MF-1582 - Fix lora-adapter MQTT client (#1583)
|
||||
- NOISSUE - Fix CoAP adapter (#1572)
|
||||
- NOISSUE - Unify MF_INFLUX_READER_DB_HOST and MF_INFLUX_WRITER_DB_HOST envars (#1585)
|
||||
- MF-1580 - Influxdb Writer changes format of update-time to string (#1581)
|
||||
- MF-1575 Add 'Name' field to ListMembers response in things svc (#1576)
|
||||
- MF-1565 - Document Bearer, Thing and Basic Authorization header (#1566)
|
||||
- MF-1567 - Use Bearer, Thing or Basic scheme in Authorization header (#1568)
|
||||
- MF-1348 - Add transport errors logging (#1544)
|
||||
- NOISSUE - Add nats wrapper for COAP (#1569)
|
||||
- MF-1469 - Indicate proper authentication scheme in Authorization header (#1523)
|
||||
- MF-1240 - Return to service transport layer only service errors (#1559)
|
||||
- Update dependencies (#1564)
|
||||
- NOISSUE - Separate Keto hosts for read and write (#1563)
|
||||
- MF-1551 - Fix Cobra usage commands and clean unnecessary struct types (#1558)
|
||||
- MF-1257 - Access messages from readers endpoint with user access token (#1470)
|
||||
- NOISSUE - Refactor MQTT subscriber (#1561)
|
||||
- MF-1059 - Add TLS support for email (#1560)
|
||||
- MF-1261 - Use StatusUnauthorized for authn and StatusForbidden for authz (#1538)
|
||||
- NOISSUE - Fix auth members list response (#1555)
|
||||
- MF-1263 - Move repeating errors to the separate package (#1540)
|
||||
- NOISSUE - Add API keys functions to CLI (#1537)
|
||||
- Fix SDK for group members (#1553)
|
||||
- NOISSUE - Fix Swagger UI (#1552)
|
||||
- MF-1008 - Make token duration configurable (#1550)
|
||||
- MF-1308 - Use IETF Health Check standard (#1541)
|
||||
- Fix user listing access control (#1546)
|
||||
- Update dependencies (#1545)
|
||||
- MF-1478 - TimescaleDB writer and reader add-on (#1542)
|
||||
- MF-1149 - Add AsyncAPI MQTT API doc (#1539)
|
||||
- MF-1535 - Add API keys functions to SDK (#1536)
|
||||
- NOISSUE - Add view and list serials endpoints in certs service (#1483)
|
||||
- MF-1516 - Fix API key issuing (#1530)
|
||||
- NOISSUE - Add disconnect endpoint in nginx conf (#1528)
|
||||
- NOISSUE - Add timestamp transformation rules for specifc JSON fields (#1514)
|
||||
- MF-1425 - Support external UUIDs for Things and Channels (#1518)
|
||||
- MF-1521 - Fix email headers (#1522)
|
||||
- Fix SenML lib dependency version (#1519)
|
||||
- Bump vernemq to 1.12.3 (#1520)
|
||||
- NOISSUE - Remove auth URL from SDK (#1511)
|
||||
- NOISSUE - Apply policies to Channels (#1505)
|
||||
- remove dead code (#1503)
|
||||
- NOISSUE - Fix listing (#1502)
|
||||
- NOISSUE - Listing Policies (#1498)
|
||||
- Fix standalone mode (#1497)
|
||||
- MF-1489 - Add API for deleting policies (#1491)
|
||||
- NOISSUE - Update group sharing policies (#1494)
|
||||
- NOISSUE - Refactor InfluxDB Reader: explicit check event + add safe conversion (#1460)
|
||||
- NOISSUE - Update users create command for CLI (#1495)
|
||||
- NOISSUE - Update self register environment variable name (#1493)
|
||||
- Bring back the job add
|
||||
- NOISSUE - Fix assigning invalid group policy (#1487)
|
||||
- MF-1443 - Add policies (#1482)
|
||||
- NOISSUE - Fix retrieving all users (#1477)
|
||||
- MF-1468 - Fix ThingsURL in Certs Service (#1474)
|
||||
- NOISSUE - Refactor single-user mode (#1471)
|
||||
- Fix UpdateChannelHandler for Redis producer (#1473)
|
||||
- NOISSUE - Add SMPP notifier (#1464)
|
||||
- NOISSUE - Update dependencies (#1453)
|
||||
- NOISSUE - Fix security warnings for dependencies (#1452)
|
||||
- Bump docker-compose version in prereq (#1449)
|
||||
- NOISSUE - Fix bootstraping (#1448)
|
||||
- MF 1413 - Use per-service URL in SDK (#1444)
|
||||
- MF-1439 - Add support for Basic Authentication in HTTP Adapter (#1441)
|
||||
- MF-1421 - Make flattening of JSON transformer only available on InfluxDB (#1432)
|
||||
- NOISSUE - Update the /disconnect endpoint HTTP method as PUT (#1438)
|
||||
- MF-1389 - Add /disconnect endpoint in Things service (#1433)
|
||||
- NOISSUE - Fix httputil implementation in users service (#1434)
|
||||
- Fix fetching user members of an empty group (#1436)
|
||||
- Change to user friendly docs urls (#1430)
|
||||
- NOISSUE - Use github action for showing OpenAPI spec with Swagger UI (#1427)
|
||||
- Fix JSON Transformer empty format handling (#1429)
|
||||
- Update README
|
||||
- NOISSUE - Update docker-compose images to latest release (#1419)
|
||||
- MF-1378 - Update dependencies (#1379)
|
||||
|
||||
## 0.12.1 - 05. MAY 2021.
|
||||
### Features and Bugfixes
|
||||
- NOISSUE - Refactor SDK memberships and fix openapi for memberships.
|
||||
- NOISSUE - Fix incorrect influxdb credentials
|
||||
- MF-1408 - Fix error handling for Thing update SQL(#1408)
|
||||
- MF-1288 - Add tests for JSON messages in message writers and readers
|
||||
- NOISSUE - Fix Postgres Reader order
|
||||
- NOISSUE - Fix nginx configuration for groups
|
||||
- NOISSUE - Add tests and connection route-map to lora-adapter
|
||||
- MF-1403 - Change vernemq building source revision
|
||||
- NOISSUE - Rm content-type check from list endpoint
|
||||
|
||||
## 0.12.0 - 29. MAR 2021.
|
||||
### Features and Bugfixes
|
||||
- MF-1394 - SDK groups (#1396)
|
||||
- NOISSUE - fix response for passwd endpoints (#1393)
|
||||
- NOISSUE - dont retrieve groups (#1392)
|
||||
- MF-1368 - Add internal http api package for query params reading (#1384)
|
||||
- MF-1390 - Fix docker-compose env_file (#1391)
|
||||
- NOISSUE - put order direction in response body (#1387)
|
||||
- NOISSUE - Certs service refactor (#1369)
|
||||
- MF-1357 - Add new endpoint for searching things (#1383)
|
||||
- NOISSUE - Add missing auth port in nginx enrypoint.sh (#1380)
|
||||
- MF-1346 - Create Groups API - add grouping of entities (#1334)
|
||||
- NOISSUE - Fix certs and vault deployment, reorganize and remove unnecessary vars (#1368)
|
||||
- MF-1317 - Configurable regexp rule for password (#1355)
|
||||
- Fix CoAP Adapter README (#1376)
|
||||
- NOISSUE - Fix default values for port and x509 provision (#1367)
|
||||
- NOISSUE - Added missing endpoints for users service (#1372)
|
||||
- MF-1365 - Add ADOPTERS.md file (#1371)
|
||||
- Fix grpc endpoint parameter permutation (#1370)
|
||||
- NOISSUE - Add IsChannelOwner grpc endpoint (#1366)
|
||||
- MF-1362 - Sort Things and Channels connections by name (#1363)
|
||||
- MF-1314 - Add value comparison filters for readers (#1353)
|
||||
- Fix env configuration and documentation (#1360)
|
||||
- NOISSUE - Support disabling Email Agent authentication (#1356)
|
||||
- NOISSUE - Upgrade Mongo, Cassandra and Influx docker images (#1354)
|
||||
- NOISSUE - Add READMEs to pkg packages (#1352)
|
||||
- NOISSUE - Correct README (#1349)
|
||||
- MF-1342 - Use environment variables in docker-compose to use tagged version of image (#1343)
|
||||
- MF-1311 - Add Notifications service (#1324)
|
||||
- MF-1344 - Fix links to API documentations #1345
|
||||
- NOISSUE - Upgrade influxdb and postgres docker images (#1341)
|
||||
- NOISSUE - Revert cli to use user token from command args (#1339)
|
||||
- MF-1276 - Fix openapi IDs and Keys format (#1338)
|
||||
- MF-1061 - Add PageMetadata to readers (#1333)
|
||||
- NOISSUE - Fix run script and compiler warnings (#1336)
|
||||
- Fix Postgres writer transaction handling (#1335)
|
||||
- Make Transformer type configurable (#1331)
|
||||
- MF-1061 - Implement v, vb, vs, vd and from/to mongodb-reader filters (#1326)
|
||||
- NOISSUE - Rename package aliases uuidProvider into uuid (#1323)
|
||||
- MF-1034 - Wrapping MQTT client (#1318)
|
||||
- MF-1061 - Fix cassandra-reader count for json format (#1327)
|
||||
- MF-1061 - Implement v, vb, vs, vd and from/to cassandra-reader filters (#1325)
|
||||
- NOISSUE - Switch to Consumers interface (#1316)
|
||||
- MF-1061 - Implement protocol, name, v, vb, vs, vd and from/to Postgres reader… (#1322)
|
||||
- MF-1061 - Add name, protocol and publisher tests to influxdb-reader (#1320)
|
||||
- NOISSUE - Fix Auth typo (#1319)
|
||||
- NOISSUE - Add health check for MQTT broker (#1305)
|
||||
- MF-1264 - Add support for JSON readers (#1295)
|
||||
- NOISSUE - Merge authz and authn into new service auth (#1313)
|
||||
- MF-1061 - Implement InfluxDB filters value, v, vb, vs, vd, from, to (#1312)
|
||||
- NOISSUE - Correct readers openapi.yml (#1310)
|
||||
- NOISSUE - Fix MQTT Forwarder client id (#1309)
|
||||
- NOISSUE - Fix dates not being init properly on save, change path construction, replace UUID with ULID for group ID (#1300)
|
||||
- NOISSUE - Remove authz from docker comp (#1307)
|
||||
- Shorten descriptions and add formats (#1306)
|
||||
- NOISSUE - remove owner id from user table and object (#1303)
|
||||
- NOISSUE - Add missing fields to openapi specs and enclose http codes in single quotes (#1302)
|
||||
- MF-1290 - Sort Things and Channels by name (#1293)
|
||||
- MF-1248 - Add access policies for users (#1246)
|
||||
- Fixes, without spaces. (#1296)
|
||||
- Add different CNs for CA and certs (#1292)
|
||||
- MF-397 - Introduce Thing Groups (#1259)
|
||||
- Add Enhancement section to the issue template (#1284)
|
||||
- Fix hardcoded env var values (#1283)
|
||||
- NOISSUE - Improve AuthN service docs (#1282)
|
||||
- MF-1268 - CLI improvements (#1274)
|
||||
- NOISSSUE - Vault integration as an addon. (#1266)
|
||||
- Fix naming in Authn API tests (#1275)
|
||||
- MF-1244 - Return UserID alongside with user Email in Identify response (#1245)
|
||||
- NOISSUE - Fix ViewGroup and UpdateGroup (#1269)
|
||||
- NOISSUE - Add ListUsers, ViewUser and ViewProfile methods (#1262)
|
||||
- NOISSUE - Rm users http package (#1256)
|
||||
- NOISSUE - Remove content-type check from decodeListUserGroupsRequest (#1255)
|
||||
- NOISSUE - Migrate swaggers to openapi 3 spec (#1250)
|
||||
- Update MQTT Broker Docker scripts (#1253)
|
||||
- update mproxy version (#1251)
|
||||
- NOISSUE - Fix group retrieval when parent id is not specified (#1247)
|
||||
- NOISSUE - Add new endpoint to retrieve configuration to be used as a template. (#1242)
|
||||
- NOISSUE - Add user groups (#1228)
|
||||
- MF-1237 - Return to transport only things service errors (#1236)
|
||||
- MF-928 - Change CoAP lib (#1233)
|
||||
- NOISSUE - Simplify make cleandocker (#1230)
|
||||
- NOISSUE - Fix malformed Swagger API specs (#1229)
|
||||
- MF-435 - Add support for env file loading (#1223)
|
||||
- update certs docs (#1227)
|
||||
- NOISSUE - Fix certs update in bootstrap config and make content handling in config.toml user friendly (#1221)
|
||||
- NOISSUE - Fix typo in authorization.js (#1226)
|
||||
- MF-983 - Add HTTP query param to connections list endpoints to fetch disconnected Things or Channels (#1217)
|
||||
- MF-1179 - Add a certificate service and certs endpoint to SDK (#1188)
|
||||
- NOISUE - Fix cache error when key is not in Redis (#1220)
|
||||
- MF-1199 - Add NATS messaging tests (#1209)
|
||||
- NOISSUE: Fix emailer (#1219)
|
||||
- NOISSUE - Update dependencies (#1218)
|
||||
- NOISSUE - Add subtopic wildcard for twin attribute's definition (#1214)
|
||||
- fix envs for nginx (#1215)
|
||||
- Remove twin mqtt related obsolete var and fix es-redis address (#1213)
|
||||
- NOISSUE - Remove unused `MF_THINGS_SECRET` env var (#1211)
|
||||
- NOISSUE - Fix some typos (#1212)
|
||||
- NOISSUE - Remove unknown Bootstrap requests (#1210)
|
||||
- NOISSUE - Use `pgcrypto` instead `uuid-ossp` for UUIDs generation (version 4) (#1208)
|
||||
- MF-1198 - Add errors package tests (#1207)
|
||||
- MF-1025 - timeout env in sec, use parseduration (#1206)
|
||||
- MF-1201 - Fix MF_THINGS_AUTH_GRPC_URL mongo reader ENVAR (#1203)
|
||||
- NOISSUE - Fix CI (#1204)
|
||||
- MF-1180 - Add redis based twins and states cache (#1184)
|
||||
- MF-739 - Add ID to the User entity (#1152)
|
||||
- NOISSUE - Fix default db name for storage databases (#1194)
|
||||
- NOISSUE - Add `MF_DOCKER_IMAGE_NAME_PREFIX` to Makefile (#1173)
|
||||
- MF-1154 - Move UUID provider to project root (#1172)
|
||||
- Fix typo in error messages (#1193)
|
||||
- MF-1190 - Add pkg for library packages (#1191)
|
||||
- MF-1177 - Implement caching in MQTT adapter (#1187)
|
||||
- NOISSUE - Refactor provision tool (#1189)
|
||||
|
||||
## 0.11.0 - 29. MAY 2020.
|
||||
### Features and Bugfixes
|
||||
- Add VerneMQ docker image build from source (#1178)
|
||||
|
||||
@@ -4,15 +4,23 @@
|
||||
MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
|
||||
bootstrap opcua authn twins mqtt provision certs
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader timescale-writer timescale-reader cli \
|
||||
bootstrap opcua auth twins mqtt provision certs smtp-notifier smpp-notifier
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
VERSION ?= $(shell git describe --abbrev=0 --tags)
|
||||
COMMIT ?= $(shell git rev-parse HEAD)
|
||||
TIME ?= $(shell date +%F_%T)
|
||||
|
||||
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
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||
go build -mod=vendor -ldflags "-s -w \
|
||||
-X 'github.com/mainflux/mainflux.BuildTime=$(TIME)' \
|
||||
-X 'github.com/mainflux/mainflux.Version=$(VERSION)' \
|
||||
-X 'github.com/mainflux/mainflux.Commit=$(COMMIT)'" \
|
||||
-o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
@@ -23,6 +31,9 @@ define make_docker
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg COMMIT=$(COMMIT) \
|
||||
--build-arg TIME=$(TIME) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
@@ -101,13 +112,3 @@ rundev:
|
||||
|
||||
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
|
||||
|
||||
@@ -10,27 +10,25 @@
|
||||
|
||||
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,
|
||||
It accepts user and thing (sensor, actuator, application) 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
|
||||
- Fine-grained access control (policies, ABAC/RBAC)
|
||||
- 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
|
||||
- [OPC UA](opcua) integration
|
||||
- Edge [Agent](agent) and [Export](export) services for remote IoT gateway management and edge computing
|
||||
- SDK
|
||||
- CLI
|
||||
- Small memory footprint and fast execution
|
||||
@@ -40,8 +38,8 @@ which maintains the project and offers professional services around it.
|
||||
|
||||
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)
|
||||
- [Docker](https://docs.docker.com/install/) (version 20.10)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.29)
|
||||
|
||||
Developing Mainflux will also require:
|
||||
|
||||
@@ -62,9 +60,25 @@ This will bring up the Mainflux docker services and interconnect them. This comm
|
||||
make run
|
||||
```
|
||||
|
||||
If you want to run services from specific release checkout code from github and make sure that
|
||||
`MF_RELEASE_TAG` in [.env](.env) is being set to match the release version
|
||||
|
||||
```bash
|
||||
git checkout tags/<release_number> -b <release_number>
|
||||
# e.g. `git checkout tags/0.13.0 -b 0.13.0`
|
||||
```
|
||||
|
||||
Check that `.env` file contains:
|
||||
|
||||
```bash
|
||||
MF_RELEASE_TAG=<release_number>
|
||||
```
|
||||
|
||||
>`docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://docs.mainflux.io/kubernetes).
|
||||
|
||||
## Usage
|
||||
|
||||
The quickest way to start using Mainflux is via the CLI. The latest version can be downloaded from the [official releases page][rel].
|
||||
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:
|
||||
|
||||
@@ -73,16 +87,14 @@ 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/).
|
||||
Additional details on using the CLI can be found in the [CLI documentation](https://docs.mainflux.io/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):
|
||||
Official documentation is hosted at [Mainflux official 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].
|
||||
@@ -101,6 +113,12 @@ The Mainflux team would like to give special thanks to [@mijicd][dejan] for his
|
||||
on designing and implementing a highly improved and optimized version of the platform,
|
||||
and [@malidukica][dusanm] for his effort on implementing the initial user interface.
|
||||
|
||||
## Professional Support
|
||||
|
||||
There are many companies offering professional support for the Mainflux system.
|
||||
|
||||
If you need this kind of support, best is to reach out to [@drasko][drasko] directly, and he will point you out to the best-matching support team.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for your interest in Mainflux and the desire to contribute!
|
||||
@@ -111,10 +129,9 @@ Thank you for your interest in Mainflux and the desire to contribute!
|
||||
|
||||
### 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>.
|
||||
You like Mainflux and you would like to make it your day job? We're always looking for talented engineers interested in open-source, IoT and distributed systems. If you recognize yourself, reach out to [@drasko][drasko] - he will contact you back.
|
||||
|
||||
>The best way to grab our attention is by sending PRs :sunglasses:.
|
||||
>The best way to grab our attention is, of course, by sending PRs :sunglasses:.
|
||||
|
||||
## Community
|
||||
|
||||
@@ -131,7 +148,7 @@ please head to company's [careers page][careers] or shoot us an e-mail at <caree
|
||||
[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
|
||||
[docs]: https://docs.mainflux.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
|
||||
@@ -143,6 +160,9 @@ please head to company's [careers page][careers] or shoot us an e-mail at <caree
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
[lora]: https://lora-alliance.org/
|
||||
[opcua]: https://opcfoundation.org/about/opc-technologies/opc-ua/
|
||||
[agent]: https://github.com/mainflux/agent
|
||||
[export]: https://github.com/mainflux/export
|
||||
[kubernetes]: https://kubernetes.io/
|
||||
[rel]: https://github.com/mainflux/mainflux/releases
|
||||
[careers]: https://www.mainflux.com/careers.html
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
asyncapi: '2.2.0'
|
||||
info:
|
||||
title: MQTT Adapter
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0
|
||||
version: '1.0.0'
|
||||
description: |
|
||||
MQTT adapter provides an MQTT API for sending messages through the platform. MQTT adapter uses [mProxy](https://github.com/mainflux/mproxy) for proxying traffic between client and MQTT broker.
|
||||
Additionally, the MQTT adapter and the NATS message broker are replicating the traffic between brokers.
|
||||
|
||||
defaultContentType: application/json
|
||||
|
||||
servers:
|
||||
dev:
|
||||
url: localhost:{port}
|
||||
protocol: mqtt
|
||||
description: Test broker
|
||||
variables:
|
||||
port:
|
||||
description: Secure connection (TLS) is available through port 8883.
|
||||
default: '1883'
|
||||
enum:
|
||||
- '1883'
|
||||
- '8883'
|
||||
username:
|
||||
description: Thing ID connected to the channel defined in the MQTT topic.
|
||||
password:
|
||||
description: Thing Key corresponding to the Thing ID.
|
||||
|
||||
channels:
|
||||
channels/{channelId}/messages/{subtopic}:
|
||||
parameters:
|
||||
channelId:
|
||||
$ref: '#/components/parameters/channelId'
|
||||
subtopic:
|
||||
$ref: '#/components/parameters/subtopic'
|
||||
publish:
|
||||
traits:
|
||||
- $ref: '#/components/operationTraits/mqtt'
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
subscribe:
|
||||
traits:
|
||||
- $ref: '#/components/operationTraits/mqtt'
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
|
||||
components:
|
||||
messages:
|
||||
jsonMsg:
|
||||
title: JSON Message
|
||||
summary: Arbitrary JSON array or object.
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: "#/components/schemas/jsonMsg"
|
||||
|
||||
schemas:
|
||||
jsonMsg:
|
||||
type: object
|
||||
description: Arbitrary JSON object or array. SenML format is recommended.
|
||||
example: |
|
||||
### SenML
|
||||
```json
|
||||
[{"bn":"some-base-name:","bt":1641646520, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]
|
||||
```
|
||||
### JSON
|
||||
```json
|
||||
{"field_1":"val_1", "t": 1641646525}
|
||||
```
|
||||
### JSON Array
|
||||
```json
|
||||
[{"field_1":"val_1", "t": 1641646520},{"field_2":"val_2", "t": 1641646522}]
|
||||
```
|
||||
|
||||
parameters:
|
||||
channelId:
|
||||
description: Channel ID connected to the Thing ID defined in the username.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
subtopic:
|
||||
description: Arbitrary message subtopic.
|
||||
schema:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
operationTraits:
|
||||
mqtt:
|
||||
bindings:
|
||||
mqtt:
|
||||
qos: 2
|
||||
@@ -0,0 +1,5 @@
|
||||
# Mainflux OpenAPI Specification
|
||||
|
||||
This folder contains an OpenAPI specifications for Mainflux API.
|
||||
|
||||
View specification in Swagger UI at [api.mainflux.io](https://api.mainflux.io)
|
||||
@@ -0,0 +1,735 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- auth
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyRequest"
|
||||
responses:
|
||||
'201':
|
||||
description: Issued new key.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'409':
|
||||
description: Failed due to using already existing ID.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/keys/{id}:
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- auth
|
||||
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.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
'204':
|
||||
description: Key revoked.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups:
|
||||
post:
|
||||
summary: Creates new group
|
||||
description: |
|
||||
Creates new group that can be used for grouping entities - things, users.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/requestBodies/GroupCreateReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets all groups.
|
||||
description: |
|
||||
Gets all groups up to a max level of hierarchy that can be fetched in one
|
||||
request ( max level = 5). Result can be filtered by metadata. Groups will
|
||||
be returned as JSON array or JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}:
|
||||
get:
|
||||
summary: Gets group info.
|
||||
description: |
|
||||
Gets info on a group specified by id.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates group data.
|
||||
description: |
|
||||
Updates Name, Description or Metadata of a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/GroupUpdateReq"
|
||||
responses:
|
||||
'200':
|
||||
description: Group updated.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Deletes group.
|
||||
description: |
|
||||
Deletes group. If group is parent and descendant groups do not have any members
|
||||
child groups will be deleted. Group cannot be deleted if has members or if
|
||||
any descendant group has members.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'204':
|
||||
description: Group removed.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}/children:
|
||||
get:
|
||||
summary: Gets group children.
|
||||
description: |
|
||||
Gets the whole tree of descendants of group for given id including itself.
|
||||
For performance reason request is limited up to a given level of hierarchy
|
||||
(max. 5).
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}/parents:
|
||||
get:
|
||||
summary: Gets group info.
|
||||
description: |
|
||||
Gets a direct line of ancestors for a group specified by id.
|
||||
Result is up to a specified hierarchy level or up to a root group.
|
||||
Result can be a JSON array or a JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}/members:
|
||||
post:
|
||||
summary: Assigns members to a group.
|
||||
description: |
|
||||
Assigns thing or user id to a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MembersReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets members of a group.
|
||||
description: |
|
||||
Array of member ids that are in the group specified with groupID.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/MembersRes"
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{userGroupID}/share:
|
||||
post:
|
||||
summary: Adds access rights on thing groups to user group with userGroupID.
|
||||
description: |
|
||||
Takes user group id through parameter and adds access rights for user group on thing group received via request body.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/UserGroupID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ShareGroupAccessReq"
|
||||
responses:
|
||||
'200':
|
||||
description: User group shared with thing group.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/members/{memberId}/groups:
|
||||
get:
|
||||
summary: Gets memberships for a member with member id.
|
||||
description: |
|
||||
Array of groups that member belongs to.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/MemberId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupRes"
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/policies:
|
||||
post:
|
||||
summary: Creates new policies.
|
||||
description: |
|
||||
Creates new policies. Only admin can use this endpoint. Therefore, you need an authentication token for the admin.
|
||||
Also, only policies defined on the system are allowed to add. For more details, please see the docs for Authorization.
|
||||
tags:
|
||||
- auth
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/PoliciesReq"
|
||||
responses:
|
||||
'201':
|
||||
description: Policies created.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'403':
|
||||
description: Unauthorized access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Deletes policies.
|
||||
description: |
|
||||
Deletes policies. Only admin can use this endpoint. Therefore, you need an authentication token for the admin.
|
||||
Also, only policies defined on the system are allowed to delete. For more details, please see the docs for Authorization.
|
||||
tags:
|
||||
- auth
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/PoliciesReq"
|
||||
responses:
|
||||
'204':
|
||||
description: Policies deleted.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
issuer_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "9118de62-c680-46b7-ad0a-21748a52833a"
|
||||
description: In ID of the entity that issued the token.
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
subject:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key subject.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated.
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires. If this field is missing,
|
||||
that means that Key is valid indefinitely.
|
||||
GroupReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
parent_id:
|
||||
type: string
|
||||
format: ulid
|
||||
description: Id of parent group, it must be existing group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
GroupUpdateSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
GroupResSchema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
description: Unique group identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form group name.
|
||||
parent_id:
|
||||
type: string
|
||||
description: Group ID of parent group.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of user that created the group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
level:
|
||||
type: integer
|
||||
description: Level in hierarchy, distance from the root group.
|
||||
path:
|
||||
type: string
|
||||
description: Hierarchy path, concatenated ids of group ancestors.
|
||||
children:
|
||||
type: object
|
||||
# schema: GroupResSchema
|
||||
created_at:
|
||||
type: string
|
||||
description: Datetime of group creation.
|
||||
updated_at:
|
||||
type: string
|
||||
description: Datetime of last group updated.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- owner_id
|
||||
- description
|
||||
- level
|
||||
- path
|
||||
- created_at
|
||||
- updated_at
|
||||
MembersReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
members:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
format: uuid | ulid
|
||||
type:
|
||||
type: string
|
||||
description: Type of entity
|
||||
ShareGroupAccessReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
thing_group_id:
|
||||
type: string
|
||||
description: Group ID of the Thing Group.
|
||||
format: uuid
|
||||
GroupsPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
level:
|
||||
type: integer
|
||||
description: Level of hierarchy up to which groups are fetched.
|
||||
required:
|
||||
- groups
|
||||
- total
|
||||
- level
|
||||
MembershipPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
required:
|
||||
- groups
|
||||
PoliciesReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
object:
|
||||
type: string
|
||||
description: |
|
||||
Specifies an object field for the field.
|
||||
Object indicates application objects such as ThingID.
|
||||
subjects:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
policies:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
|
||||
parameters:
|
||||
ApiKeyId:
|
||||
name: id
|
||||
description: API Key ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
UserGroupID:
|
||||
name: userGroupID
|
||||
description: User Group ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Group ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
MemberId:
|
||||
name: memberId
|
||||
description: Member id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid | ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Level:
|
||||
name: level
|
||||
description: Level of hierarchy up to which to retrieve groups from given group id.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 5
|
||||
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: {}
|
||||
Tree:
|
||||
name: tree
|
||||
description: Specify type of response, JSON array or tree.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
GroupCreateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupReqSchema"
|
||||
GroupUpdateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupUpdateSchema"
|
||||
MembersReq:
|
||||
description: JSON array of member IDs.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembersReqSchema"
|
||||
ShareGroupAccessReq:
|
||||
description: test
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareGroupAccessReqSchema"
|
||||
PoliciesReq:
|
||||
description: JSON-formatted document describing adding policies request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PoliciesReqSchema"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
GroupCreateRes:
|
||||
description: Group created.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created group's relative URL.
|
||||
example: /groups/{groupId}
|
||||
ShareAccessRightRes:
|
||||
description: User group shared with thing group.
|
||||
GroupRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
GroupsPageRes:
|
||||
description: Group data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupsPage"
|
||||
MembersRes:
|
||||
description: Groups data retrieved. Groups assigned to a member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
MembershipPageRes:
|
||||
description: Groups data retrieved. Groups assigned to a member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -13,20 +13,18 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed configs
|
||||
@@ -38,19 +36,18 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
@@ -58,16 +55,15 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates config info
|
||||
@@ -78,22 +74,21 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Config updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a Config
|
||||
@@ -103,16 +98,15 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
204:
|
||||
'204':
|
||||
description: Config removed.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed config ID.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
@@ -123,22 +117,21 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Config updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/connections/{configId}:
|
||||
put:
|
||||
@@ -149,22 +142,21 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Config updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
@@ -173,16 +165,20 @@ paths:
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid external key provided.
|
||||
'404':
|
||||
description: Failed to retrieve corresponding config.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
@@ -191,16 +187,17 @@ paths:
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapEncAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/EncConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
Failed to retrieve corresponding config.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/state/{configId}:
|
||||
put:
|
||||
@@ -211,33 +208,44 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
|
||||
responses:
|
||||
204:
|
||||
'204':
|
||||
description: Config removed.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
@@ -247,7 +255,8 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: ID of the Channel.
|
||||
format: uuid
|
||||
description: Channel unique identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
@@ -267,7 +276,7 @@ components:
|
||||
$ref: "#/components/schemas/State"
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
- external_key
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -298,9 +307,11 @@ components:
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
@@ -323,39 +334,16 @@ components:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
- content
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
EncConfigAuth:
|
||||
name: configAuthorization
|
||||
description: |
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
@@ -452,12 +440,12 @@ components:
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the thing is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
@@ -507,3 +495,37 @@ components:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
bootstrapAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: string
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <external_key>"
|
||||
|
||||
bootstrapEncAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: aes-sha256-uuid
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <external_enc_key>"
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -0,0 +1,257 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Certs service
|
||||
description: HTTP API for Certs service
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/certs:
|
||||
post:
|
||||
summary: Creates a certificate for thing
|
||||
description: Creates a certificate for thing
|
||||
tags:
|
||||
- certs
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
description: Unexpected server-side error ocurred.
|
||||
/certs/{certID}:
|
||||
get:
|
||||
summary: Retrieves a certificate
|
||||
description: |
|
||||
Retrieves a certificate for a given cert ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/CertRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revokes a certificate
|
||||
description: |
|
||||
Revokes a certificate for a given cert ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/serials/{thingID}:
|
||||
get:
|
||||
summary: Retrieves certificates' serial IDs
|
||||
description: |
|
||||
Retrieves a list of certificates' serial IDs for a given thing ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/SerialsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding certificates.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
ThingID:
|
||||
name: thingID
|
||||
description: Thing ID
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
CertID:
|
||||
name: certID
|
||||
description: Serial of certificate
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Cert:
|
||||
type: object
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
issuing_ca:
|
||||
type: string
|
||||
description: CA Certificate that is used to issue client certs, usually intermediate.
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Serial:
|
||||
type: object
|
||||
properties:
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
CertsPage:
|
||||
type: object
|
||||
properties:
|
||||
certs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
SerialsPage:
|
||||
type: object
|
||||
properties:
|
||||
serials:
|
||||
type: array
|
||||
description: Certificate serials IDs.
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
Revoke:
|
||||
type: object
|
||||
properties:
|
||||
revocation_time:
|
||||
type: string
|
||||
description: Certificate revocation time
|
||||
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- thing_id
|
||||
- ttl
|
||||
- key_bits
|
||||
- key_type
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
ttl:
|
||||
type: string
|
||||
key_type:
|
||||
type: string
|
||||
key_bits:
|
||||
type: integer
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
CertRes:
|
||||
description: Certificate data.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
CertsPageRes:
|
||||
description: Certificates page.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CertsPage"
|
||||
SerialsPageRes:
|
||||
description: Serials page.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SerialsPage"
|
||||
RevokeRes:
|
||||
description: Certificate revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revoke"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -0,0 +1,220 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Notifiers service
|
||||
description: HTTP API for Notifiers service.
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/subscriptions:
|
||||
post:
|
||||
summary: Create subscription
|
||||
description: Creates a new subscription give a topic and contact.
|
||||
tags:
|
||||
- notifiers
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/Create"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/Create"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"409":
|
||||
description: Failed due to using an existing topic and contact.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: List subscriptions
|
||||
description: List subscriptions given list parameters.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Topic"
|
||||
- $ref: "#/components/parameters/Contact"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Page"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/subscriptions/{id}:
|
||||
get:
|
||||
summary: Get subscription with the provided id
|
||||
description: Retrieves a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/View"
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Delete subscription with the provided id
|
||||
description: Removes a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"204":
|
||||
description: Subscription removed
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Subscription:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
example: 01EWDVKBQSG80B6PQRS9PAAY35
|
||||
description: ULID id of the subscription.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: An id of the owner who created subscription.
|
||||
topic:
|
||||
type: string
|
||||
example: topic.subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
Page:
|
||||
type: object
|
||||
properties:
|
||||
subscriptions:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
|
||||
parameters:
|
||||
Id:
|
||||
name: id
|
||||
description: Unique identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Topic:
|
||||
name: topic
|
||||
description: Topic name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Contact:
|
||||
name: contact
|
||||
description: Subscription contact.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
Create:
|
||||
description: JSON-formatted document describing the new subscription to be created
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
|
||||
responses:
|
||||
Create:
|
||||
description: Created a new subscription.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created subscription relative URL
|
||||
example: /subscriptions/{id}
|
||||
View:
|
||||
description: View subscription.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
Page:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Page"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -13,23 +13,32 @@ paths:
|
||||
tags:
|
||||
- messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MessageReq"
|
||||
responses:
|
||||
202:
|
||||
"202":
|
||||
description: Message is accepted for processing.
|
||||
400:
|
||||
"400":
|
||||
description: Message discarded due to its malformed content.
|
||||
403:
|
||||
description: Message discarded due to missing or invalid credentials.
|
||||
404:
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Message discarded due to invalid channel id.
|
||||
415:
|
||||
"415":
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
500:
|
||||
description: Unexpected server-side error occurred.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
@@ -96,13 +105,6 @@ components:
|
||||
$ref: "#/components/schemas/SenMLRecord"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ID:
|
||||
name: id
|
||||
description: Unique channel identifier.
|
||||
@@ -115,13 +117,46 @@ components:
|
||||
requestBodies:
|
||||
MessageReq:
|
||||
description: |
|
||||
Message to be distributed. Since the platform expects messages to be
|
||||
properly formatted SenML in order to be post-processed, clients are
|
||||
obliged to specify Content-Type header for each published message.
|
||||
Note that all messages that aren't SenML will be accepted and published,
|
||||
but no post-processing will be applied.
|
||||
Message to be distributed. Since the platform expects messages to be
|
||||
properly formatted SenML in order to be post-processed, clients are
|
||||
obliged to specify Content-Type header for each published message.
|
||||
Note that all messages that aren't SenML will be accepted and published,
|
||||
but no post-processing will be applied.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SenMLArray"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Thing access: "Authorization: Thing <thing_key>"
|
||||
|
||||
basicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
* Things access: "Authorization: Basic <base64-encoded_credentials>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- basicAuth: []
|
||||
@@ -11,19 +11,17 @@ paths:
|
||||
description: Adds new device to proxy
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ProvisionReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: Created
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Unauthorized.
|
||||
500:
|
||||
description: Unexpected server-side error ocurred.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets current mapping.
|
||||
description: Gets current mapping. This can be used in UI
|
||||
@@ -31,27 +29,25 @@ paths:
|
||||
configuration created with provision service.
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ProvisionRes"
|
||||
403:
|
||||
description: Unauthorized.
|
||||
500:
|
||||
description: Unexpected server-side error ocurred.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token. Used instead of credentials in env or config.toml.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ProvisionReq:
|
||||
description: MAC address of device or other identifier
|
||||
@@ -71,9 +67,28 @@ components:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
ProvisionRes:
|
||||
description: Current mapping JSON representation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -16,18 +16,35 @@ paths:
|
||||
tags:
|
||||
- messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Publisher"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Value"
|
||||
- $ref: "#/components/parameters/BoolValue"
|
||||
- $ref: "#/components/parameters/StringValue"
|
||||
- $ref: "#/components/parameters/DataValue"
|
||||
- $ref: "#/components/parameters/From"
|
||||
- $ref: "#/components/parameters/To"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/MessagesPageRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
@@ -89,20 +106,13 @@ components:
|
||||
description: Time of updating measurement.
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Thing access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
@@ -123,6 +133,77 @@ components:
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Publisher:
|
||||
name: Publisher
|
||||
description: Unique thing identifier.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: SenML message name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Value:
|
||||
name: v
|
||||
description: SenML message value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
BoolValue:
|
||||
name: vb
|
||||
description: SenML message bool value.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
StringValue:
|
||||
name: vs
|
||||
description: SenML message string value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
DataValue:
|
||||
name: vd
|
||||
description: SenML message data value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Comparator:
|
||||
name: comparator
|
||||
description: Value comparison operator.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: eq
|
||||
enum:
|
||||
- eq
|
||||
- lt
|
||||
- le
|
||||
- gt
|
||||
- ge
|
||||
required: false
|
||||
From:
|
||||
name: from
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
required: false
|
||||
To:
|
||||
name: to
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
required: false
|
||||
|
||||
responses:
|
||||
MessagesPageRes:
|
||||
@@ -131,6 +212,30 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesPage"
|
||||
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
thingAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <thing_key>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- thingAuth: []
|
||||
@@ -0,0 +1,23 @@
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Service status.
|
||||
enum:
|
||||
- pass
|
||||
version:
|
||||
type: string
|
||||
description: Service version.
|
||||
example: 0.0.1
|
||||
commit:
|
||||
type: string
|
||||
description: Service commit hash.
|
||||
example: 7d6f4dc4f7f0c1fa3dc24eddfb18bb5073ff4f62
|
||||
description:
|
||||
type: string
|
||||
description: Service description.
|
||||
example: <service_name> service
|
||||
build_time:
|
||||
type: string
|
||||
description: Service build time.
|
||||
example: 1970-01-01_00:00:00
|
||||
@@ -13,52 +13,77 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
$ref: "#/components/responses/CreateThingRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
409:
|
||||
'409':
|
||||
description: Entity already exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed things
|
||||
summary: Retrieves things
|
||||
description: |
|
||||
Retrieves a list of managed things. Due to performance concerns, data
|
||||
Retrieves a list of things. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Order"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ThingsPageRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/search:
|
||||
post:
|
||||
summary: Search and retrieves things
|
||||
description: |
|
||||
Retrieves a list of things with name and metadata filtering.
|
||||
Due to performance concerns, data is retrieved in subsets.
|
||||
The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- things
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingsSearchReq"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/ThingsPageRes"
|
||||
'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: Unprocessable Entity
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bulk:
|
||||
post:
|
||||
@@ -68,20 +93,18 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingsCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: Things registered.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}:
|
||||
get:
|
||||
@@ -89,18 +112,17 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ThingRes"
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Thing does not exist.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates thing info
|
||||
@@ -111,22 +133,21 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Thing updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Thing does not exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a thing
|
||||
@@ -136,16 +157,40 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
204:
|
||||
'204':
|
||||
description: Thing removed.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed thing's ID.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}/share:
|
||||
post:
|
||||
summary: Shares a thing with user identified by request body.
|
||||
description: |
|
||||
Adds 'read', 'write' or 'delete' policies to the user identified by the request body.
|
||||
Sharing a particular thing is only allowed to users who have 'write' access to that thing.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ShareThingReq"
|
||||
responses:
|
||||
'200':
|
||||
description: Policies shared.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'403':
|
||||
description: Lack of policies in order to share the thing.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}/key:
|
||||
patch:
|
||||
@@ -155,24 +200,23 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Thing key updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Thing does not exist.
|
||||
409:
|
||||
'409':
|
||||
description: Specified key already exists.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels:
|
||||
post:
|
||||
@@ -182,47 +226,47 @@ paths:
|
||||
be the channel's owner.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ChannelCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
$ref: "#/components/responses/ChannelCreateRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
409:
|
||||
'409':
|
||||
description: Entity already exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed channels
|
||||
summary: Retrieves channels
|
||||
description: |
|
||||
Retrieves a list of managed channels. Due to performance concerns, data
|
||||
Retrieves a list of channels. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Order"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ChannelsPageRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels/bulk:
|
||||
post:
|
||||
@@ -232,22 +276,20 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ChannelsCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: Channels registered.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
409:
|
||||
'409':
|
||||
description: Entity already exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels/{chanId}:
|
||||
get:
|
||||
@@ -255,20 +297,19 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ChannelRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed channel's ID.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Channel does not exist.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates channel info
|
||||
@@ -279,22 +320,21 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ChannelCreateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Channel updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Channel does not exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a channel
|
||||
@@ -304,16 +344,15 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
responses:
|
||||
204:
|
||||
'204':
|
||||
description: Channel removed.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed channel's ID.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/connect:
|
||||
post:
|
||||
@@ -323,28 +362,49 @@ paths:
|
||||
Channel and thing are owned by user identified using the provided access token.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConnCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
$ref: "#/components/responses/ConnCreateRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
409:
|
||||
'409':
|
||||
description: Entity already exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/disconnect:
|
||||
put:
|
||||
summary: Disconnect things and channels using lists of IDs.
|
||||
description: |
|
||||
Disconnect things from channels specified by lists of IDs.
|
||||
Channels and things are owned by user identified using the provided access token.
|
||||
tags:
|
||||
- things
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/DisconnReq"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/DisconnRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}/channels:
|
||||
get:
|
||||
summary: Retrieves list of channels connected or not connected to specified thing
|
||||
summary: List of channels connected to specified thing
|
||||
description: |
|
||||
Retrieves list of channels connected to specified thing with pagination
|
||||
metadata.
|
||||
@@ -356,21 +416,21 @@ paths:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Connected"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ChannelsPageRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Thing does not exist.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels/{chanId}/things:
|
||||
get:
|
||||
summary: Retrieves list of things connected or not connected to specified channel
|
||||
summary: List of things connected to specified channel
|
||||
description: |
|
||||
Retrieves list of things connected to specified channel with pagination
|
||||
metadata.
|
||||
@@ -382,17 +442,17 @@ paths:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Connected"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/ThingsPageRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
422:
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels/{chanId}/things/{thingId}:
|
||||
put:
|
||||
@@ -403,19 +463,18 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Thing connected.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Channel or thing does not exist.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Disconnects the thing from the channel
|
||||
@@ -425,21 +484,20 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
204:
|
||||
'204':
|
||||
description: Thing disconnected.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
401:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Channel or thing does not exist.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels/{chanId}/access:
|
||||
/identify/channels/{chanId}/access-by-key:
|
||||
post:
|
||||
summary: Checks if thing has access to a channel.
|
||||
description: |
|
||||
@@ -452,17 +510,17 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/IdentityReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/AccessGrantedRes"
|
||||
401:
|
||||
'401':
|
||||
description: |
|
||||
Thing and channel are not connected, or thing with specified key doesn't
|
||||
exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/channels/{chanId}/access-by-id:
|
||||
/identify/channels/{chanId}/access-by-id:
|
||||
post:
|
||||
summary: Checks if thing has access to a channel.
|
||||
description: |
|
||||
@@ -475,15 +533,15 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/AccessByIDReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Thing has access to the specified channel.
|
||||
401:
|
||||
'401':
|
||||
description: |
|
||||
Thing and channel are not connected, or thing with specified ID doesn't
|
||||
exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/identify:
|
||||
post:
|
||||
@@ -496,20 +554,60 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/IdentityReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: "#/components/responses/IdentityRes"
|
||||
401:
|
||||
'401':
|
||||
description: Thing with specified key doesn't exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}:
|
||||
get:
|
||||
summary: Retrieves things
|
||||
description: |
|
||||
Retrieves a list of things that belong to a group. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Order"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/ThingsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: |
|
||||
Thing key that is used for thing auth. If there is
|
||||
not one provided service will generate one in UUID
|
||||
@@ -519,7 +617,11 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Thing unique identifier.
|
||||
format: uuid
|
||||
description: Thing unique identifier. This can be either
|
||||
provided by the user or left blank. If the user provides a UUID,
|
||||
it would be validated. If there is not one provided then
|
||||
the service will generate one in UUID format.
|
||||
ThingReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -531,17 +633,56 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded thing's data.
|
||||
ThingsReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name filter. Filtering is performed as a case-insensitive partial match.
|
||||
metadata:
|
||||
type: object
|
||||
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
default: 0
|
||||
minimum: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
order:
|
||||
type: string
|
||||
description: Order type.
|
||||
default: id
|
||||
enum:
|
||||
- name
|
||||
- id
|
||||
dir:
|
||||
type: string
|
||||
description: Order direction.
|
||||
default: desc
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
ThingResSchema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique thing identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form thing name.
|
||||
key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Auto-generated access key.
|
||||
metadata:
|
||||
type: object
|
||||
@@ -616,8 +757,6 @@ components:
|
||||
ConnectionReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
$ref: "#/components/schemas/Key"
|
||||
channel_ids:
|
||||
type: array
|
||||
description: Channel IDs.
|
||||
@@ -628,29 +767,44 @@ components:
|
||||
description: Thing IDs
|
||||
items:
|
||||
type: string
|
||||
ShareThingReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
description: User ID.
|
||||
items:
|
||||
type: string
|
||||
policies:
|
||||
type: array
|
||||
description: Policies
|
||||
items:
|
||||
type: string
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ThingId:
|
||||
name: thingId
|
||||
description: Unique thing identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Unique group identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
@@ -685,7 +839,28 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
required: false
|
||||
Order:
|
||||
name: order
|
||||
description: Order type.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: id
|
||||
enum:
|
||||
- name
|
||||
- id
|
||||
required: false
|
||||
Direction:
|
||||
name: dir
|
||||
description: Order direction.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: desc
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
@@ -731,6 +906,13 @@ components:
|
||||
description: Free-form thing name.
|
||||
metadata:
|
||||
type: object
|
||||
ThingsSearchReq:
|
||||
description: JSON-formatted document describing search parameters.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ThingsReqSchema"
|
||||
KeyUpdateReq:
|
||||
required: true
|
||||
description: JSON containing thing.
|
||||
@@ -741,6 +923,7 @@ components:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Thing key that is used for thing auth.
|
||||
ChannelCreateReq:
|
||||
description: JSON-formatted document describing the updated channel.
|
||||
@@ -770,6 +953,13 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConnectionReqSchema"
|
||||
DisconnReq:
|
||||
description: JSON-formatted document describing the entities for disconnection.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConnectionReqSchema"
|
||||
IdentityReq:
|
||||
description: JSON-formatted document that contains thing key.
|
||||
required: true
|
||||
@@ -780,6 +970,7 @@ components:
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Thing key that is used for thing auth.
|
||||
required:
|
||||
- token
|
||||
@@ -793,7 +984,15 @@ components:
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Thing ID by which thing is uniquely identified.
|
||||
ShareThingReq:
|
||||
description: JSON-formatted document describing sharing things policies.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareThingReqSchema"
|
||||
|
||||
responses:
|
||||
CreateThingRes:
|
||||
@@ -849,6 +1048,8 @@ components:
|
||||
type: string
|
||||
description: Created thing's relative URL.
|
||||
example: /things/{thingId}
|
||||
DisconnRes:
|
||||
description: Things disconnected.
|
||||
AccessGrantedRes:
|
||||
description: |
|
||||
Thing has access to the specified channel and the thing ID is returned.
|
||||
@@ -869,3 +1070,20 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -13,43 +13,40 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TwinReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
$ref: "#/components/responses/TwinCreateRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
get:
|
||||
summary: Retrieves managed twins
|
||||
summary: Retrieves twins
|
||||
description: |
|
||||
Retrieves a list of managed twins. Due to performance concerns, data
|
||||
Retrieves a list of twins. Due to performance concerns, data
|
||||
is retrieved in subsets.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Name'
|
||||
- $ref: '#/components/parameters/Metadata'
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: '#/components/responses/TwinsPageRes'
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/twins/{twinID}:
|
||||
@@ -58,18 +55,17 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: '#/components/responses/TwinRes'
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed twin's ID.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
put:
|
||||
summary: Updates twin info
|
||||
@@ -79,22 +75,21 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/TwinReq'
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: Twin updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed twin's ID or malformed JSON.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
delete:
|
||||
summary: Removes a twin
|
||||
@@ -102,18 +97,17 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
responses:
|
||||
204:
|
||||
'204':
|
||||
description: Twin removed.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed twin's ID.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided
|
||||
404:
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/states/{twinID}:
|
||||
@@ -126,31 +120,32 @@ paths:
|
||||
- states
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Metadata'
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
$ref: '#/components/responses/StatesPageRes'
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
@@ -193,6 +188,7 @@ components:
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
minimum: 1
|
||||
required: true
|
||||
|
||||
@@ -243,6 +239,7 @@ components:
|
||||
description: Email address of Mainflux user that owns twin.
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique twin identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
@@ -292,6 +289,7 @@ components:
|
||||
properties:
|
||||
twin_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of twin state belongs to.
|
||||
id:
|
||||
type: number
|
||||
@@ -362,3 +360,20 @@ components:
|
||||
$ref: '#/components/schemas/StatesPage'
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -16,33 +16,45 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UserCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
$ref: "#/components/responses/UserCreateRes"
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets info on currently logged in user.
|
||||
summary: Retrieves users
|
||||
description: |
|
||||
Gets info on currently logged in user. Info is obtained using
|
||||
authorization token
|
||||
Retrieves a list of users. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/UserRes"
|
||||
400:
|
||||
'200':
|
||||
$ref: "#/components/responses/UsersPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'401':
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
This endpoint is available only for administrators.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates info on currently logged in user.
|
||||
@@ -51,115 +63,98 @@ paths:
|
||||
authorization token and the new received info.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UserUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
'200':
|
||||
description: User updated.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
404:
|
||||
'404':
|
||||
description: Failed due to non existing user.
|
||||
403:
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/users/{userId}/groups:
|
||||
get:
|
||||
summary: Get groups that user belongs to
|
||||
description: Retrieves a list of groups that user belongs to.
|
||||
/users/profile:
|
||||
get:
|
||||
summary: Gets info on currently logged in user.
|
||||
description: |
|
||||
Gets info on currently logged in user. Info is obtained using
|
||||
authorization token
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
- users
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/UserRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}:
|
||||
get:
|
||||
summary: Retrieves users
|
||||
description: |
|
||||
Retrieves a list of users that belong to a group. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/UserID"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/GroupsRes'
|
||||
403:
|
||||
'200':
|
||||
$ref: "#/components/responses/UsersPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/groups:
|
||||
post:
|
||||
summary: Create users group
|
||||
description: |
|
||||
Create users group.
|
||||
tags:
|
||||
- groups
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/CreateGroupReq'
|
||||
responses:
|
||||
200:
|
||||
description: Group created.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Group'
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
get:
|
||||
summary: Get users groups
|
||||
description: |
|
||||
Get all users groups
|
||||
tags:
|
||||
- groups
|
||||
security:
|
||||
- Authorization: []
|
||||
responses:
|
||||
200:
|
||||
description: Groups retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupsPage'
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/tokens:
|
||||
post:
|
||||
summary: User authentication
|
||||
description: Generates an access token when provided with proper credentials.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UserCreateReq"
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: User authenticated.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Token'
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
403:
|
||||
'401':
|
||||
description: Failed due to using invalid credentials.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/password/reset-request:
|
||||
post:
|
||||
@@ -174,13 +169,13 @@ paths:
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/RequestPasswordReset'
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: Users link for reseting password.
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/password/reset:
|
||||
put:
|
||||
@@ -194,13 +189,13 @@ paths:
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/PasswordReset'
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: User link .
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/password:
|
||||
patch:
|
||||
@@ -209,33 +204,36 @@ paths:
|
||||
When authenticated user wants to change password.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/PasswordChange'
|
||||
responses:
|
||||
201:
|
||||
'201':
|
||||
description: User link .
|
||||
400:
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
415:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
Authorization:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
schemas:
|
||||
Token:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
format: jwt
|
||||
description: Generated access token.
|
||||
required:
|
||||
- token
|
||||
@@ -255,24 +253,6 @@ components:
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
GroupReqObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Unique name of the group. Group name matching `"^[a-zA-Z0-9]+$"` regexp.
|
||||
parent_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Id of parent group
|
||||
description:
|
||||
type: string
|
||||
description: Description of group
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded thing's data.
|
||||
required:
|
||||
- name
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
@@ -280,58 +260,24 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: UUID id of the user.
|
||||
description: User unique identifier.
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: "test@example.com"
|
||||
description: User's email address will be used as its unique identifier
|
||||
metadata:
|
||||
type: string
|
||||
format: JSON
|
||||
description: Users metadata
|
||||
Group:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: UUID id of the group.
|
||||
name:
|
||||
type: string
|
||||
example: "MainflxGroup"
|
||||
description: Group name matching `"^[a-zA-Z0-9]+$"` regexp.
|
||||
description:
|
||||
type: string
|
||||
description: Description free form text describing a group
|
||||
description: User's email address will be used as its unique identifier.
|
||||
metadata:
|
||||
type: object
|
||||
description: Groups metadata
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
UsersPage:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: ID of the user
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to User
|
||||
UserMetadata:
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: string
|
||||
description: Users metadata
|
||||
GroupsPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
things:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Group"
|
||||
$ref: "#/components/schemas/User"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
@@ -341,6 +287,14 @@ components:
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- things
|
||||
UserMetadata:
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
@@ -370,7 +324,15 @@ components:
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: UUID
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Unique group identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
@@ -407,13 +369,6 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserMetadata"
|
||||
CreateGroupReq:
|
||||
description: JSON-formated document describing the new group to be created.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupReqObj'
|
||||
RequestPasswordReset:
|
||||
description: Initiate password request procedure.
|
||||
required: true
|
||||
@@ -424,7 +379,8 @@ components:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: Email of the user
|
||||
format: email
|
||||
description: User email.
|
||||
PasswordReset:
|
||||
description: Password reset request data, new password and token that is appended on password reset link received in email.
|
||||
content:
|
||||
@@ -434,15 +390,18 @@ components:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
description: New password
|
||||
format: password
|
||||
description: New password.
|
||||
minimum: 8
|
||||
confirm_password:
|
||||
type: string
|
||||
description: New password confirmed
|
||||
format: password
|
||||
description: New confirmation password.
|
||||
minimum: 8
|
||||
token:
|
||||
type: string
|
||||
description: Reset token generated and sent in email
|
||||
format: jwt
|
||||
description: Reset token generated and sent in email.
|
||||
PasswordChange:
|
||||
description: Password change data. User can change its password.
|
||||
required: true
|
||||
@@ -453,11 +412,12 @@ components:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
format: pass
|
||||
description: New password
|
||||
format: password
|
||||
description: New password.
|
||||
old_password:
|
||||
type: string
|
||||
description: Confirm password
|
||||
format: password
|
||||
description: Old password.
|
||||
|
||||
responses:
|
||||
UserCreateRes:
|
||||
@@ -468,7 +428,8 @@ components:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Registred user relative URL
|
||||
format: url
|
||||
description: Registered user relative URL.
|
||||
example: /users/{userId}
|
||||
UserRes:
|
||||
description: Data retrieved.
|
||||
@@ -476,12 +437,28 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
GroupsRes:
|
||||
UsersPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupsPage'
|
||||
|
||||
$ref: "#/components/schemas/UsersPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
+5303
File diff suppressed because it is too large
Load Diff
+129
@@ -0,0 +1,129 @@
|
||||
// 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 IsChannelOwner(ChannelOwnerReq) returns (google.protobuf.Empty) {}
|
||||
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
|
||||
rpc Identify(Token) returns (ThingID) {}
|
||||
}
|
||||
|
||||
service AuthService {
|
||||
rpc Issue(IssueReq) returns (Token) {}
|
||||
rpc Identify(Token) returns (UserIdentity) {}
|
||||
rpc Authorize(AuthorizeReq) returns (AuthorizeRes) {}
|
||||
rpc AddPolicy(AddPolicyReq) returns (AddPolicyRes) {}
|
||||
rpc DeletePolicy(DeletePolicyReq) returns (DeletePolicyRes) {}
|
||||
rpc ListPolicies(ListPoliciesReq) returns (ListPoliciesRes) {}
|
||||
rpc Assign(Assignment) returns(google.protobuf.Empty) {}
|
||||
rpc Members(MembersReq) returns (MembersRes) {}
|
||||
}
|
||||
|
||||
message AccessByKeyReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ChannelOwnerReq {
|
||||
string owner = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ThingID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message ChannelID {
|
||||
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 UserIdentity {
|
||||
string id = 1;
|
||||
string email = 2;
|
||||
}
|
||||
|
||||
message IssueReq {
|
||||
string id = 1;
|
||||
string email = 2;
|
||||
uint32 type = 3;
|
||||
}
|
||||
|
||||
message AuthorizeReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message AuthorizeRes {
|
||||
bool authorized = 1;
|
||||
}
|
||||
|
||||
message AddPolicyReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message AddPolicyRes {
|
||||
bool authorized = 1;
|
||||
}
|
||||
|
||||
message DeletePolicyReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message DeletePolicyRes {
|
||||
bool deleted = 1;
|
||||
}
|
||||
|
||||
message ListPoliciesReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message ListPoliciesRes {
|
||||
repeated string policies = 1;
|
||||
}
|
||||
|
||||
message Assignment {
|
||||
string token = 1;
|
||||
string groupID = 2;
|
||||
string memberID = 3;
|
||||
}
|
||||
|
||||
message MembersReq {
|
||||
string token = 1;
|
||||
string groupID = 2;
|
||||
uint64 offset = 3;
|
||||
uint64 limit = 4;
|
||||
string type = 5;
|
||||
}
|
||||
|
||||
message MembersRes {
|
||||
uint64 total = 1;
|
||||
uint64 offset = 2;
|
||||
uint64 limit = 3;
|
||||
string type = 4;
|
||||
repeated string members = 5;
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Auth - Authentication and Authorization service
|
||||
|
||||
Auth service provides authentication features as an API for managing authentication keys as well as administering groups of entities - `things` and `users`.
|
||||
|
||||
# Authentication
|
||||
User service is using Auth service gRPC API to obtain login token or password reset token. Authentication key consists of the following fields:
|
||||
- ID - key ID
|
||||
- Type - one of the three types described below
|
||||
- IssuerID - an ID of the Mainflux User who issued the key
|
||||
- Subject - user email
|
||||
- IssuedAt - the timestamp when the key is issued
|
||||
- ExpiresAt - the timestamp after which the key is invalid
|
||||
|
||||
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
|
||||
|
||||
Authentication keys are represented and distributed by the corresponding [JWT](jwt.io).
|
||||
|
||||
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user.
|
||||
|
||||
API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. This also means that, despite being used as a JWT, it requires a query to the database to validate the API key. The user with API key can perform all the same actions as the user with login key (can act on behalf of the user for Thing, Channel, or user profile management), *except issuing new API keys*.
|
||||
|
||||
Recovery key is the password recovery key. It's short-lived token used for password recovery process.
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of 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)
|
||||
- revoke (API keys only)
|
||||
|
||||
# Groups
|
||||
User and Things service are using Auth gRPC API to get the list of ids that are part of a group. Groups can be organized as tree structure.
|
||||
Group consists of the following fields:
|
||||
|
||||
- ID - ULID id uniquely representing group
|
||||
- Name - name of the group, name of the group is unique at the same level of tree hierarchy for a given tree.
|
||||
- ParentID - id of the parent group
|
||||
- OwnerID - id of the user that created a group
|
||||
- Description - free form text, up to 1024 characters
|
||||
- Metadata - Arbitrary, object-encoded group's data
|
||||
- Path - tree path consisting of group ids
|
||||
- CreatedAt - timestamp at which the group is created
|
||||
- UpdatedAt - timestamp at which the group is updated
|
||||
|
||||
## 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_AUTH_LOG_LEVEL | Service level (debug, info, warn, error) | error |
|
||||
| MF_AUTH_DB_HOST | Database host address | localhost |
|
||||
| MF_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| MF_AUTH_DB_USER | Database user | mainflux |
|
||||
| MF_AUTH_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_AUTH_DB | Name of the database used by the service | auth |
|
||||
| MF_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_AUTH_HTTP_PORT | Auth service HTTP port | 8180 |
|
||||
| MF_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| MF_AUTH_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTH_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTH_SECRET | String used for signing tokens | auth |
|
||||
| MF_AUTH_LOGIN_TOKEN_DURATION | The login token expiration period | 10h |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_KETO_READ_REMOTE_HOST | Keto Read Host | mainflux-keto |
|
||||
| MF_KETO_WRITE_REMOTE_HOST | Keto Write Host | mainflux-keto |
|
||||
| MF_KETO_READ_REMOTE_PORT | Keto Read Port | 4466 |
|
||||
| MF_KETO_WRITE_REMOTE_PORT | Keto Write Port | 4467 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`auth`](https://github.com/mainflux/mainflux/blob/master/docker/docker-compose.yml#L71-L94) service section in
|
||||
docker-compose to see how service is deployed.
|
||||
|
||||
|
||||
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 auth
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_AUTH_LOG_LEVEL=[Service log level] MF_AUTH_DB_HOST=[Database host address] MF_AUTH_DB_PORT=[Database host port] MF_AUTH_DB_USER=[Database user] MF_AUTH_DB_PASS=[Database password] MF_AUTH_DB=[Name of the database used by the service] MF_AUTH_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTH_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTH_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTH_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTH_HTTP_PORT=[Service HTTP port] MF_AUTH_GRPC_PORT=[Service gRPC port] MF_AUTH_SECRET=[String used for signing tokens] MF_AUTH_SERVER_CERT=[Path to server certificate] MF_AUTH_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] MF_AUTH_LOGIN_TOKEN_DURATION=[The login token expiration period] $GOBIN/mainflux-auth
|
||||
```
|
||||
|
||||
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](https://api.mainflux.io/?urls.primaryName=auth-openapi.yml).
|
||||
|
||||
[doc]: https://docs.mainflux.io
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of AuthN service HTTP API.
|
||||
// Package api contains implementation of Auth service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,334 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/mainflux/mainflux"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "mainflux.AuthService"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
addPolicy endpoint.Endpoint
|
||||
deletePolicy endpoint.Endpoint
|
||||
listPolicies endpoint.Endpoint
|
||||
assign endpoint.Endpoint
|
||||
members endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthServiceClient {
|
||||
return &grpcClient{
|
||||
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Issue",
|
||||
encodeIssueRequest,
|
||||
decodeIssueResponse,
|
||||
mainflux.UserIdentity{},
|
||||
).Endpoint()),
|
||||
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Identify",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
mainflux.UserIdentity{},
|
||||
).Endpoint()),
|
||||
authorize: kitot.TraceClient(tracer, "authorize")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Authorize",
|
||||
encodeAuthorizeRequest,
|
||||
decodeAuthorizeResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
).Endpoint()),
|
||||
addPolicy: kitot.TraceClient(tracer, "add_policy")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"AddPolicy",
|
||||
encodeAddPolicyRequest,
|
||||
decodeAddPolicyResponse,
|
||||
mainflux.AddPolicyRes{},
|
||||
).Endpoint()),
|
||||
deletePolicy: kitot.TraceClient(tracer, "delete_policy")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"DeletePolicy",
|
||||
encodeDeletePolicyRequest,
|
||||
decodeDeletePolicyResponse,
|
||||
mainflux.DeletePolicyRes{},
|
||||
).Endpoint()),
|
||||
listPolicies: kitot.TraceClient(tracer, "list_policies")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"ListPolicies",
|
||||
encodeListPoliciesRequest,
|
||||
decodeListPoliciesResponse,
|
||||
mainflux.ListPoliciesRes{},
|
||||
).Endpoint()),
|
||||
assign: kitot.TraceClient(tracer, "assign")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Assign",
|
||||
encodeAssignRequest,
|
||||
decodeAssignResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
).Endpoint()),
|
||||
members: kitot.TraceClient(tracer, "members")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Members",
|
||||
encodeMembersRequest,
|
||||
decodeMembersResponse,
|
||||
mainflux.MembersRes{},
|
||||
).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{id: req.GetId(), email: req.GetEmail(), keyType: req.Type})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.Token{Value: ir.id}, nil
|
||||
}
|
||||
|
||||
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &mainflux.IssueReq{Id: req.id, Email: req.email, Type: req.keyType}, nil
|
||||
}
|
||||
|
||||
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserIdentity)
|
||||
return identityRes{id: res.GetId(), email: res.GetEmail()}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserIdentity, 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.UserIdentity{Id: ir.id, Email: ir.email}, nil
|
||||
}
|
||||
|
||||
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.UserIdentity)
|
||||
return identityRes{id: res.GetId(), email: res.GetEmail()}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.authorize(ctx, authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.AuthorizeRes{}, err
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
return &mainflux.AuthorizeRes{Authorized: ar.authorized}, err
|
||||
}
|
||||
|
||||
func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AuthorizeRes)
|
||||
return authorizeRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &mainflux.AuthorizeReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) AddPolicy(ctx context.Context, in *mainflux.AddPolicyReq, opts ...grpc.CallOption) (*mainflux.AddPolicyRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.addPolicy(ctx, policyReq{Act: in.GetAct(), Obj: in.GetObj(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.AddPolicyRes{}, err
|
||||
}
|
||||
|
||||
apr := res.(addPolicyRes)
|
||||
return &mainflux.AddPolicyRes{Authorized: apr.authorized}, err
|
||||
}
|
||||
|
||||
func decodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AddPolicyRes)
|
||||
return addPolicyRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAddPolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(policyReq)
|
||||
return &mainflux.AddPolicyReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) DeletePolicy(ctx context.Context, in *mainflux.DeletePolicyReq, opts ...grpc.CallOption) (*mainflux.DeletePolicyRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.deletePolicy(ctx, policyReq{Act: in.GetAct(), Obj: in.GetObj(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.DeletePolicyRes{}, err
|
||||
}
|
||||
|
||||
dpr := res.(deletePolicyRes)
|
||||
return &mainflux.DeletePolicyRes{Deleted: dpr.deleted}, err
|
||||
}
|
||||
|
||||
func decodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.DeletePolicyRes)
|
||||
return deletePolicyRes{deleted: res.GetDeleted()}, nil
|
||||
}
|
||||
|
||||
func encodeDeletePolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(policyReq)
|
||||
return &mainflux.DeletePolicyReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) ListPolicies(ctx context.Context, in *mainflux.ListPoliciesReq, opts ...grpc.CallOption) (*mainflux.ListPoliciesRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.listPolicies(ctx, listPoliciesReq{Obj: in.GetObj(), Act: in.GetAct(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.ListPoliciesRes{}, err
|
||||
}
|
||||
|
||||
lpr := res.(listPoliciesRes)
|
||||
return &mainflux.ListPoliciesRes{Policies: lpr.policies}, err
|
||||
}
|
||||
|
||||
func decodeListPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.ListPoliciesRes)
|
||||
return listPoliciesRes{policies: res.GetPolicies()}, nil
|
||||
}
|
||||
|
||||
func encodeListPoliciesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(listPoliciesReq)
|
||||
return &mainflux.ListPoliciesReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.members(ctx, membersReq{
|
||||
token: req.GetToken(),
|
||||
groupID: req.GetGroupID(),
|
||||
memberType: req.GetType(),
|
||||
offset: req.GetOffset(),
|
||||
limit: req.GetLimit(),
|
||||
})
|
||||
if err != nil {
|
||||
return &mainflux.MembersRes{}, err
|
||||
}
|
||||
|
||||
mr := res.(membersRes)
|
||||
|
||||
return &mainflux.MembersRes{
|
||||
Offset: mr.offset,
|
||||
Limit: mr.limit,
|
||||
Total: mr.total,
|
||||
Type: mr.groupType,
|
||||
Members: mr.members,
|
||||
}, err
|
||||
}
|
||||
|
||||
func encodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(membersReq)
|
||||
return &mainflux.MembersReq{
|
||||
Token: req.token,
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
GroupID: req.groupID,
|
||||
Type: req.memberType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.MembersRes)
|
||||
return membersRes{
|
||||
offset: res.Offset,
|
||||
limit: res.Limit,
|
||||
total: res.Total,
|
||||
members: res.Members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Assign(ctx context.Context, req *mainflux.Assignment, _ ...grpc.CallOption) (r *empty.Empty, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
_, err = client.assign(ctx, assignReq{token: req.GetToken(), groupID: req.GetGroupID(), memberID: req.GetMemberID()})
|
||||
if err != nil {
|
||||
return &empty.Empty{}, err
|
||||
}
|
||||
|
||||
return &empty.Empty{}, err
|
||||
}
|
||||
|
||||
func encodeAssignRequest(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AuthorizeRes)
|
||||
return authorizeRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAssignResponse(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &mainflux.AuthorizeReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of Auth service gRPC API.
|
||||
package grpc
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
|
||||
key := auth.Key{
|
||||
Type: req.keyType,
|
||||
Subject: req.email,
|
||||
IssuerID: req.id,
|
||||
IssuedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
_, secret, err := svc.Issue(ctx, "", key)
|
||||
if err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
|
||||
return issueRes{secret}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identifyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
id, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
ret := identityRes{
|
||||
id: id.ID,
|
||||
email: id.Email,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(authReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
err := svc.Authorize(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
return authorizeRes{authorized: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
func addPolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return addPolicyRes{}, err
|
||||
}
|
||||
|
||||
err := svc.AddPolicy(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return addPolicyRes{}, err
|
||||
}
|
||||
return addPolicyRes{authorized: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
func deletePolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
|
||||
err := svc.DeletePolicy(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
return deletePolicyRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listPoliciesEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listPoliciesReq)
|
||||
|
||||
page, err := svc.ListPolicies(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
return listPoliciesRes{policies: page.Policies}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(assignReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
|
||||
_, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
|
||||
err = svc.Assign(ctx, req.token, req.memberID, req.groupID, req.groupType)
|
||||
if err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
return emptyRes{}, nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func membersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(membersReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
}
|
||||
mp, err := svc.ListMembers(ctx, req.token, req.groupID, req.memberType, pm)
|
||||
if err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
var members []string
|
||||
for _, m := range mp.Members {
|
||||
members = append(members, m.ID)
|
||||
}
|
||||
return membersRes{
|
||||
offset: req.offset,
|
||||
limit: req.limit,
|
||||
total: mp.PageMetadata.Total,
|
||||
members: members,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
grpcapi "github.com/mainflux/mainflux/auth/api/grpc"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
thingsType = "things"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
|
||||
numOfThings = 5
|
||||
numOfUsers = 5
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
var svc auth.Service
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: authoritiesObj, Relation: memberRelation})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
t := jwt.New(secret)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterAuthServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
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
|
||||
email string
|
||||
kind uint32
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.LoginKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.RecoveryKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthenticated",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.APIKey,
|
||||
err: nil,
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: 32,
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
email: "",
|
||||
kind: auth.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Id: tc.id, Email: tc.email, 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) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, recoverySecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
|
||||
|
||||
_, apiSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute), IssuerID: id, Subject: email})
|
||||
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
|
||||
idt mainflux.UserIdentity
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "identify user with user token",
|
||||
token: loginSecret,
|
||||
idt: mainflux.UserIdentity{Email: email, Id: id},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with recovery token",
|
||||
token: recoverySecret,
|
||||
idt: mainflux.UserIdentity{Email: email, Id: id},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with API token",
|
||||
token: apiSecret,
|
||||
idt: mainflux.UserIdentity{Email: email, Id: id},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
idt: mainflux.UserIdentity{},
|
||||
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user with empty token",
|
||||
token: "",
|
||||
idt: mainflux.UserIdentity{},
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
idt, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, *idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, *idt))
|
||||
}
|
||||
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 TestAuthorize(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
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
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
ar mainflux.AuthorizeRes
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: authoritiesObj,
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AuthorizeRes{Authorized: true},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized relation",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: authoritiesObj,
|
||||
relation: "unauthorizedRelation",
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized object",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: "unauthorizedobject",
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized subject",
|
||||
token: loginSecret,
|
||||
subject: "unauthorizedSubject",
|
||||
object: authoritiesObj,
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with invalid ACL",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ar, err := client.Authorize(context.Background(), &mainflux.AuthorizeReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.ar, *ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.ar, *ar))
|
||||
}
|
||||
|
||||
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 TestAddPolicy(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
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)
|
||||
|
||||
groupAdminObj := "groupadmin"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
ar mainflux.AddPolicyRes
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "add groupadmin policy to user",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: groupAdminObj,
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AddPolicyRes{Authorized: true},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "add policy to user with invalid ACL",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
ar: mainflux.AddPolicyRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
apr, err := client.AddPolicy(context.Background(), &mainflux.AddPolicyReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
if apr != nil {
|
||||
assert.Equal(t, tc.ar, *apr, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.ar, *apr))
|
||||
}
|
||||
|
||||
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 TestDeletePolicy(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
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)
|
||||
|
||||
readRelation := "read"
|
||||
thingID := "thing"
|
||||
|
||||
apr, err := client.AddPolicy(context.Background(), &mainflux.AddPolicyReq{Sub: id, Obj: thingID, Act: readRelation})
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding read policy to user expected to succeed: %s", err))
|
||||
assert.True(t, apr.GetAuthorized(), fmt.Sprintf("Adding read policy expected to make user authorized, expected %v got %v", true, apr.GetAuthorized()))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
dpr *mainflux.DeletePolicyRes
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "delete valid policy",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: thingID,
|
||||
relation: readRelation,
|
||||
dpr: &mainflux.DeletePolicyRes{Deleted: true},
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "delete invalid policy",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
dpr: &mainflux.DeletePolicyRes{Deleted: false},
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
dpr, err := client.DeletePolicy(context.Background(), &mainflux.DeletePolicyReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
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()))
|
||||
assert.Equal(t, tc.dpr.GetDeleted(), dpr.GetDeleted(), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.dpr.GetDeleted(), dpr.GetDeleted()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMembers(t *testing.T) {
|
||||
_, token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Name: "Mainflux",
|
||||
Description: description,
|
||||
}
|
||||
|
||||
var things []string
|
||||
for i := 0; i < numOfThings; i++ {
|
||||
thID, err := uuid.New().ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
|
||||
|
||||
err = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: thID, Relation: "owner"})
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding a policy expected to succeed: %s", err))
|
||||
|
||||
things = append(things, thID)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for i := 0; i < numOfUsers; i++ {
|
||||
id, err := uuid.New().ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
|
||||
|
||||
users = append(users, id)
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), token, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
|
||||
err = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: group.ID, Relation: "groupadmin"})
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding a policy expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, thingsType, things...)
|
||||
assert.Nil(t, err, fmt.Sprintf("Assign members to expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, usersType, users...)
|
||||
assert.Nil(t, err, fmt.Sprintf("Assign members to group expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
groupID string
|
||||
groupType string
|
||||
size int
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "get all things with user token",
|
||||
groupID: group.ID,
|
||||
token: token,
|
||||
groupType: thingsType,
|
||||
size: numOfThings,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "get all users with user token",
|
||||
groupID: group.ID,
|
||||
token: token,
|
||||
groupType: usersType,
|
||||
size: numOfUsers,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
}
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
for _, tc := range cases {
|
||||
m, err := client.Members(context.Background(), &mainflux.MembersReq{Token: tc.token, GroupID: tc.groupID, Type: tc.groupType, Offset: 0, Limit: 10})
|
||||
e, ok := status.FromError(err)
|
||||
assert.Equal(t, tc.size, len(m.Members), fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.size, len(m.Members)))
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
assert.True(t, ok, "OK expected to be true")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
kind uint32
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.kind != auth.LoginKey &&
|
||||
req.kind != auth.APIKey &&
|
||||
req.kind != auth.RecoveryKey {
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type issueReq struct {
|
||||
id string
|
||||
email string
|
||||
keyType uint32
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.email == "" {
|
||||
return apiutil.ErrMissingEmail
|
||||
}
|
||||
if req.keyType != auth.LoginKey &&
|
||||
req.keyType != auth.APIKey &&
|
||||
req.keyType != auth.RecoveryKey {
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type assignReq struct {
|
||||
token string
|
||||
groupID string
|
||||
memberID string
|
||||
groupType string
|
||||
}
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.groupID == "" || req.memberID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type membersReq struct {
|
||||
token string
|
||||
groupID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
memberType string
|
||||
}
|
||||
|
||||
func (req membersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.memberType == "" {
|
||||
return apiutil.ErrMissingMemberType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authReq represents authorization request. It contains:
|
||||
// 1. subject - an action invoker
|
||||
// 2. object - an entity over which action will be executed
|
||||
// 3. action - type of action that will be executed (read/write)
|
||||
type authReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
func (req authReq) validate() error {
|
||||
if req.Sub == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Obj == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Act == "" {
|
||||
return apiutil.ErrMissingPolicyAct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type policyReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
func (req policyReq) validate() error {
|
||||
if req.Sub == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Obj == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Act == "" {
|
||||
return apiutil.ErrMissingPolicyAct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listPoliciesReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
email string
|
||||
}
|
||||
|
||||
type issueRes struct {
|
||||
value string
|
||||
}
|
||||
|
||||
type authorizeRes struct {
|
||||
authorized bool
|
||||
}
|
||||
|
||||
type addPolicyRes struct {
|
||||
authorized bool
|
||||
}
|
||||
|
||||
type deletePolicyRes struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
type listPoliciesRes struct {
|
||||
policies []string
|
||||
}
|
||||
|
||||
type membersRes struct {
|
||||
total uint64
|
||||
offset uint64
|
||||
limit uint64
|
||||
groupType string
|
||||
members []string
|
||||
}
|
||||
type emptyRes struct {
|
||||
err error
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
issue kitgrpc.Handler
|
||||
identify kitgrpc.Handler
|
||||
authorize kitgrpc.Handler
|
||||
addPolicy kitgrpc.Handler
|
||||
deletePolicy kitgrpc.Handler
|
||||
listPolicies kitgrpc.Handler
|
||||
assign kitgrpc.Handler
|
||||
members kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new AuthServiceServer instance.
|
||||
func NewServer(tracer opentracing.Tracer, svc auth.Service) mainflux.AuthServiceServer {
|
||||
return &grpcServer{
|
||||
issue: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssueRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
identify: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
|
||||
decodeIdentifyRequest,
|
||||
encodeIdentifyResponse,
|
||||
),
|
||||
authorize: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "authorize")(authorizeEndpoint(svc)),
|
||||
decodeAuthorizeRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
addPolicy: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "add_policy")(addPolicyEndpoint(svc)),
|
||||
decodeAddPolicyRequest,
|
||||
encodeAddPolicyResponse,
|
||||
),
|
||||
deletePolicy: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_policy")(deletePolicyEndpoint(svc)),
|
||||
decodeDeletePolicyRequest,
|
||||
encodeDeletePolicyResponse,
|
||||
),
|
||||
listPolicies: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "list_policies")(listPoliciesEndpoint(svc)),
|
||||
decodeListPoliciesRequest,
|
||||
encodeListPoliciesResponse,
|
||||
),
|
||||
assign: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
encodeEmptyResponse,
|
||||
),
|
||||
members: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "members")(membersEndpoint(svc)),
|
||||
decodeMembersRequest,
|
||||
encodeMembersResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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.UserIdentity, error) {
|
||||
_, res, err := s.identify.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.UserIdentity), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Authorize(ctx context.Context, req *mainflux.AuthorizeReq) (*mainflux.AuthorizeRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.AuthorizeRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) AddPolicy(ctx context.Context, req *mainflux.AddPolicyReq) (*mainflux.AddPolicyRes, error) {
|
||||
_, res, err := s.addPolicy.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.AddPolicyRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) DeletePolicy(ctx context.Context, req *mainflux.DeletePolicyReq) (*mainflux.DeletePolicyRes, error) {
|
||||
_, res, err := s.deletePolicy.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.DeletePolicyRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) ListPolicies(ctx context.Context, req *mainflux.ListPoliciesReq) (*mainflux.ListPoliciesRes, error) {
|
||||
_, res, err := s.listPolicies.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.ListPoliciesRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Assign(ctx context.Context, token *mainflux.Assignment) (*empty.Empty, error) {
|
||||
_, res, err := s.assign.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*empty.Empty), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Members(ctx context.Context, req *mainflux.MembersReq) (*mainflux.MembersRes, error) {
|
||||
_, res, err := s.members.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.MembersRes), nil
|
||||
}
|
||||
|
||||
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.IssueReq)
|
||||
return issueReq{id: req.GetId(), email: req.GetEmail(), keyType: req.GetType()}, nil
|
||||
}
|
||||
|
||||
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(issueRes)
|
||||
return &mainflux.Token{Value: res.value}, nil
|
||||
}
|
||||
|
||||
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.UserIdentity{Id: res.id, Email: res.email}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AuthorizeReq)
|
||||
return authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &mainflux.AuthorizeRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAddPolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AddPolicyReq)
|
||||
return policyReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(addPolicyRes)
|
||||
return &mainflux.AddPolicyRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.Token)
|
||||
return assignReq{token: req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func decodeDeletePolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.DeletePolicyReq)
|
||||
return policyReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(deletePolicyRes)
|
||||
return &mainflux.DeletePolicyRes{Deleted: res.deleted}, nil
|
||||
}
|
||||
|
||||
func decodeListPoliciesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.ListPoliciesReq)
|
||||
return listPoliciesReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeListPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(listPoliciesRes)
|
||||
return &mainflux.ListPoliciesRes{Policies: res.policies}, nil
|
||||
}
|
||||
|
||||
func decodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.MembersReq)
|
||||
return membersReq{
|
||||
token: req.GetToken(),
|
||||
groupID: req.GetGroupID(),
|
||||
memberType: req.GetType(),
|
||||
offset: req.Offset,
|
||||
limit: req.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(membersRes)
|
||||
return &mainflux.MembersRes{
|
||||
Total: res.total,
|
||||
Offset: res.offset,
|
||||
Limit: res.limit,
|
||||
Type: res.groupType,
|
||||
Members: res.members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeEmptyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(emptyRes)
|
||||
return &empty.Empty{}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrInvalidAuthKey,
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMissingPolicyAct:
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
errors.Contains(err, auth.ErrKeyExpired),
|
||||
err == apiutil.ErrMissingEmail,
|
||||
err == apiutil.ErrBearerToken:
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package http contains implementation of users service HTTP API.
|
||||
package http
|
||||
@@ -0,0 +1,370 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func createGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
group := auth.Group{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
ParentID: req.ParentID,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
group, err := svc.CreateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
return groupRes{created: true, id: group.ID}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
|
||||
group, err := svc.ViewGroup(ctx, req.token, req.id)
|
||||
if err != nil {
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
|
||||
res := viewGroupRes{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
group := auth.Group{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
_, err := svc.UpdateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
res := groupRes{created: false}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.RemoveGroup(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deleteRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listGroupsEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListGroups(ctx, req.token, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMemberships(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMembershipsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
|
||||
page, err := svc.ListMemberships(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func shareGroupAccessEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(shareGroupAccessReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return shareGroupRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.AssignGroupAccessRights(ctx, req.token, req.ThingGroupID, req.userGroupID); err != nil {
|
||||
return shareGroupRes{}, err
|
||||
}
|
||||
return shareGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listChildrenEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListChildren(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func listParentsEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
|
||||
page, err := svc.ListParents(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(assignReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Assign(ctx, req.token, req.groupID, req.Type, req.Members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assignRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func unassignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(unassignReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Unassign(ctx, req.token, req.groupID, req.Members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unassignRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMembersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMembersReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListMembers(ctx, req.token, req.id, req.groupType, pm)
|
||||
if err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
return buildUsersResponse(page, req.groupType), nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildGroupsResponseTree(page auth.GroupPage) groupPageRes {
|
||||
groupsMap := map[string]*auth.Group{}
|
||||
// Parents' map keeps its array of children.
|
||||
parentsMap := map[string][]*auth.Group{}
|
||||
for i := range page.Groups {
|
||||
if _, ok := groupsMap[page.Groups[i].ID]; !ok {
|
||||
groupsMap[page.Groups[i].ID] = &page.Groups[i]
|
||||
parentsMap[page.Groups[i].ID] = make([]*auth.Group, 0)
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
if children, ok := parentsMap[group.ParentID]; ok {
|
||||
children = append(children, group)
|
||||
parentsMap[group.ParentID] = children
|
||||
}
|
||||
}
|
||||
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Limit: page.Limit,
|
||||
Offset: page.Offset,
|
||||
Total: page.Total,
|
||||
Level: page.Level,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
if children, ok := parentsMap[group.ID]; ok {
|
||||
group.Children = children
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
view := toViewGroupRes(*group)
|
||||
if children, ok := parentsMap[group.ParentID]; len(children) == 0 || !ok {
|
||||
res.Groups = append(res.Groups, view)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func toViewGroupRes(group auth.Group) viewGroupRes {
|
||||
view := viewGroupRes{
|
||||
ID: group.ID,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
Level: group.Level,
|
||||
Path: group.Path,
|
||||
Children: make([]*viewGroupRes, 0),
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
|
||||
for _, ch := range group.Children {
|
||||
child := toViewGroupRes(*ch)
|
||||
view.Children = append(view.Children, &child)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func buildGroupsResponse(gp auth.GroupPage) groupPageRes {
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: gp.Total,
|
||||
Level: gp.Level,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
|
||||
for _, group := range gp.Groups {
|
||||
view := viewGroupRes{
|
||||
ID: group.ID,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
Level: group.Level,
|
||||
Path: group.Path,
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
res.Groups = append(res.Groups, view)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func buildUsersResponse(mp auth.MemberPage, groupType string) memberPageRes {
|
||||
res := memberPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: mp.Total,
|
||||
Offset: mp.Offset,
|
||||
Limit: mp.Limit,
|
||||
Name: mp.Name,
|
||||
},
|
||||
Type: groupType,
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
for _, m := range mp.Members {
|
||||
res.Members = append(res.Members, m.ID)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package groups_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
email = "user@example.com"
|
||||
secret = "secret"
|
||||
id = "testID"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
keys := mocks.NewKeyRepository()
|
||||
groups := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
policies := mocks.NewKetoMock(map[string][]mocks.MockSubjectSet{})
|
||||
return auth.New(keys, groups, idProvider, t, policies, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestShareGroupAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
type shareGroupAccessReq struct {
|
||||
token string
|
||||
userGroupID string
|
||||
ThingGroupID string `json:"thing_group_id"`
|
||||
}
|
||||
data := shareGroupAccessReq{token: apiToken, userGroupID: "ug", ThingGroupID: "tg"}
|
||||
invalidData := shareGroupAccessReq{token: apiToken, userGroupID: "ug", ThingGroupID: ""}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
userGroupID string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "share a user group with thing group",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with invalid thing group",
|
||||
req: toJSON(invalidData),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share an invalid user group with thing group",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share an invalid user group with invalid thing group",
|
||||
req: toJSON(invalidData),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with thing group with invalid content type",
|
||||
req: toJSON(data),
|
||||
contentType: "",
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with thing group with invalid token",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: "token",
|
||||
userGroupID: "ug",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/groups/%s/share", ts.URL, tc.userGroupID),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type createGroupReq struct {
|
||||
token string
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req createGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if len(req.Name) > maxNameSize || req.Name == "" {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateGroupReq struct {
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listGroupsReq struct {
|
||||
token string
|
||||
id string
|
||||
level uint64
|
||||
// - `true` - result is JSON tree representing groups hierarchy,
|
||||
// - `false` - result is JSON array of groups.
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listGroupsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.level > auth.MaxLevel || req.level < auth.MinLevel {
|
||||
return apiutil.ErrMaxLevelExceeded
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembersReq struct {
|
||||
token string
|
||||
id string
|
||||
groupType string
|
||||
offset uint64
|
||||
limit uint64
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembershipsReq struct {
|
||||
token string
|
||||
id string
|
||||
offset uint64
|
||||
limit uint64
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembershipsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type assignReq struct {
|
||||
token string
|
||||
groupID string
|
||||
Type string `json:"type,omitempty"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Type == "" {
|
||||
return apiutil.ErrMissingMemberType
|
||||
}
|
||||
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.Members) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type shareGroupAccessReq struct {
|
||||
token string
|
||||
userGroupID string
|
||||
ThingGroupID string `json:"thing_group_id"`
|
||||
}
|
||||
|
||||
func (req shareGroupAccessReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ThingGroupID == "" || req.userGroupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type unassignReq struct {
|
||||
assignReq
|
||||
}
|
||||
|
||||
func (req unassignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.Members) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type groupReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req groupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*memberPageRes)(nil)
|
||||
_ mainflux.Response = (*groupRes)(nil)
|
||||
_ mainflux.Response = (*deleteRes)(nil)
|
||||
_ mainflux.Response = (*assignRes)(nil)
|
||||
_ mainflux.Response = (*unassignRes)(nil)
|
||||
)
|
||||
|
||||
type memberPageRes struct {
|
||||
pageRes
|
||||
Type string `json:"type"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (res memberPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res memberPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res memberPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type shareGroupRes struct {
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type viewGroupRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
// Indicates a level in tree hierarchy from first group node - root.
|
||||
Level int `json:"level"`
|
||||
// Path in a tree consisting of group ids
|
||||
// parentID1.parentID2.childID1
|
||||
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
|
||||
Path string `json:"path"`
|
||||
Children []*viewGroupRes `json:"children,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (res viewGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewGroupRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type groupRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res groupRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res groupRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/groups/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type groupPageRes struct {
|
||||
pageRes
|
||||
Groups []viewGroupRes `json:"groups"`
|
||||
}
|
||||
|
||||
type pageRes struct {
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
Level uint64 `json:"level"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (res groupPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res groupPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type deleteRes struct{}
|
||||
|
||||
func (res deleteRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res deleteRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deleteRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type assignRes struct{}
|
||||
|
||||
func (res assignRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res assignRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res assignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type unassignRes struct{}
|
||||
|
||||
func (res unassignRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res unassignRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res unassignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxNameSize = 254
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
levelKey = "level"
|
||||
metadataKey = "metadata"
|
||||
treeKey = "tree"
|
||||
groupType = "type"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
defLevel = 1
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
mux.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_group")(createGroupEndpoint(svc)),
|
||||
decodeGroupCreate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "view_group")(viewGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "update_group")(updateGroupEndpoint(svc)),
|
||||
decodeGroupUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_group")(deleteGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:subjectGroupID/share", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "share_group_access")(shareGroupAccessEndpoint(svc)),
|
||||
decodeShareGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(listGroupsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/children", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_children")(listChildrenEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/parents", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_parents_groups")(listParentsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "unassign")(unassignEndpoint(svc)),
|
||||
decodeUnassignRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_members")(listMembersEndpoint(svc)),
|
||||
decodeListMembersRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/members/:memberID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_memberships")(listMemberships(svc)),
|
||||
decodeListMembershipsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeShareGroupRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := shareGroupAccessReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
userGroupID: bone.GetValue(r, "subjectGroupID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := apiutil.ReadUintQuery(r, levelKey, defLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := apiutil.ReadBoolQuery(r, treeKey, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listGroupsReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
level: l,
|
||||
metadata: m,
|
||||
tree: t,
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := apiutil.ReadBoolQuery(r, treeKey, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := apiutil.ReadStringQuery(r, groupType, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembersReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
groupType: t,
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
tree: tree,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembershipsReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "memberID"),
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := createGroupReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateGroupReq{
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := groupReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := assignReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUnassignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := unassignReq{
|
||||
assignReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
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, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrEmptyList,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrNameSize:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrAuthentication):
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
|
||||
case errors.Contains(err, errors.ErrCreateEntity),
|
||||
errors.Contains(err, errors.ErrUpdateEntity),
|
||||
errors.Contains(err, errors.ErrViewEntity),
|
||||
errors.Contains(err, errors.ErrRemoveEntity):
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueKeyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
@@ -19,8 +19,7 @@ func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
newKey := authn.Key{
|
||||
Issuer: req.issuer,
|
||||
newKey := auth.Key{
|
||||
IssuedAt: now,
|
||||
Type: req.Type,
|
||||
}
|
||||
@@ -31,14 +30,14 @@ func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
newKey.ExpiresAt = exp
|
||||
}
|
||||
|
||||
key, err := svc.Issue(ctx, req.issuer, newKey)
|
||||
key, secret, err := svc.Issue(ctx, req.token, newKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := issueKeyRes{
|
||||
ID: key.ID,
|
||||
Value: key.Secret,
|
||||
Value: secret,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
@@ -48,7 +47,7 @@ func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func revokeEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
func retrieveEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(keyReq)
|
||||
|
||||
@@ -56,28 +55,38 @@ func revokeEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Revoke(ctx, req.issuer, req.id); err != nil {
|
||||
key, err := svc.RetrieveKey(ctx, req.token, req.id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := retrieveKeyRes{
|
||||
ID: key.ID,
|
||||
IssuerID: key.IssuerID,
|
||||
Subject: key.Subject,
|
||||
Type: key.Type,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
ret.ExpiresAt = &key.ExpiresAt
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(keyReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Revoke(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return revokeKeyRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(keyReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := svc.Retrieve(ctx, req.issuer, req.id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http_test
|
||||
package keys_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,22 +14,23 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
authn "github.com/mainflux/mainflux/authn"
|
||||
httpapi "github.com/mainflux/mainflux/authn/api/http"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type issueRequest struct {
|
||||
@@ -52,7 +53,7 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
@@ -62,15 +63,22 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() authn.Service {
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
uuidProvider := uuid.NewMock()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: "authorities", Relation: "member"})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc authn.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New())
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -81,16 +89,16 @@ func toJSON(data interface{}) string {
|
||||
|
||||
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))
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login 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}
|
||||
lk := issueRequest{Type: auth.LoginKey}
|
||||
ak := issueRequest{Type: auth.APIKey, Duration: time.Hour}
|
||||
rk := issueRequest{Type: auth.RecoveryKey}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -100,72 +108,73 @@ func TestIssue(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "issue user key",
|
||||
req: toJSON(uk),
|
||||
desc: "issue login key with empty token",
|
||||
req: toJSON(lk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusCreated,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: userKey.Secret,
|
||||
token: loginSecret,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusBadRequest,
|
||||
token: loginSecret,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue user key wrong content type",
|
||||
req: toJSON(uk),
|
||||
ct: "", token: userKey.Secret,
|
||||
desc: "issue login key wrong content type",
|
||||
req: toJSON(lk),
|
||||
ct: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key wrong content type",
|
||||
desc: "issue recovery key wrong content type",
|
||||
req: toJSON(rk),
|
||||
ct: "",
|
||||
token: userKey.Secret,
|
||||
token: loginSecret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key unauthorized",
|
||||
desc: "issue key with an invalid token",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key with empty token",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusBadRequest,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid request",
|
||||
req: "{",
|
||||
ct: contentType,
|
||||
token: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON",
|
||||
req: "{invalid}",
|
||||
ct: contentType,
|
||||
token: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON content",
|
||||
req: `{"Type":{"key":"value"}}`,
|
||||
ct: contentType,
|
||||
token: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
@@ -187,11 +196,11 @@ func TestIssue(t *testing.T) {
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
|
||||
|
||||
k, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
k, _, err := svc.Issue(context.Background(), loginSecret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
@@ -207,20 +216,20 @@ func TestRetrieve(t *testing.T) {
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: k.ID,
|
||||
token: loginKey.Secret,
|
||||
token: loginSecret,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing key",
|
||||
id: "non-existing",
|
||||
token: loginKey.Secret,
|
||||
token: loginSecret,
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key unauthorized",
|
||||
desc: "retrieve a key with an invalid token",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -239,12 +248,12 @@ func TestRetrieve(t *testing.T) {
|
||||
|
||||
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()}
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
|
||||
|
||||
k, err := svc.Issue(context.Background(), userKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
k, _, err := svc.Issue(context.Background(), loginSecret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
@@ -259,20 +268,20 @@ func TestRevoke(t *testing.T) {
|
||||
{
|
||||
desc: "revoke an existing key",
|
||||
id: k.ID,
|
||||
token: userKey.Secret,
|
||||
token: loginSecret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a non-existing key",
|
||||
id: "non-existing",
|
||||
token: userKey.Secret,
|
||||
token: loginSecret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a key unauthorized",
|
||||
desc: "revoke key with invalid token",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden},
|
||||
status: http.StatusUnauthorized},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type issueKeyReq struct {
|
||||
token 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.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Type != auth.LoginKey &&
|
||||
req.Type != auth.RecoveryKey &&
|
||||
req.Type != auth.APIKey {
|
||||
return apiutil.ErrInvalidAPIKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type keyReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req keyReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
package keys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -34,6 +34,27 @@ func (res issueKeyRes) Empty() bool {
|
||||
return res.Value == ""
|
||||
}
|
||||
|
||||
type retrieveKeyRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
IssuerID string `json:"issuer_id,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Type uint32 `json:"type,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (res retrieveKeyRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res retrieveKeyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res retrieveKeyRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type revokeKeyRes struct {
|
||||
}
|
||||
|
||||
@@ -48,7 +69,3 @@ func (res revokeKeyRes) Headers() map[string]string {
|
||||
func (res revokeKeyRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -14,24 +13,20 @@ import (
|
||||
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/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
var errUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc authn.Service, tracer opentracing.Tracer) http.Handler {
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
|
||||
mux := bone.New()
|
||||
|
||||
mux.Post("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssue,
|
||||
@@ -53,21 +48,17 @@ func MakeHandler(svc authn.Service, tracer opentracing.Tracer) http.Handler {
|
||||
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"),
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := issueKeyReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(authn.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -75,8 +66,8 @@ func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
issuer: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -101,27 +92,26 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, authn.ErrMalformedEntity):
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrInvalidAPIKey:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, authn.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, authn.ErrNotFound):
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, authn.ErrConflict):
|
||||
case errors.Contains(err, errors.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):
|
||||
case errors.Contains(err, errors.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)
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func createPolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policiesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return createPolicyRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.AddPolicies(ctx, req.token, req.Object, req.SubjectIDs, req.Policies); err != nil {
|
||||
return createPolicyRes{}, err
|
||||
}
|
||||
|
||||
return createPolicyRes{created: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deletePoliciesEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policiesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deletePoliciesRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.DeletePolicies(ctx, req.token, req.Object, req.SubjectIDs, req.Policies); err != nil {
|
||||
return deletePoliciesRes{}, err
|
||||
}
|
||||
|
||||
return deletePoliciesRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package policies_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = uuid.Prefix + "-000000000001"
|
||||
email = "user@example.com"
|
||||
unauthzID = uuid.Prefix + "-000000000002"
|
||||
unauthzEmail = "unauthz@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
|
||||
req.Header.Set("Referer", "http://localhost")
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: "authorities", Relation: "member"})
|
||||
mockAuthzDB[unauthzID] = append(mockAuthzDB[unauthzID], mocks.MockSubjectSet{Object: "users", Relation: "member"})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
type addPolicyRequest struct {
|
||||
SubjectIDs []string `json:"subjects"`
|
||||
Policies []string `json:"policies"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
func TestAddPolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, userLoginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: unauthzID, Subject: unauthzEmail})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing unauthorized user's key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
valid := addPolicyRequest{Object: "obj", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
multipleValid := addPolicyRequest{Object: "obj", Policies: []string{"write", "delete"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidObject := addPolicyRequest{Object: "", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidPolicies := addPolicyRequest{Object: "obj", Policies: []string{"read", "invalid"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidSubjects := addPolicyRequest{Object: "obj", Policies: []string{"read", "access"}, SubjectIDs: []string{"", "user2"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ct string
|
||||
status int
|
||||
req string
|
||||
}{
|
||||
{
|
||||
desc: "Add policies with authorized access",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusCreated,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add multiple policies to multiple user",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusCreated,
|
||||
req: toJSON(multipleValid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with unauthorized access",
|
||||
token: userLoginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusForbidden,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid token",
|
||||
token: "invalid",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty token",
|
||||
token: "",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid content type",
|
||||
token: loginSecret,
|
||||
ct: "text/html",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty content type",
|
||||
token: loginSecret,
|
||||
ct: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid object field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidObject),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid policies field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidPolicies),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid subjects field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidSubjects),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/policies", 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 TestDeletePolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, userLoginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: unauthzID, Subject: unauthzEmail})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing unauthorized user's key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
policies := addPolicyRequest{Object: "obj", Policies: []string{"read", "write", "delete"}, SubjectIDs: []string{"user1", "user2", "user3"}}
|
||||
err = svc.AddPolicies(context.Background(), loginSecret, policies.Object, policies.SubjectIDs, policies.Policies)
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding policies expected to succeed: %s", err))
|
||||
|
||||
validSingleDeleteReq := addPolicyRequest{Object: "obj", Policies: []string{"read"}, SubjectIDs: []string{"user1"}}
|
||||
validMultipleDeleteReq := addPolicyRequest{Object: "obj", Policies: []string{"write", "delete"}, SubjectIDs: []string{"user2", "user3"}}
|
||||
invalidObject := addPolicyRequest{Object: "", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidPolicies := addPolicyRequest{Object: "obj", Policies: []string{"read", "invalid"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidSubjects := addPolicyRequest{Object: "obj", Policies: []string{"read", "access"}, SubjectIDs: []string{"", "user2"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ct string
|
||||
req string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "Delete policies with unauthorized access",
|
||||
token: userLoginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusForbidden,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid token",
|
||||
token: "invalid",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty token",
|
||||
token: "",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with authorized access",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusNoContent,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete multiple policies to multiple user",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusNoContent,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid content type",
|
||||
token: loginSecret,
|
||||
ct: "text/html",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty content type",
|
||||
token: loginSecret,
|
||||
ct: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid object field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidObject),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid policies field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidPolicies),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid subjects field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidSubjects),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/policies", 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package policies
|
||||
|
||||
import "github.com/mainflux/mainflux/internal/apiutil"
|
||||
|
||||
// Action represents an enum for the policies used in the Mainflux.
|
||||
type Action int
|
||||
|
||||
const (
|
||||
Create Action = iota
|
||||
Read
|
||||
Write
|
||||
Delete
|
||||
Access
|
||||
Member
|
||||
Unknown
|
||||
)
|
||||
|
||||
var actions = map[string]Action{
|
||||
"create": Create,
|
||||
"read": Read,
|
||||
"write": Write,
|
||||
"delete": Delete,
|
||||
"access": Access,
|
||||
"member": Member,
|
||||
}
|
||||
|
||||
type policiesReq struct {
|
||||
token string
|
||||
SubjectIDs []string `json:"subjects"`
|
||||
Policies []string `json:"policies"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
func (req policiesReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if len(req.SubjectIDs) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
if len(req.Policies) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
if req.Object == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
for _, policy := range req.Policies {
|
||||
if _, ok := actions[policy]; !ok {
|
||||
return apiutil.ErrMalformedPolicy
|
||||
}
|
||||
}
|
||||
|
||||
for _, subID := range req.SubjectIDs {
|
||||
if subID == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package policies
|
||||
|
||||
import "net/http"
|
||||
|
||||
type createPolicyRes struct {
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type deletePoliciesRes struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Code() int {
|
||||
if res.deleted {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
|
||||
mux.Post("/policies", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_policy_bulk")(createPolicyEndpoint(svc)),
|
||||
decodePoliciesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/policies", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_policies")(deletePoliciesEndpoint(svc)),
|
||||
decodePoliciesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodePoliciesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := policiesReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func 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, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrEmptyList,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMalformedPolicy:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/api/http/groups"
|
||||
"github.com/mainflux/mainflux/auth/api/http/keys"
|
||||
"github.com/mainflux/mainflux/auth/api/http/policies"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, tracer opentracing.Tracer, logger logger.Logger) http.Handler {
|
||||
mux := bone.New()
|
||||
mux = keys.MakeHandler(svc, mux, tracer, logger)
|
||||
mux = groups.MakeHandler(svc, mux, tracer, logger)
|
||||
mux = policies.MakeHandler(svc, mux, tracer, logger)
|
||||
mux.GetFunc("/health", mainflux.Health("auth"))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
return mux
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc auth.Service, logger log.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListPolicies(ctx context.Context, pr auth.PolicyReq) (p auth.PolicyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_policies 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.ListPolicies(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Issue(ctx context.Context, token string, newKey auth.Key) (key auth.Key, secret string, 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, token, newKey)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Revoke(ctx context.Context, token, 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, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RetrieveKey(ctx context.Context, token, id string) (key auth.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.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id auth.Identity, 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)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method authorize 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.Authorize(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddPolicy(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add_policy 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.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_policy_bulk 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.AddPolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeletePolicy(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method delete_policy 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.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method delete_policies 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.DeletePolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (g auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, group.Name, 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.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, group.Name, 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.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_group for token %s and id %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.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_group for token %s and id %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.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_groups for token %s took %s to complete", token, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListGroups(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_children for token %s and parent %s took %s to complete", token, parentID, 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.ListChildren(ctx, token, parentID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_parents for token %s and child %s took for child %s to complete", token, childID, 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.ListParents(ctx, token, childID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_members for token %s and group id %s took %s to complete", token, groupID, 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.ListMembers(ctx, token, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_memberships for token %s and member id %s took %s to complete", token, memberID, 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.ListMemberships(ctx, token, memberID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, 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.Assign(ctx, token, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, 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.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method share_group_access 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.AssignGroupAccessRights(ctx, token, thingGroupID, userGroupID)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListPolicies(ctx context.Context, pr auth.PolicyReq) (p auth.PolicyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_policies").Add(1)
|
||||
ms.latency.With("method", "list_policies").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListPolicies(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.Key) (auth.Key, string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_key").Add(1)
|
||||
ms.latency.With("method", "issue_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Issue(ctx, token, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Revoke(ctx context.Context, token, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_key").Add(1)
|
||||
ms.latency.With("method", "revoke_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Revoke(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RetrieveKey(ctx context.Context, token, id string) (auth.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve_key").Add(1)
|
||||
ms.latency.With("method", "retrieve_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.Identity, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identify").Add(1)
|
||||
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "authorize").Add(1)
|
||||
ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.Authorize(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_policy").Add(1)
|
||||
ms.latency.With("method", "add_policy").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_policy_bulk").Add(1)
|
||||
ms.latency.With("method", "create_policy_bulk").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AddPolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "delete_policy").Add(1)
|
||||
ms.latency.With("method", "delete_policy").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "delete_policies").Add(1)
|
||||
ms.latency.With("method", "delete_policies").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.DeletePolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_group").Add(1)
|
||||
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_group").Add(1)
|
||||
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_group").Add(1)
|
||||
ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_group").Add(1)
|
||||
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_groups").Add(1)
|
||||
ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListGroups(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "parents").Add(1)
|
||||
ms.latency.With("method", "parents").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListParents(ctx, token, childID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_children").Add(1)
|
||||
ms.latency.With("method", "list_children").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChildren(ctx, token, parentID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_members").Add(1)
|
||||
ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMembers(ctx, token, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_memberships").Add(1)
|
||||
ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMemberships(ctx, token, memberID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "assign").Add(1)
|
||||
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "unassign").Add(1)
|
||||
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "share_group_access").Add(1)
|
||||
ms.latency.With("method", "share_group_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AssignGroupAccessRights(ctx, token, thingGroupID, userGroupID)
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLevel represents the maximum group hierarchy level.
|
||||
MaxLevel = uint64(5)
|
||||
// MinLevel represents the minimum group hierarchy level.
|
||||
MinLevel = uint64(1)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAssignToGroup indicates failure to assign member to a group.
|
||||
ErrAssignToGroup = errors.New("failed to assign member to a group")
|
||||
|
||||
// ErrUnassignFromGroup indicates failure to unassign member from a group.
|
||||
ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
|
||||
|
||||
// ErrMissingParent indicates that parent can't be found
|
||||
ErrMissingParent = errors.New("failed to retrieve parent")
|
||||
|
||||
// ErrGroupNotEmpty indicates group is not empty, can't be deleted.
|
||||
ErrGroupNotEmpty = errors.New("group is not empty")
|
||||
|
||||
// ErrMemberAlreadyAssigned indicates that members is already assigned.
|
||||
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
|
||||
)
|
||||
|
||||
// GroupMetadata defines the Metadata type.
|
||||
type GroupMetadata map[string]interface{}
|
||||
|
||||
// Member represents the member information.
|
||||
type Member struct {
|
||||
ID string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Group represents the group information.
|
||||
type Group struct {
|
||||
ID string
|
||||
OwnerID string
|
||||
ParentID string
|
||||
Name string
|
||||
Description string
|
||||
Metadata GroupMetadata
|
||||
// Indicates a level in tree hierarchy.
|
||||
// Root node is level 1.
|
||||
Level int
|
||||
// Path in a tree consisting of group ids
|
||||
// parentID1.parentID2.childID1
|
||||
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
|
||||
Path string
|
||||
Children []*Group
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PageMetadata contains page metadata that helps navigation.
|
||||
type PageMetadata struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Size uint64
|
||||
Level uint64
|
||||
Name string
|
||||
Type string
|
||||
Metadata GroupMetadata
|
||||
}
|
||||
|
||||
// GroupPage contains page related metadata as well as list of groups that
|
||||
// belong to this page.
|
||||
type GroupPage struct {
|
||||
PageMetadata
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
// MemberPage contains page related metadata as well as list of members that
|
||||
// belong to this page.
|
||||
type MemberPage struct {
|
||||
PageMetadata
|
||||
Members []Member
|
||||
}
|
||||
|
||||
// GroupService specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type GroupService interface {
|
||||
// CreateGroup creates new group.
|
||||
CreateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// UpdateGroup updates the group identified by the provided ID.
|
||||
UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// ViewGroup retrieves data about the group identified by ID.
|
||||
ViewGroup(ctx context.Context, token, id string) (Group, error)
|
||||
|
||||
// ListGroups retrieves groups.
|
||||
ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListChildren retrieves groups that are children to group identified by parentID
|
||||
ListChildren(ctx context.Context, token, parentID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListParents retrieves groups that are parent to group identified by childID.
|
||||
ListParents(ctx context.Context, token, childID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListMembers retrieves everything that is assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID, groupType string, pm PageMetadata) (MemberPage, error)
|
||||
|
||||
// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
|
||||
ListMemberships(ctx context.Context, token, memberID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RemoveGroup removes the group identified with the provided ID.
|
||||
RemoveGroup(ctx context.Context, token, id string) error
|
||||
|
||||
// Assign adds a member with memberID into the group identified by groupID.
|
||||
Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) error
|
||||
|
||||
// Unassign removes member with memberID from group identified by groupID.
|
||||
Unassign(ctx context.Context, token, groupID string, memberIDs ...string) error
|
||||
|
||||
// AssignGroupAccessRights adds access rights on thing groups to user group.
|
||||
AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error
|
||||
}
|
||||
|
||||
// GroupRepository specifies a group persistence API.
|
||||
type GroupRepository interface {
|
||||
// Save group
|
||||
Save(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Update a group
|
||||
Update(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Delete a group
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// RetrieveByID retrieves group by its id
|
||||
RetrieveByID(ctx context.Context, id string) (Group, error)
|
||||
|
||||
// RetrieveAll retrieves all groups.
|
||||
RetrieveAll(ctx context.Context, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
|
||||
RetrieveAllParents(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
|
||||
RetrieveAllChildren(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// Retrieves list of groups that member belongs to
|
||||
Memberships(ctx context.Context, memberID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// Members retrieves everything that is assigned to a group identified by groupID.
|
||||
Members(ctx context.Context, groupID, groupType string, pm PageMetadata) (MemberPage, error)
|
||||
|
||||
// Assign adds a member to group.
|
||||
Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error
|
||||
|
||||
// Unassign removes a member from a group
|
||||
Unassign(ctx context.Context, groupID string, memberIDs ...string) error
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
|
||||
const secret = "test"
|
||||
|
||||
func key() authn.Key {
|
||||
func key() auth.Key {
|
||||
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
|
||||
return authn.Key{
|
||||
return auth.Key{
|
||||
ID: "id",
|
||||
Type: authn.UserKey,
|
||||
Issuer: "user@email.com",
|
||||
Secret: "",
|
||||
Type: auth.LoginKey,
|
||||
Subject: "user@email.com",
|
||||
IssuerID: "",
|
||||
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: exp,
|
||||
}
|
||||
@@ -32,12 +32,9 @@ func key() authn.Key {
|
||||
func TestIssue(t *testing.T) {
|
||||
tokenizer := jwt.New(secret)
|
||||
|
||||
emptyIssuer := key()
|
||||
emptyIssuer.Issuer = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
@@ -59,10 +56,10 @@ func TestParse(t *testing.T) {
|
||||
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)
|
||||
apiKey := key()
|
||||
apiKey.Type = auth.APIKey
|
||||
apiKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
apiToken, err := tokenizer.Issue(apiKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
expKey := key()
|
||||
@@ -72,7 +69,7 @@ func TestParse(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
key auth.Key
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
@@ -84,22 +81,21 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "parse ivalid key",
|
||||
key: authn.Key{},
|
||||
key: auth.Key{},
|
||||
token: "invalid",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
|
||||
{
|
||||
desc: "parse expired key",
|
||||
key: authn.Key{},
|
||||
key: auth.Key{},
|
||||
token: expToken,
|
||||
err: authn.ErrKeyExpired,
|
||||
err: auth.ErrKeyExpired,
|
||||
},
|
||||
{
|
||||
desc: "parse expired user key",
|
||||
key: userKey,
|
||||
token: userToken,
|
||||
err: nil,
|
||||
desc: "parse expired API key",
|
||||
key: apiKey,
|
||||
token: apiToken,
|
||||
err: auth.ErrAPIKeyExpired,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,19 +6,22 @@ package jwt
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const issuerName = "mainflux.auth"
|
||||
|
||||
type claims struct {
|
||||
jwt.StandardClaims
|
||||
Type *uint32 `json:"type,omitempty"`
|
||||
IssuerID string `json:"issuer_id,omitempty"`
|
||||
Type *uint32 `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (c claims) Valid() error {
|
||||
if c.Type == nil || *c.Type > authn.APIKey {
|
||||
return authn.ErrMalformedEntity
|
||||
if c.Type == nil || *c.Type > auth.APIKey || c.Issuer != issuerName {
|
||||
return errors.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return c.StandardClaims.Valid()
|
||||
@@ -29,18 +32,19 @@ type tokenizer struct {
|
||||
}
|
||||
|
||||
// New returns new JWT Tokenizer.
|
||||
func New(secret string) authn.Tokenizer {
|
||||
func New(secret string) auth.Tokenizer {
|
||||
return tokenizer{secret: secret}
|
||||
}
|
||||
|
||||
func (svc tokenizer) Issue(key authn.Key) (string, error) {
|
||||
func (svc tokenizer) Issue(key auth.Key) (string, error) {
|
||||
claims := claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: key.Issuer,
|
||||
Subject: key.Secret,
|
||||
Issuer: issuerName,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt.UTC().Unix(),
|
||||
},
|
||||
Type: &key.Type,
|
||||
IssuerID: key.IssuerID,
|
||||
Type: &key.Type,
|
||||
}
|
||||
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
@@ -54,11 +58,11 @@ func (svc tokenizer) Issue(key authn.Key) (string, error) {
|
||||
return token.SignedString([]byte(svc.secret))
|
||||
}
|
||||
|
||||
func (svc tokenizer) Parse(token string) (authn.Key, error) {
|
||||
func (svc tokenizer) Parse(token string) (auth.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 nil, errors.ErrAuthentication
|
||||
}
|
||||
return []byte(svc.secret), nil
|
||||
})
|
||||
@@ -66,22 +70,22 @@ func (svc tokenizer) Parse(token string) (authn.Key, error) {
|
||||
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
|
||||
if c.Type != nil && *c.Type == auth.APIKey {
|
||||
return c.toKey(), auth.ErrAPIKeyExpired
|
||||
}
|
||||
return authn.Key{}, errors.Wrap(authn.ErrKeyExpired, err)
|
||||
return auth.Key{}, errors.Wrap(auth.ErrKeyExpired, err)
|
||||
}
|
||||
return authn.Key{}, errors.Wrap(authn.ErrUnauthorizedAccess, err)
|
||||
return auth.Key{}, errors.Wrap(errors.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
return c.toKey(), nil
|
||||
}
|
||||
|
||||
func (c claims) toKey() authn.Key {
|
||||
key := authn.Key{
|
||||
func (c claims) toKey() auth.Key {
|
||||
key := auth.Key{
|
||||
ID: c.Id,
|
||||
Issuer: c.Issuer,
|
||||
Secret: c.Subject,
|
||||
IssuerID: c.IssuerID,
|
||||
Subject: c.Subject,
|
||||
IssuedAt: time.Unix(c.IssuedAt, 0).UTC(),
|
||||
}
|
||||
if c.ExpiresAt != 0 {
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package keto contains PolicyAgent implementation using Keto.
|
||||
package keto
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
subjectSetRegex = "^.{1,}:.{1,}#.{1,}$" // expected subject set structure is <namespace>:<object>#<relation>
|
||||
ketoNamespace = "members"
|
||||
)
|
||||
|
||||
type policyAgent struct {
|
||||
writer acl.WriteServiceClient
|
||||
checker acl.CheckServiceClient
|
||||
reader acl.ReadServiceClient
|
||||
}
|
||||
|
||||
// NewPolicyAgent returns a gRPC communication functionalities
|
||||
// to communicate with ORY Keto.
|
||||
func NewPolicyAgent(checker acl.CheckServiceClient, writer acl.WriteServiceClient, reader acl.ReadServiceClient) auth.PolicyAgent {
|
||||
return policyAgent{checker: checker, writer: writer, reader: reader}
|
||||
}
|
||||
|
||||
func (pa policyAgent) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
res, err := pa.checker.Check(context.Background(), &acl.CheckRequest{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: getSubject(pr),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errors.ErrAuthorization)
|
||||
}
|
||||
if !res.GetAllowed() {
|
||||
return errors.ErrAuthorization
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa policyAgent) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
var ss *acl.Subject
|
||||
switch isSubjectSet(pr.Subject) {
|
||||
case true:
|
||||
namespace, object, relation := parseSubjectSet(pr.Subject)
|
||||
ss = &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{Namespace: namespace, Object: object, Relation: relation}},
|
||||
}
|
||||
default:
|
||||
ss = &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
trt := pa.writer.TransactRelationTuples
|
||||
_, err := trt(context.Background(), &acl.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*acl.RelationTupleDelta{
|
||||
{
|
||||
Action: acl.RelationTupleDelta_INSERT,
|
||||
RelationTuple: &acl.RelationTuple{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: ss,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (pa policyAgent) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
trt := pa.writer.TransactRelationTuples
|
||||
_, err := trt(context.Background(), &acl.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*acl.RelationTupleDelta{
|
||||
{
|
||||
Action: acl.RelationTupleDelta_DELETE,
|
||||
RelationTuple: &acl.RelationTuple{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: &acl.Subject{Ref: &acl.Subject_Id{
|
||||
Id: pr.Subject,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (pa policyAgent) RetrievePolicies(ctx context.Context, pr auth.PolicyReq) ([]*acl.RelationTuple, error) {
|
||||
var ss *acl.Subject
|
||||
switch isSubjectSet(pr.Subject) {
|
||||
case true:
|
||||
namespace, object, relation := parseSubjectSet(pr.Subject)
|
||||
ss = &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{Namespace: namespace, Object: object, Relation: relation}},
|
||||
}
|
||||
default:
|
||||
ss = &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
res, err := pa.reader.ListRelationTuples(ctx, &acl.ListRelationTuplesRequest{
|
||||
Query: &acl.ListRelationTuplesRequest_Query{
|
||||
Namespace: ketoNamespace,
|
||||
Relation: pr.Relation,
|
||||
Subject: ss,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return []*acl.RelationTuple{}, err
|
||||
}
|
||||
|
||||
tuple := res.GetRelationTuples()
|
||||
for res.NextPageToken != "" {
|
||||
tuple = append(tuple, res.GetRelationTuples()...)
|
||||
}
|
||||
|
||||
return tuple, nil
|
||||
}
|
||||
|
||||
// getSubject returns a 'subject' field for ACL(access control lists).
|
||||
// If the given PolicyReq argument contains a subject as subject set,
|
||||
// it returns subject set; otherwise, it returns a subject.
|
||||
func getSubject(pr auth.PolicyReq) *acl.Subject {
|
||||
if isSubjectSet(pr.Subject) {
|
||||
return &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
return &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
// isSubjectSet returns true when given subject is subject set.
|
||||
// Otherwise, it returns false.
|
||||
func isSubjectSet(subject string) bool {
|
||||
r, err := regexp.Compile(subjectSetRegex)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return r.MatchString(subject)
|
||||
}
|
||||
|
||||
func parseSubjectSet(subjectSet string) (namespace, object, relation string) {
|
||||
r := strings.Split(subjectSet, ":")
|
||||
if len(r) != 2 {
|
||||
return
|
||||
}
|
||||
namespace = r[0]
|
||||
|
||||
r = strings.Split(r[1], "#")
|
||||
if len(r) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
object = r[0]
|
||||
relation = r[1]
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package keto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSubjectSet(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
subjectSet string
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
desc: "check valid subject set",
|
||||
subjectSet: "namespace:object#relation",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing namespace field",
|
||||
subjectSet: ":object#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing object field",
|
||||
subjectSet: "namespace:#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing relation field",
|
||||
subjectSet: "namespace:object#",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, empty subject set",
|
||||
subjectSet: ":#",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing subject set identifier",
|
||||
subjectSet: "namespace:#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing object field",
|
||||
subjectSet: "namespace:object",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, unexpected object field",
|
||||
subjectSet: "namespace:object@relation",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
iss := isSubjectSet(tc.subjectSet)
|
||||
assert.Equal(t, iss, tc.result, fmt.Sprintf("%s expected to be %v, got %v\n", tc.desc, tc.result, iss))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetSubject(t *testing.T) {
|
||||
p1 := auth.PolicyReq{Subject: "subject", Object: "object", Relation: "relation"}
|
||||
s1 := getSubject(p1)
|
||||
ref1 := s1.GetRef()
|
||||
_, ok := ref1.(*acl.Subject_Id)
|
||||
assert.True(t, ok, fmt.Errorf("subject reference of %#v is expected to be (*acl.Subject_Id), got %T", p1, ref1))
|
||||
|
||||
p2 := auth.PolicyReq{Subject: "members:group#access", Object: "object", Relation: "relation"}
|
||||
s2 := getSubject(p2)
|
||||
ref2 := s2.GetRef()
|
||||
_, ok = ref2.(*acl.Subject_Set)
|
||||
assert.True(t, ok, fmt.Errorf("subject reference of %#v is expected to be (*acl.Subject_Set), got %T", p2, ref2))
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -15,11 +15,15 @@ var (
|
||||
|
||||
// ErrKeyExpired indicates that the Key is expired.
|
||||
ErrKeyExpired = errors.New("use of expired key")
|
||||
|
||||
// ErrAPIKeyExpired indicates that the Key is expired
|
||||
// and that the key type is API key.
|
||||
ErrAPIKeyExpired = errors.New("use of expired API key")
|
||||
)
|
||||
|
||||
const (
|
||||
// UserKey is temporary User key received on successfull login.
|
||||
UserKey uint32 = iota
|
||||
// LoginKey is temporary User key received on successfull login.
|
||||
LoginKey uint32 = iota
|
||||
// RecoveryKey represents a key for resseting password.
|
||||
RecoveryKey
|
||||
// APIKey enables the one to act on behalf of the user.
|
||||
@@ -30,12 +34,18 @@ const (
|
||||
type Key struct {
|
||||
ID string
|
||||
Type uint32
|
||||
Issuer string
|
||||
Secret string
|
||||
IssuerID string
|
||||
Subject string
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Identity contains ID and Email.
|
||||
type Identity struct {
|
||||
ID string
|
||||
Email string
|
||||
}
|
||||
|
||||
// Expired verifies if the key is expired.
|
||||
func (k Key) Expired() bool {
|
||||
if k.Type == APIKey && k.ExpiresAt.IsZero() {
|
||||
@@ -1,14 +1,14 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn_test
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -17,12 +17,12 @@ func TestExpired(t *testing.T) {
|
||||
exp1 := time.Now()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
key auth.Key
|
||||
expired bool
|
||||
}{
|
||||
{
|
||||
desc: "not expired key",
|
||||
key: authn.Key{
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: exp,
|
||||
},
|
||||
@@ -30,7 +30,7 @@ func TestExpired(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "expired key",
|
||||
key: authn.Key{
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now().UTC().Add(2 * time.Minute),
|
||||
ExpiresAt: exp1,
|
||||
},
|
||||
@@ -38,16 +38,16 @@ func TestExpired(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "user key with no expiration date",
|
||||
key: authn.Key{
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "API key with no expiration date",
|
||||
key: authn.Key{
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now(),
|
||||
Type: authn.APIKey,
|
||||
Type: auth.APIKey,
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ auth.GroupRepository = (*groupRepositoryMock)(nil)
|
||||
|
||||
type groupRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
// Map of groups, group id as a key.
|
||||
// groups map[GroupID]auth.Group
|
||||
groups map[string]auth.Group
|
||||
// Map of groups with group id as key that are
|
||||
// children (i.e. has same parent id) is element
|
||||
// in children's map where parent id is key.
|
||||
// children map[ParentID]map[GroupID]auth.Group
|
||||
children map[string]map[string]auth.Group
|
||||
// Map of parents' id with child group id as key.
|
||||
// Each child has one parent.
|
||||
// parents map[ChildID]ParentID
|
||||
parents map[string]string
|
||||
// Map of groups (with group id as key) which
|
||||
// represent memberships is element in
|
||||
// memberships' map where member id is a key.
|
||||
// memberships map[MemberID]map[GroupID]auth.Group
|
||||
memberships map[string]map[string]auth.Group
|
||||
// Map of group members where member id is a key
|
||||
// is an element in the map members where group id is a key.
|
||||
// members map[type][GroupID]map[MemberID]MemberID
|
||||
members map[string]map[string]map[string]string
|
||||
}
|
||||
|
||||
// NewGroupRepository creates in-memory user repository
|
||||
func NewGroupRepository() auth.GroupRepository {
|
||||
return &groupRepositoryMock{
|
||||
groups: make(map[string]auth.Group),
|
||||
children: make(map[string]map[string]auth.Group),
|
||||
parents: make(map[string]string),
|
||||
memberships: make(map[string]map[string]auth.Group),
|
||||
members: make(map[string]map[string]map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Save(ctx context.Context, group auth.Group) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[group.ID]; ok {
|
||||
return auth.Group{}, errors.ErrConflict
|
||||
}
|
||||
path := group.ID
|
||||
|
||||
if group.ParentID != "" {
|
||||
parent, ok := grm.groups[group.ParentID]
|
||||
if !ok {
|
||||
return auth.Group{}, errors.ErrCreateEntity
|
||||
}
|
||||
if _, ok := grm.children[group.ParentID]; !ok {
|
||||
grm.children[group.ParentID] = make(map[string]auth.Group)
|
||||
}
|
||||
grm.children[group.ParentID][group.ID] = group
|
||||
grm.parents[group.ID] = group.ParentID
|
||||
path = fmt.Sprintf("%s.%s", parent.Path, path)
|
||||
}
|
||||
|
||||
group.Path = path
|
||||
group.Level = len(strings.Split(path, "."))
|
||||
|
||||
grm.groups[group.ID] = group
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Update(ctx context.Context, group auth.Group) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
up, ok := grm.groups[group.ID]
|
||||
if !ok {
|
||||
return auth.Group{}, errors.ErrNotFound
|
||||
}
|
||||
up.Name = group.Name
|
||||
up.Description = group.Description
|
||||
up.Metadata = group.Metadata
|
||||
up.UpdatedAt = time.Now()
|
||||
|
||||
grm.groups[group.ID] = up
|
||||
return up, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[id]; !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
if len(grm.members[id]) > 0 {
|
||||
return auth.ErrGroupNotEmpty
|
||||
}
|
||||
|
||||
// This is not quite exact, it should go in depth
|
||||
for _, ch := range grm.children[id] {
|
||||
if len(grm.members[ch.ID]) > 0 {
|
||||
return auth.ErrGroupNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// This is not quite exact, it should go in depth
|
||||
delete(grm.groups, id)
|
||||
for _, ch := range grm.children[id] {
|
||||
delete(grm.members, ch.ID)
|
||||
}
|
||||
|
||||
delete(grm.children, id)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
|
||||
val, ok := grm.groups[id]
|
||||
if !ok {
|
||||
return auth.Group{}, errors.ErrNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []auth.Group
|
||||
for _, g := range grm.groups {
|
||||
items = append(items, g)
|
||||
}
|
||||
return auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
for _, memberID := range memberIDs {
|
||||
for typ, m := range grm.members[groupID] {
|
||||
_, ok := m[memberID]
|
||||
if !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
delete(grm.members[groupID][typ], memberID)
|
||||
delete(grm.memberships[memberID], groupID)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
if _, ok := grm.members[groupID]; !ok {
|
||||
grm.members[groupID] = make(map[string]map[string]string)
|
||||
}
|
||||
|
||||
for _, memberID := range memberIDs {
|
||||
if _, ok := grm.members[groupID][groupType]; !ok {
|
||||
grm.members[groupID][groupType] = make(map[string]string)
|
||||
}
|
||||
if _, ok := grm.memberships[memberID]; !ok {
|
||||
grm.memberships[memberID] = make(map[string]auth.Group)
|
||||
}
|
||||
|
||||
grm.members[groupID][groupType][memberID] = memberID
|
||||
grm.memberships[memberID][groupID] = grm.groups[groupID]
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []auth.Group
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
i := uint64(0)
|
||||
for _, g := range grm.memberships[memberID] {
|
||||
if i >= first && i < last {
|
||||
items = append(items, g)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Limit: pm.Limit,
|
||||
Offset: pm.Offset,
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []auth.Member
|
||||
members, ok := grm.members[groupID][groupType]
|
||||
if !ok {
|
||||
return auth.MemberPage{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
i := uint64(0)
|
||||
for _, g := range members {
|
||||
if i >= first && i < last {
|
||||
items = append(items, auth.Member{ID: g, Type: groupType})
|
||||
}
|
||||
i++
|
||||
}
|
||||
return auth.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if groupID == "" {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return auth.GroupPage{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
groups := make([]auth.Group, 0)
|
||||
groups, err := grm.getParents(groups, group)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: groups,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(groups)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) getParents(groups []auth.Group, group auth.Group) ([]auth.Group, error) {
|
||||
groups = append(groups, group)
|
||||
parentID, ok := grm.parents[group.ID]
|
||||
if !ok && parentID == "" {
|
||||
return groups, nil
|
||||
}
|
||||
parent, ok := grm.groups[parentID]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("parent with id: %s not found", parentID))
|
||||
}
|
||||
return grm.getParents(groups, parent)
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
|
||||
groups := make([]auth.Group, 0)
|
||||
groups = append(groups, group)
|
||||
for ch := range grm.parents {
|
||||
g, ok := grm.groups[ch]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("child with id %s not found", ch))
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: groups,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(groups)),
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -7,48 +7,49 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ authn.KeyRepository = (*keyRepositoryMock)(nil)
|
||||
var _ auth.KeyRepository = (*keyRepositoryMock)(nil)
|
||||
|
||||
type keyRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
keys map[string]authn.Key
|
||||
keys map[string]auth.Key
|
||||
}
|
||||
|
||||
// NewKeyRepository creates in-memory user repository
|
||||
func NewKeyRepository() authn.KeyRepository {
|
||||
func NewKeyRepository() auth.KeyRepository {
|
||||
return &keyRepositoryMock{
|
||||
keys: make(map[string]authn.Key),
|
||||
keys: make(map[string]auth.Key),
|
||||
}
|
||||
}
|
||||
|
||||
func (krm *keyRepositoryMock) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
func (krm *keyRepositoryMock) Save(ctx context.Context, key auth.Key) (string, error) {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
if _, ok := krm.keys[key.ID]; ok {
|
||||
return "", authn.ErrConflict
|
||||
return "", errors.ErrConflict
|
||||
}
|
||||
|
||||
krm.keys[key.ID] = key
|
||||
return key.ID, nil
|
||||
}
|
||||
func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
|
||||
func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, error) {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
|
||||
if key, ok := krm.keys[id]; ok && key.IssuerID == issuerID {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return authn.Key{}, authn.ErrNotFound
|
||||
return auth.Key{}, errors.ErrNotFound
|
||||
}
|
||||
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuer, id string) error {
|
||||
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuerID, id string) error {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
|
||||
if key, ok := krm.keys[id]; ok && key.IssuerID == issuerID {
|
||||
delete(krm.keys, id)
|
||||
}
|
||||
return nil
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
type MockSubjectSet struct {
|
||||
Object string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type policyAgentMock struct {
|
||||
mu sync.Mutex
|
||||
// authzDb stores 'subject' as a key, and subject policies as a value.
|
||||
authzDB map[string][]MockSubjectSet
|
||||
}
|
||||
|
||||
// NewKetoMock returns a mock service for Keto.
|
||||
// This mock is not implemented yet.
|
||||
func NewKetoMock(db map[string][]MockSubjectSet) auth.PolicyAgent {
|
||||
return &policyAgentMock{authzDB: db}
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
for _, ss := range ssList {
|
||||
if ss.Object == pr.Object && ss.Relation == pr.Relation {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.ErrAuthorization
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
pa.authzDB[pr.Subject] = append(pa.authzDB[pr.Subject], MockSubjectSet{Object: pr.Object, Relation: pr.Relation})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
for k, ss := range ssList {
|
||||
if ss.Object == pr.Object && ss.Relation == pr.Relation {
|
||||
ssList[k] = MockSubjectSet{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) RetrievePolicies(ctx context.Context, pr auth.PolicyReq) ([]*acl.RelationTuple, error) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
tuple := []*acl.RelationTuple{}
|
||||
for _, ss := range ssList {
|
||||
if ss.Relation == pr.Relation {
|
||||
tuple = append(tuple, &acl.RelationTuple{Object: ss.Object, Relation: ss.Relation})
|
||||
}
|
||||
}
|
||||
return tuple, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
// PolicyReq represents an argument struct for making a policy related
|
||||
// function calls.
|
||||
type PolicyReq struct {
|
||||
Subject string
|
||||
Object string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type PolicyPage struct {
|
||||
Policies []string
|
||||
}
|
||||
|
||||
// Authz represents a authorization service. It exposes
|
||||
// functionalities through `auth` to perform authorization.
|
||||
type Authz interface {
|
||||
// Authorize checks authorization of the given `subject`. Basically,
|
||||
// Authorize verifies that Is `subject` allowed to `relation` on
|
||||
// `object`. Authorize returns a non-nil error if the subject has
|
||||
// no relation on the object (which simply means the operation is
|
||||
// denied).
|
||||
Authorize(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicy creates a policy for the given subject, so that, after
|
||||
// AddPolicy, `subject` has a `relation` on `object`. Returns a non-nil
|
||||
// error in case of failures.
|
||||
AddPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicies adds new policies for given subjects. This method is
|
||||
// only allowed to use as an admin.
|
||||
AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error
|
||||
|
||||
// DeletePolicy removes a policy.
|
||||
DeletePolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// DeletePolicies deletes policies for given subjects. This method is
|
||||
// only allowed to use as an admin.
|
||||
DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error
|
||||
|
||||
// ListPolicies lists policies based on the given PolicyReq structure.
|
||||
ListPolicies(ctx context.Context, pr PolicyReq) (PolicyPage, error)
|
||||
}
|
||||
|
||||
// PolicyAgent facilitates the communication to authorization
|
||||
// services and implements Authz functionalities for certain
|
||||
// authorization services (e.g. ORY Keto).
|
||||
type PolicyAgent interface {
|
||||
// CheckPolicy checks if the subject has a relation on the object.
|
||||
// It returns a non-nil error if the subject has no relation on
|
||||
// the object (which simply means the operation is denied).
|
||||
CheckPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicy creates a policy for the given subject, so that, after
|
||||
// AddPolicy, `subject` has a `relation` on `object`. Returns a non-nil
|
||||
// error in case of failures.
|
||||
AddPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// DeletePolicy removes a policy.
|
||||
DeletePolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
RetrievePolicies(ctx context.Context, pr PolicyReq) ([]*acl.RelationTuple, error)
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errStringToUUID = errors.New("error converting string to uuid")
|
||||
errGetTotal = errors.New("failed to get total number of groups")
|
||||
errCreateMetadataQuery = errors.New("failed to create query for metadata")
|
||||
|
||||
errTruncation = "string_data_right_truncation"
|
||||
errFK = "foreign_key_violation"
|
||||
groupIDFkeyy = "group_relations_group_id_fkey"
|
||||
)
|
||||
|
||||
var _ auth.GroupRepository = (*groupRepository)(nil)
|
||||
|
||||
type groupRepository struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// NewGroupRepo instantiates a PostgreSQL implementation of group
|
||||
// repository.
|
||||
func NewGroupRepo(db Database) auth.GroupRepository {
|
||||
return &groupRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
// For root group path is initialized with id
|
||||
q := `INSERT INTO groups (name, description, id, path, owner_id, metadata, created_at, updated_at)
|
||||
VALUES (:name, :description, :id, :id, :owner_id, :metadata, :created_at, :updated_at)
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
if g.ParentID != "" {
|
||||
// Path is constructed in insert_group_tr - init.go
|
||||
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at)
|
||||
VALUES ( :name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at)
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
}
|
||||
|
||||
dbg, err := toDBGroup(g)
|
||||
if err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbg)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, err)
|
||||
case errDuplicate:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbg = dbGroup{}
|
||||
if err := row.StructScan(&dbg); err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
return toGroup(dbg)
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
|
||||
dbu, err := toDBGroup(g)
|
||||
if err != nil {
|
||||
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbu = dbGroup{}
|
||||
if err := row.StructScan(&dbu); err != nil {
|
||||
return g, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
qd := `DELETE FROM groups WHERE id = :id`
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
}
|
||||
dbg, err := toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
switch pqErr.Constraint {
|
||||
case groupIDFkeyy:
|
||||
return errors.Wrap(auth.ErrGroupNotEmpty, err)
|
||||
}
|
||||
return errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
dbu := dbGroup{
|
||||
ID: id,
|
||||
}
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups WHERE id = $1`
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return auth.Group{}, errors.Wrap(errors.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
_, metaQuery, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
var mq string
|
||||
if metaQuery != "" {
|
||||
mq = fmt.Sprintf(" AND %s", metaQuery)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
|
||||
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := gr.processRows(rows)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
cq := "SELECT COUNT(*) FROM groups"
|
||||
if metaQuery != "" {
|
||||
cq = fmt.Sprintf(" %s WHERE %s", cq, metaQuery)
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level`
|
||||
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path @> parent.path`
|
||||
|
||||
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveParents, err)
|
||||
}
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) < :level`
|
||||
|
||||
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path <@ parent.path `
|
||||
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveChildren, err)
|
||||
}
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) retrieve(ctx context.Context, groupID, retQuery, cntQuery string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
_, mq, err := getGroupsMetadataQuery("g", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
|
||||
retQuery = fmt.Sprintf(`%s %s`, retQuery, mq)
|
||||
cntQuery = fmt.Sprintf(`%s %s`, cntQuery, mq)
|
||||
|
||||
dbPage, err := toDBGroupPage(groupID, "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, retQuery, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := gr.processRows(rows)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cntQuery, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Level: pm.Level,
|
||||
Total: total,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
|
||||
}
|
||||
|
||||
func (gr groupRepository) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
|
||||
WHERE gr.group_id = :group_id AND gr.type = :type %s`, mq)
|
||||
|
||||
if groupType == "" {
|
||||
q = fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
|
||||
WHERE gr.group_id = :group_id %s`, mq)
|
||||
}
|
||||
|
||||
params, err := toDBMemberPage("", groupID, groupType, pm)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []auth.Member
|
||||
for rows.Next() {
|
||||
member := dbMember{}
|
||||
if err := rows.StructScan(&member); err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, auth.Member{ID: member.MemberID, Type: member.Type})
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups g, group_relations gr
|
||||
WHERE gr.group_id = :group_id AND gr.group_id = g.id AND gr.type = :type %s;`, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
page := auth.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params, err := toDBMemberPage(memberID, "", "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []auth.Group
|
||||
for rows.Next() {
|
||||
dbg := dbGroup{}
|
||||
if err := rows.StructScan(&dbg); err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
gr, err := toGroup(dbg)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id %s `, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string, ids ...string) error {
|
||||
tx, err := gr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
|
||||
VALUES(:group_id, :member_id, :type, :created_at, :updated_at)`
|
||||
|
||||
for _, id := range ids {
|
||||
dbg, err := toDBGroupRelation(id, groupID, groupType)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
created := time.Now()
|
||||
dbg.CreatedAt = created
|
||||
dbg.UpdatedAt = created
|
||||
|
||||
if _, err := tx.NamedExecContext(ctx, qIns, dbg); err != nil {
|
||||
tx.Rollback()
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
return errors.Wrap(errors.ErrConflict, errors.New(pqErr.Detail))
|
||||
case errDuplicate:
|
||||
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pqErr.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Unassign(ctx context.Context, groupID string, ids ...string) error {
|
||||
tx, err := gr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
qDel := `DELETE from group_relations WHERE group_id = :group_id AND member_id = :member_id`
|
||||
|
||||
for _, id := range ids {
|
||||
dbg, err := toDBGroupRelation(id, groupID, "")
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExecContext(ctx, qDel, dbg); err != nil {
|
||||
tx.Rollback()
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbMember struct {
|
||||
MemberID string `db:"member_id"`
|
||||
GroupID string `db:"group_id"`
|
||||
Type string `db:"type"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type dbGroup struct {
|
||||
ID string `db:"id"`
|
||||
ParentID sql.NullString `db:"parent_id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Level int `db:"level"`
|
||||
Path string `db:"path"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type dbGroupPage struct {
|
||||
ID string `db:"id"`
|
||||
ParentID string `db:"parent_id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Path string `db:"path"`
|
||||
Level uint64 `db:"level"`
|
||||
Total uint64 `db:"total"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
}
|
||||
|
||||
type dbMemberPage struct {
|
||||
GroupID string `db:"group_id"`
|
||||
MemberID string `db:"member_id"`
|
||||
Type string `db:"type"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func toUUID(id string) (uuid.NullUUID, error) {
|
||||
var uid uuid.NullUUID
|
||||
if id == "" {
|
||||
return uuid.NullUUID{UUID: uuid.Nil, Valid: false}, nil
|
||||
}
|
||||
err := uid.Scan(id)
|
||||
return uid, err
|
||||
}
|
||||
|
||||
func toString(id uuid.NullUUID) (string, error) {
|
||||
if id.Valid {
|
||||
return id.UUID.String(), nil
|
||||
}
|
||||
if id.UUID == uuid.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return "", errStringToUUID
|
||||
}
|
||||
|
||||
func toDBGroup(g auth.Group) (dbGroup, error) {
|
||||
ownerID, err := toUUID(g.OwnerID)
|
||||
if err != nil {
|
||||
return dbGroup{}, err
|
||||
}
|
||||
|
||||
var parentID sql.NullString
|
||||
if g.ParentID != "" {
|
||||
parentID = sql.NullString{String: g.ParentID, Valid: true}
|
||||
}
|
||||
|
||||
meta := dbMetadata(g.Metadata)
|
||||
|
||||
return dbGroup{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
ParentID: parentID,
|
||||
OwnerID: ownerID,
|
||||
Description: g.Description,
|
||||
Metadata: meta,
|
||||
Path: g.Path,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBGroupPage(id, path string, pm auth.PageMetadata) (dbGroupPage, error) {
|
||||
level := auth.MaxLevel
|
||||
if pm.Level < auth.MaxLevel {
|
||||
level = pm.Level
|
||||
}
|
||||
return dbGroupPage{
|
||||
Metadata: dbMetadata(pm.Metadata),
|
||||
ID: id,
|
||||
Path: path,
|
||||
Level: level,
|
||||
Total: pm.Total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) {
|
||||
return dbMemberPage{
|
||||
GroupID: groupID,
|
||||
MemberID: memberID,
|
||||
Type: groupType,
|
||||
Metadata: dbMetadata(pm.Metadata),
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGroup(dbu dbGroup) (auth.Group, error) {
|
||||
ownerID, err := toString(dbu.OwnerID)
|
||||
if err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
return auth.Group{
|
||||
ID: dbu.ID,
|
||||
Name: dbu.Name,
|
||||
ParentID: dbu.ParentID.String,
|
||||
OwnerID: ownerID,
|
||||
Description: dbu.Description,
|
||||
Metadata: auth.GroupMetadata(dbu.Metadata),
|
||||
Level: dbu.Level,
|
||||
Path: dbu.Path,
|
||||
UpdatedAt: dbu.UpdatedAt,
|
||||
CreatedAt: dbu.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dbGroupRelation struct {
|
||||
GroupID sql.NullString `db:"group_id"`
|
||||
MemberID sql.NullString `db:"member_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Type string `db:"type"`
|
||||
}
|
||||
|
||||
func toDBGroupRelation(memberID, groupID, groupType string) (dbGroupRelation, error) {
|
||||
var grID sql.NullString
|
||||
if groupID != "" {
|
||||
grID = sql.NullString{String: groupID, Valid: true}
|
||||
}
|
||||
|
||||
var mID sql.NullString
|
||||
if memberID != "" {
|
||||
mID = sql.NullString{String: memberID, Valid: true}
|
||||
}
|
||||
|
||||
return dbGroupRelation{
|
||||
GroupID: grID,
|
||||
MemberID: mID,
|
||||
Type: groupType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getGroupsMetadataQuery(db string, m auth.GroupMetadata) (mb []byte, mq string, err error) {
|
||||
if len(m) > 0 {
|
||||
mq = `metadata @> :metadata`
|
||||
if db != "" {
|
||||
mq = db + "." + mq
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, errCreateMetadataQuery)
|
||||
}
|
||||
mb = b
|
||||
}
|
||||
return mb, mq, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) processRows(rows *sqlx.Rows) ([]auth.Group, error) {
|
||||
var items []auth.Group
|
||||
for rows.Next() {
|
||||
dbg := dbGroup{}
|
||||
if err := rows.StructScan(&dbg); err != nil {
|
||||
return items, err
|
||||
}
|
||||
group, err := toGroup(dbg)
|
||||
if err != nil {
|
||||
return items, err
|
||||
}
|
||||
items = append(items, group)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
|
||||
rows, err := db.NamedQueryContext(ctx, query, params)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errGetTotal, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
total := uint64(0)
|
||||
if rows.Next() {
|
||||
if err := rows.Scan(&total); err != nil {
|
||||
return 0, errors.Wrap(errGetTotal, err)
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// dbMetadata type for handling metadata properly in database/sql
|
||||
type dbMetadata map[string]interface{}
|
||||
|
||||
// Scan - Implement the database/sql scanner interface
|
||||
func (m *dbMetadata) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.ErrScanMetadata
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value Implements valuer
|
||||
func (m dbMetadata) Value() (driver.Value, error) {
|
||||
if len(m) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNameSize = 254
|
||||
maxDescSize = 1024
|
||||
groupName = "Mainflux"
|
||||
description = "description"
|
||||
)
|
||||
|
||||
var (
|
||||
invalidName = strings.Repeat("m", maxNameSize+1)
|
||||
invalidDesc = strings.Repeat("m", maxDescSize+1)
|
||||
metadata = auth.GroupMetadata{
|
||||
"admin": "true",
|
||||
}
|
||||
)
|
||||
|
||||
func generateGroupID(t *testing.T) string {
|
||||
grpID, err := ulidProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
return grpID
|
||||
}
|
||||
|
||||
func TestGroupSave(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
usrID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
wrongID, err := ulidProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
grpID := generateGroupID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "create new group",
|
||||
group: auth.Group{
|
||||
ID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create new group with existing name",
|
||||
group: auth.Group{
|
||||
ID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid name",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
OwnerID: usrID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid description",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: "withParent",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent and existing name",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with wrong parent",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: wrongID,
|
||||
OwnerID: usrID,
|
||||
Name: "wrongParent",
|
||||
},
|
||||
err: errors.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := groupRepo.Save(context.Background(), tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGroupRetrieveByID(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "TestGroupRetrieveByID1",
|
||||
OwnerID: uid,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
retrieved, err := groupRepo.RetrieveByID(context.Background(), group1.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.True(t, retrieved.ID == group1.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group1.ID, retrieved.ID))
|
||||
|
||||
// Round to milliseconds as otherwise saving and retriving from DB
|
||||
// adds rounding error.
|
||||
creationTime := time.Now().UTC().Round(time.Millisecond)
|
||||
group2 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "TestGroupRetrieveByID",
|
||||
OwnerID: uid,
|
||||
ParentID: group1.ID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group2)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), group2.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.True(t, retrieved.ID == group2.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group2.ID, retrieved.ID))
|
||||
assert.True(t, retrieved.CreatedAt.Equal(creationTime), fmt.Sprintf("Save group, CreatedAt: expected %s got %s\n", creationTime, retrieved.CreatedAt))
|
||||
assert.True(t, retrieved.UpdatedAt.Equal(creationTime), fmt.Sprintf("Save group, UpdatedAt: expected %s got %s\n", creationTime, retrieved.UpdatedAt))
|
||||
assert.True(t, retrieved.Level == 2, fmt.Sprintf("Save group, Level: expected %d got %d\n", retrieved.Level, 2))
|
||||
assert.True(t, retrieved.ParentID == group1.ID, fmt.Sprintf("Save group, Level: expected %s got %s\n", group1.ID, retrieved.ParentID))
|
||||
assert.True(t, retrieved.Description == description, fmt.Sprintf("Save group, Description: expected %v got %v\n", retrieved.Description, description))
|
||||
assert.True(t, retrieved.Path == fmt.Sprintf("%s.%s", group1.ID, group2.ID), fmt.Sprintf("Save group, Path: expected %s got %s\n", fmt.Sprintf("%s.%s", group1.ID, group2.ID), retrieved.Path))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), generateGroupID(t))
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", errors.ErrNotFound, err))
|
||||
}
|
||||
|
||||
func TestGroupUpdate(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
updateTime := time.Now().UTC()
|
||||
groupID := generateGroupID(t)
|
||||
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
Name: groupName + "TestGroupUpdate",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
retrieved, err := groupRepo.RetrieveByID(context.Background(), group.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
groupUpdate auth.Group
|
||||
groupExpected auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update group for existing id",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Name: groupName + "Updated",
|
||||
UpdatedAt: updateTime,
|
||||
Metadata: auth.GroupMetadata{"admin": "false"},
|
||||
},
|
||||
groupExpected: auth.Group{
|
||||
Name: groupName + "Updated",
|
||||
UpdatedAt: updateTime,
|
||||
Metadata: auth.GroupMetadata{"admin": "false"},
|
||||
CreatedAt: retrieved.CreatedAt,
|
||||
Path: retrieved.Path,
|
||||
ParentID: retrieved.ParentID,
|
||||
ID: retrieved.ID,
|
||||
Level: retrieved.Level,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update group for non-existing id",
|
||||
groupUpdate: auth.Group{
|
||||
ID: "wrong",
|
||||
Name: groupName + "-2",
|
||||
},
|
||||
err: errors.ErrUpdateEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid name",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid description",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
updated, err := groupRepo.Update(context.Background(), tc.groupUpdate)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if tc.desc == "update group for existing id" {
|
||||
assert.True(t, updated.Level == tc.groupExpected.Level, fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Level, updated.Level))
|
||||
assert.True(t, updated.Name == tc.groupExpected.Name, fmt.Sprintf("%s:Name: expected %s got %s\n", tc.desc, tc.groupExpected.Name, updated.Name))
|
||||
assert.True(t, updated.Metadata["admin"] == tc.groupExpected.Metadata["admin"], fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Metadata["admin"], updated.Metadata["admin"]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupDelete(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
groupParent := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
groupParent, err = groupRepo.Save(context.Background(), groupParent)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
creationTime = time.Now().UTC()
|
||||
groupChild1 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: groupParent.ID,
|
||||
Name: groupName + "child1",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
creationTime = time.Now().UTC()
|
||||
groupChild2 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: groupParent.ID,
|
||||
Name: groupName + "child2",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
meta := auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
|
||||
groupChild1, err = groupRepo.Save(context.Background(), groupChild1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
groupChild2, err = groupRepo.Save(context.Background(), groupChild2)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
gp, err := groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Retrieve children for parent: expected %v got %v\n", nil, err))
|
||||
assert.True(t, gp.Total == 3, fmt.Sprintf("Number of children + parent: expected %d got %d\n", 3, gp.Total))
|
||||
|
||||
thingID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("thing id create unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), groupChild1.ID, "things", thingID)
|
||||
require.Nil(t, err, fmt.Sprintf("thing assign got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete non empty group: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupChild2.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete empty group: expected %v got %v\n", nil, err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupParent.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete parent with children with members: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
|
||||
|
||||
gp, err = groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("retrieve children after one child removed: expected %v got %v\n", nil, err))
|
||||
assert.True(t, gp.Total == 2, fmt.Sprintf("number of children + parent: expected %d got %d\n", 2, gp.Total))
|
||||
|
||||
err = groupRepo.Unassign(context.Background(), groupChild1.ID, thingID)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to remove thing from a group error: %s", err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupParent.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete parent with children with no members: expected %v got %v\n", nil, err))
|
||||
|
||||
_, err = groupRepo.RetrieveByID(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.PageMetadata{
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
wrongMeta := auth.PageMetadata{
|
||||
Metadata: auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
},
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(auth.MaxLevel)
|
||||
parentID := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if i < metaNum {
|
||||
group.Metadata = metadata.Metadata
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
Size uint64
|
||||
Metadata auth.PageMetadata
|
||||
}{
|
||||
"retrieve all groups": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: n,
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
Size: n,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: metaNum,
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: metaNum,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(0),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: wrongMeta.Metadata,
|
||||
},
|
||||
Size: uint64(0),
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(metaNum),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: uint64(metaNum),
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(metaNum),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: uint64(metaNum),
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAll(context.Background(), tc.Metadata)
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
|
||||
assert.Equal(t, tc.Metadata.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Metadata.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllParents(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.GroupMetadata{
|
||||
"field": "value",
|
||||
}
|
||||
wrongMeta := auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
}
|
||||
|
||||
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.Equal(t, uint64(0), p.Total, fmt.Sprintf("expected total %d got %d\n", 0, p.Total))
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(10)
|
||||
parentID := ""
|
||||
parentMiddle := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if n-i <= metaNum {
|
||||
group.Metadata = metadata
|
||||
}
|
||||
if i == n/2 {
|
||||
parentMiddle = group.ID
|
||||
}
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
level uint64
|
||||
parentID string
|
||||
Size uint64
|
||||
Total uint64
|
||||
Metadata auth.GroupMetadata
|
||||
}{
|
||||
"retrieve all parents": {
|
||||
Total: n,
|
||||
Size: auth.MaxLevel + 1,
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
Total: metaNum,
|
||||
Size: metaNum,
|
||||
Metadata: metadata,
|
||||
parentID: parentID,
|
||||
level: auth.MaxLevel,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
Total: uint64(0),
|
||||
Metadata: wrongMeta,
|
||||
Size: uint64(0),
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
Total: n,
|
||||
Size: 2 + 1,
|
||||
level: uint64(2),
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
Total: metaNum,
|
||||
Size: metaNum,
|
||||
level: 3,
|
||||
Metadata: metadata,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve parent groups from children in the middle": {
|
||||
Total: n/2 + 1,
|
||||
Size: n/2 + 1,
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentMiddle,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAllParents(context.Background(), tc.parentID, auth.PageMetadata{Level: tc.level, Metadata: tc.Metadata})
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
|
||||
assert.Equal(t, tc.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllChildren(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.GroupMetadata{
|
||||
"field": "value",
|
||||
}
|
||||
wrongMeta := auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
}
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(10)
|
||||
groupID := generateGroupID(t)
|
||||
firstParentID := groupID
|
||||
parentID := ""
|
||||
parentMiddle := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if i < metaNum {
|
||||
group.Metadata = metadata
|
||||
}
|
||||
if i == n/2 {
|
||||
parentMiddle = group.ID
|
||||
}
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
groupID = generateGroupID(t)
|
||||
}
|
||||
|
||||
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.Equal(t, n, p.Total, fmt.Sprintf("expected total %d got %d\n", n, p.Total))
|
||||
|
||||
cases := map[string]struct {
|
||||
parentID string
|
||||
size uint64
|
||||
total uint64
|
||||
metadata auth.PageMetadata
|
||||
}{
|
||||
"retrieve all children": {
|
||||
size: auth.MaxLevel,
|
||||
total: n,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
size: metaNum,
|
||||
total: metaNum,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
total: 0,
|
||||
size: 0,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: wrongMeta,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
total: n,
|
||||
size: 2,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: 2,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
total: metaNum,
|
||||
size: metaNum,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: 3,
|
||||
Metadata: metadata,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve parent groups from children in the middle": {
|
||||
total: n / 2,
|
||||
size: n / 2,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
parentID: parentMiddle,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAllChildren(context.Background(), tc.parentID, tc.metadata)
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssign(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
group, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
|
||||
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrMemberAlreadyAssigned), fmt.Sprintf("assign member again: expected %v got %v\n", auth.ErrMemberAlreadyAssigned, err))
|
||||
}
|
||||
|
||||
func TestUnassign(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
group, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
|
||||
|
||||
mid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
|
||||
|
||||
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 2, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 2, mp.Total))
|
||||
|
||||
err = groupRepo.Unassign(context.Background(), group.ID, mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
|
||||
|
||||
mp, err = groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("members retrieve unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
}
|
||||
|
||||
func cleanUp(t *testing.T) {
|
||||
_, err := db.Exec("delete from group_relations")
|
||||
require.Nil(t, err, fmt.Sprintf("clean relations unexpected error: %s", err))
|
||||
_, err = db.Exec("delete from groups")
|
||||
require.Nil(t, err, fmt.Sprintf("clean groups unexpected error: %s", err))
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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: "auth_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS keys (
|
||||
id VARCHAR(254) NOT NULL,
|
||||
type SMALLINT,
|
||||
subject VARCHAR(254) NOT NULL,
|
||||
issuer_id UUID NOT NULL,
|
||||
issued_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP,
|
||||
PRIMARY KEY (id, issuer_id)
|
||||
)`,
|
||||
`CREATE EXTENSION IF NOT EXISTS LTREE`,
|
||||
`CREATE TABLE IF NOT EXISTS groups (
|
||||
id VARCHAR(254) UNIQUE NOT NULL,
|
||||
parent_id VARCHAR(254),
|
||||
owner_id VARCHAR(254),
|
||||
name VARCHAR(254) NOT NULL,
|
||||
description VARCHAR(1024),
|
||||
metadata JSONB,
|
||||
path LTREE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
UNIQUE (owner_id, name, parent_id),
|
||||
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS group_relations (
|
||||
member_id VARCHAR(254) NOT NULL,
|
||||
group_id VARCHAR(254) NOT NULL,
|
||||
type VARCHAR(254),
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
FOREIGN KEY (group_id) REFERENCES groups (id),
|
||||
PRIMARY KEY (member_id, group_id)
|
||||
)`,
|
||||
`CREATE INDEX path_gist_idx ON groups USING GIST (path);`,
|
||||
`CREATE OR REPLACE FUNCTION inherit_group()
|
||||
RETURNS trigger
|
||||
LANGUAGE PLPGSQL
|
||||
AS
|
||||
$$
|
||||
BEGIN
|
||||
IF NEW.parent_id IS NULL OR NEW.parent_id = '' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT id FROM groups WHERE id = NEW.parent_id) THEN
|
||||
RAISE EXCEPTION 'wrong parent id';
|
||||
END IF;
|
||||
SELECT text2ltree(ltree2text(path) || '.' || NEW.id) INTO NEW.path FROM groups WHERE id = NEW.parent_id;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$`,
|
||||
`CREATE TRIGGER inherit_group_tr
|
||||
BEFORE INSERT
|
||||
ON groups
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE inherit_group();`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS keys`,
|
||||
`DROP EXTENSION IF EXISTS LTREE`,
|
||||
`DROP TABLE IF EXISTS groups`,
|
||||
`DROP TABLE IF EXISTS group_relations`,
|
||||
`DROP FUNCTION IF EXISTS inherit_group`,
|
||||
`DROP TRIGGER IF EXISTS inherit_group_tr ON groups`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ var (
|
||||
errRetrieve = errors.New("failed to retrieve key from database")
|
||||
errDelete = errors.New("failed to delete key from database")
|
||||
)
|
||||
var _ authn.KeyRepository = (*repo)(nil)
|
||||
var _ auth.KeyRepository = (*repo)(nil)
|
||||
|
||||
const (
|
||||
errDuplicate = "unique_violation"
|
||||
@@ -27,15 +27,15 @@ type repo struct {
|
||||
}
|
||||
|
||||
// New instantiates a PostgreSQL implementation of key repository.
|
||||
func New(db Database) authn.KeyRepository {
|
||||
func New(db Database) auth.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)`
|
||||
func (kr repo) Save(ctx context.Context, key auth.Key) (string, error) {
|
||||
q := `INSERT INTO keys (id, type, issuer_id, subject, issued_at, expires_at)
|
||||
VALUES (:id, :type, :issuer_id, :subject, :issued_at, :expires_at)`
|
||||
|
||||
dbKey := toDBKey(key)
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, dbKey); err != nil {
|
||||
@@ -43,7 +43,7 @@ func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
if pqErr.Code.Name() == errDuplicate {
|
||||
return "", errors.Wrap(authn.ErrConflict, pqErr)
|
||||
return "", errors.Wrap(errors.ErrConflict, pqErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,26 +53,26 @@ func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
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`
|
||||
func (kr repo) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, error) {
|
||||
q := `SELECT id, type, issuer_id, subject, issued_at, expires_at FROM keys WHERE issuer_id = $1 AND id = $2`
|
||||
key := dbKey{}
|
||||
if err := kr.db.QueryRowxContext(ctx, q, issuer, id).StructScan(&key); err != nil {
|
||||
if err := kr.db.QueryRowxContext(ctx, q, issuerID, 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 auth.Key{}, errors.Wrap(errors.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return authn.Key{}, errors.Wrap(errRetrieve, err)
|
||||
return auth.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`
|
||||
func (kr repo) Remove(ctx context.Context, issuerID, id string) error {
|
||||
q := `DELETE FROM keys WHERE issuer_id = :issuer_id AND id = :id`
|
||||
key := dbKey{
|
||||
ID: id,
|
||||
Issuer: issuer,
|
||||
ID: id,
|
||||
IssuerID: issuerID,
|
||||
}
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, key); err != nil {
|
||||
return errors.Wrap(errDelete, err)
|
||||
@@ -84,17 +84,19 @@ func (kr repo) Remove(ctx context.Context, issuer, id string) error {
|
||||
type dbKey struct {
|
||||
ID string `db:"id"`
|
||||
Type uint32 `db:"type"`
|
||||
Issuer string `db:"issuer"`
|
||||
IssuerID string `db:"issuer_id"`
|
||||
Subject string `db:"subject"`
|
||||
Revoked bool `db:"revoked"`
|
||||
IssuedAt time.Time `db:"issued_at"`
|
||||
ExpiresAt sql.NullTime `db:"expires_at"`
|
||||
}
|
||||
|
||||
func toDBKey(key authn.Key) dbKey {
|
||||
func toDBKey(key auth.Key) dbKey {
|
||||
ret := dbKey{
|
||||
ID: key.ID,
|
||||
Type: key.Type,
|
||||
Issuer: key.Issuer,
|
||||
IssuerID: key.IssuerID,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
@@ -104,11 +106,12 @@ func toDBKey(key authn.Key) dbKey {
|
||||
return ret
|
||||
}
|
||||
|
||||
func toKey(key dbKey) authn.Key {
|
||||
ret := authn.Key{
|
||||
func toKey(key dbKey) auth.Key {
|
||||
ret := auth.Key{
|
||||
ID: key.ID,
|
||||
Type: key.Type,
|
||||
Issuer: key.Issuer,
|
||||
IssuerID: key.IssuerID,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if key.ExpiresAt.Valid {
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/ulid"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const email = "user-save@example.com"
|
||||
|
||||
var (
|
||||
expTime = time.Now().Add(5 * time.Minute)
|
||||
idProvider = uuid.New()
|
||||
ulidProvider = ulid.New()
|
||||
)
|
||||
|
||||
func TestKeySave(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
id, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a new key",
|
||||
key: auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: id,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with duplicate id",
|
||||
key: auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: id,
|
||||
},
|
||||
err: errors.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)
|
||||
|
||||
id, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: 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
|
||||
owner string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: key.ID,
|
||||
owner: key.IssuerID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve key with empty issuer id",
|
||||
id: key.ID,
|
||||
owner: "",
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existent key",
|
||||
id: "",
|
||||
owner: key.IssuerID,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Retrieve(context.Background(), 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 TestKeyRemove(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
id, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: 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
|
||||
owner string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing key",
|
||||
id: key.ID,
|
||||
owner: key.IssuerID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key that does not exist",
|
||||
id: key.ID,
|
||||
owner: key.IssuerID,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Remove(context.Background(), 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))
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
"github.com/mainflux/mainflux/auth/postgres"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -32,7 +30,7 @@ func TestMain(m *testing.M) {
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
container, err := pool.Run("postgres", "13.3-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
@@ -21,6 +21,9 @@ type database struct {
|
||||
type Database interface {
|
||||
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
|
||||
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
|
||||
QueryxContext(context.Context, string, ...interface{}) (*sqlx.Rows, error)
|
||||
NamedQueryContext(context.Context, string, interface{}) (*sqlx.Rows, error)
|
||||
BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error)
|
||||
}
|
||||
|
||||
// NewDatabase creates a ThingDatabase instance
|
||||
@@ -30,6 +33,11 @@ func NewDatabase(db *sqlx.DB) Database {
|
||||
}
|
||||
}
|
||||
|
||||
func (d database) NamedQueryContext(ctx context.Context, query string, args interface{}) (*sqlx.Rows, error) {
|
||||
addSpanTags(ctx, query)
|
||||
return d.db.NamedQueryContext(ctx, query, args)
|
||||
}
|
||||
|
||||
func (d database) NamedExecContext(ctx context.Context, query string, args interface{}) (sql.Result, error) {
|
||||
addSpanTags(ctx, query)
|
||||
return d.db.NamedExecContext(ctx, query, args)
|
||||
@@ -40,6 +48,21 @@ func (d database) QueryRowxContext(ctx context.Context, query string, args ...in
|
||||
return d.db.QueryRowxContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (d database) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
addSpanTags(ctx, query)
|
||||
return d.db.QueryxContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (d database) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error) {
|
||||
span := opentracing.SpanFromContext(ctx)
|
||||
if span != nil {
|
||||
span.SetTag("span.kind", "client")
|
||||
span.SetTag("peer.service", "postgres")
|
||||
span.SetTag("db.type", "sql")
|
||||
}
|
||||
return d.db.BeginTxx(ctx, opts)
|
||||
}
|
||||
|
||||
func addSpanTags(ctx context.Context, query string) {
|
||||
span := opentracing.SpanFromContext(ctx)
|
||||
if span != nil {
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/ulid"
|
||||
)
|
||||
|
||||
const (
|
||||
recoveryDuration = 5 * time.Minute
|
||||
thingsGroupType = "things"
|
||||
|
||||
authoritiesObject = "authorities"
|
||||
memberRelation = "member"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFailedToRetrieveMembers failed to retrieve group members.
|
||||
ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members")
|
||||
|
||||
// ErrFailedToRetrieveMembership failed to retrieve memberships
|
||||
ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships")
|
||||
|
||||
// ErrFailedToRetrieveAll failed to retrieve groups.
|
||||
ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups")
|
||||
|
||||
// ErrFailedToRetrieveParents failed to retrieve groups.
|
||||
ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups")
|
||||
|
||||
// ErrFailedToRetrieveChildren failed to retrieve groups.
|
||||
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new login key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
)
|
||||
|
||||
// Authn specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
type Authn interface {
|
||||
// Issue issues a new Key, returning its token value alongside.
|
||||
Issue(ctx context.Context, token string, key Key) (Key, string, error)
|
||||
|
||||
// Revoke removes the Key with the provided id that is
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(ctx context.Context, token, id string) error
|
||||
|
||||
// RetrieveKey retrieves data for the Key identified by the provided
|
||||
// ID, that is issued by the user identified by the provided key.
|
||||
RetrieveKey(ctx context.Context, token, id string) (Key, error)
|
||||
|
||||
// Identify validates token token. If token is valid, content
|
||||
// is returned. If token is invalid, or invocation failed for some
|
||||
// other reason, non-nil error value is returned in response.
|
||||
Identify(ctx context.Context, token string) (Identity, error)
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
type Service interface {
|
||||
Authn
|
||||
Authz
|
||||
|
||||
// GroupService implements groups API, creating groups, assigning members
|
||||
GroupService
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
groups GroupRepository
|
||||
idProvider mainflux.IDProvider
|
||||
ulidProvider mainflux.IDProvider
|
||||
agent PolicyAgent
|
||||
tokenizer Tokenizer
|
||||
loginDuration time.Duration
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer, policyAgent PolicyAgent, duration time.Duration) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
groups: groups,
|
||||
idProvider: idp,
|
||||
ulidProvider: ulid.New(),
|
||||
agent: policyAgent,
|
||||
loginDuration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Issue(ctx context.Context, token string, key Key) (Key, string, error) {
|
||||
if key.IssuedAt.IsZero() {
|
||||
return Key{}, "", ErrInvalidKeyIssuedAt
|
||||
}
|
||||
switch key.Type {
|
||||
case APIKey:
|
||||
return svc.userKey(ctx, token, key)
|
||||
case RecoveryKey:
|
||||
return svc.tmpKey(recoveryDuration, key)
|
||||
default:
|
||||
return svc.tmpKey(svc.loginDuration, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, token, id string) error {
|
||||
issuerID, _, err := svc.login(token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
if err := svc.keys.Remove(ctx, issuerID, id); err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
|
||||
issuerID, _, err := svc.login(token)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return svc.keys.Retrieve(ctx, issuerID, id)
|
||||
}
|
||||
|
||||
func (svc service) Identify(ctx context.Context, token string) (Identity, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
if err == ErrAPIKeyExpired {
|
||||
err = svc.keys.Remove(ctx, key.IssuerID, key.ID)
|
||||
return Identity{}, errors.Wrap(ErrAPIKeyExpired, err)
|
||||
}
|
||||
if err != nil {
|
||||
return Identity{}, errors.Wrap(errIdentify, err)
|
||||
}
|
||||
|
||||
switch key.Type {
|
||||
case RecoveryKey, LoginKey:
|
||||
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
||||
case APIKey:
|
||||
_, err := svc.keys.Retrieve(context.TODO(), key.IssuerID, key.ID)
|
||||
if err != nil {
|
||||
return Identity{}, errors.ErrAuthentication
|
||||
}
|
||||
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
||||
default:
|
||||
return Identity{}, errors.ErrAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Authorize(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.CheckPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) AddPolicy(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Authorize(ctx, PolicyReq{Object: authoritiesObject, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, subjectID := range subjectIDs {
|
||||
for _, relation := range relations {
|
||||
if err := svc.AddPolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add '%s' policy on object '%s' for subject '%s': %s", relation, object, subjectID, err), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) DeletePolicy(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user identified by token is the admin.
|
||||
if err := svc.Authorize(ctx, PolicyReq{Object: authoritiesObject, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, subjectID := range subjectIDs {
|
||||
for _, relation := range relations {
|
||||
if err := svc.DeletePolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy on object '%s' for subject '%s': %s", relation, object, subjectID, err), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.agent.AddPolicy(ctx, PolicyReq{Object: thingGroupID, Relation: memberRelation, Subject: fmt.Sprintf("%s:%s#%s", "members", userGroupID, memberRelation)})
|
||||
}
|
||||
|
||||
func (svc service) ListPolicies(ctx context.Context, pr PolicyReq) (PolicyPage, error) {
|
||||
res, err := svc.agent.RetrievePolicies(ctx, pr)
|
||||
if err != nil {
|
||||
return PolicyPage{}, err
|
||||
}
|
||||
var page PolicyPage
|
||||
for _, tuple := range res {
|
||||
page.Policies = append(page.Policies, tuple.GetObject())
|
||||
}
|
||||
return page, err
|
||||
}
|
||||
|
||||
func (svc service) tmpKey(duration time.Duration, key Key) (Key, string, error) {
|
||||
key.ExpiresAt = key.IssuedAt.Add(duration)
|
||||
secret, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
return key, secret, nil
|
||||
}
|
||||
|
||||
func (svc service) userKey(ctx context.Context, token string, key Key) (Key, string, error) {
|
||||
id, sub, err := svc.login(token)
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
key.IssuerID = id
|
||||
if key.Subject == "" {
|
||||
key.Subject = sub
|
||||
}
|
||||
|
||||
keyID, err := svc.idProvider.ID()
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.ID = keyID
|
||||
|
||||
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
secret, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
return key, secret, nil
|
||||
}
|
||||
|
||||
func (svc service) login(token string) (string, string, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Only login key token is valid for login.
|
||||
if key.Type != LoginKey || key.IssuerID == "" {
|
||||
return "", "", errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return key.IssuerID, key.Subject, nil
|
||||
}
|
||||
|
||||
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
ulid, err := svc.ulidProvider.ID()
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
timestamp := getTimestmap()
|
||||
group.UpdatedAt = timestamp
|
||||
group.CreatedAt = timestamp
|
||||
|
||||
group.ID = ulid
|
||||
group.OwnerID = user.ID
|
||||
|
||||
group, err = svc.groups.Save(ctx, group)
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: group.ID, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (svc service) ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAll(ctx, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListParents(ctx context.Context, token string, childID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAllParents(ctx, childID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListChildren(ctx context.Context, token string, parentID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAllChildren(ctx, parentID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListMembers(ctx context.Context, token string, groupID, groupType string, pm PageMetadata) (MemberPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return MemberPage{}, err
|
||||
}
|
||||
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
|
||||
if err != nil {
|
||||
return MemberPage{}, errors.Wrap(ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.groups.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
group.UpdatedAt = getTimestmap()
|
||||
return svc.groups.Update(ctx, group)
|
||||
}
|
||||
|
||||
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
return svc.groups.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) Assign(ctx context.Context, token string, groupID, groupType string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.groups.Assign(ctx, groupID, groupType, memberIDs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if groupType == thingsGroupType {
|
||||
ss := fmt.Sprintf("%s:%s#%s", "members", groupID, memberRelation)
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
for _, action := range []string{"read", "write", "delete"} {
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: memberID, Relation: action, Subject: ss}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add thing: '%s' to thing group: '%s'", memberID, groupID), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: groupID, Relation: memberRelation, Subject: memberID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add user: '%s' to user group: '%s'", memberID, groupID), errs)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ss := fmt.Sprintf("%s:%s#%s", "members", groupID, memberRelation)
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
// If the member is a user, <groupID>#member@memberID must be deleted.
|
||||
if err := svc.agent.DeletePolicy(ctx, PolicyReq{Object: groupID, Relation: memberRelation, Subject: memberID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete a membership of member '%s' from group '%s'", memberID, groupID), errs)
|
||||
}
|
||||
|
||||
// If the member is a Thing, memberID#read|write|delete@(members:groupID#member) must be deleted.
|
||||
for _, action := range []string{"read", "write", "delete"} {
|
||||
if err := svc.agent.DeletePolicy(ctx, PolicyReq{Object: memberID, Relation: action, Subject: ss}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy from member '%s'", action, memberID), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := svc.groups.Unassign(ctx, groupID, memberIDs...)
|
||||
return errors.Wrap(err, errs)
|
||||
}
|
||||
|
||||
func (svc service) ListMemberships(ctx context.Context, token string, memberID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.Memberships(ctx, memberID, pm)
|
||||
}
|
||||
|
||||
func getTimestmap() time.Time {
|
||||
return time.Now().UTC().Round(time.Millisecond)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
package auth
|
||||
|
||||
// Tokenizer specifies API for encoding and decoding between string and Key.
|
||||
type Tokenizer interface {
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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/auth"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
assign = "assign"
|
||||
saveGroup = "save_group"
|
||||
deleteGroup = "delete_group"
|
||||
updateGroup = "update_group"
|
||||
retrieveByID = "retrieve_by_id"
|
||||
retrieveAllParents = "retrieve_all_parents"
|
||||
retrieveAllChildren = "retrieve_all_children"
|
||||
retrieveAll = "retrieve_all_groups"
|
||||
memberships = "memberships"
|
||||
members = "members"
|
||||
unassign = "unassign"
|
||||
)
|
||||
|
||||
var _ auth.GroupRepository = (*groupRepositoryMiddleware)(nil)
|
||||
|
||||
type groupRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo auth.GroupRepository
|
||||
}
|
||||
|
||||
// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
|
||||
func GroupRepositoryMiddleware(tracer opentracing.Tracer, gr auth.GroupRepository) auth.GroupRepository {
|
||||
return groupRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: gr,
|
||||
}
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, saveGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Save(ctx, g)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, updateGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Update(ctx, g)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, deleteGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Delete(ctx, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveByID)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllParents)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllParents(ctx, groupID, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllChildren)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllChildren(ctx, groupID, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAll)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAll(ctx, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, memberships)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Memberships(ctx, memberID, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, members)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Members(ctx, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
|
||||
span := createSpan(ctx, grm.tracer, assign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Assign(ctx, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
|
||||
span := createSpan(ctx, grm.tracer, unassign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Unassign(ctx, groupID, memberIDs...)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ package tracing
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
@@ -18,25 +18,26 @@ const (
|
||||
revokeOp = "remove"
|
||||
)
|
||||
|
||||
var _ authn.KeyRepository = (*keyRepositoryMiddleware)(nil)
|
||||
var _ auth.KeyRepository = (*keyRepositoryMiddleware)(nil)
|
||||
|
||||
// keyRepositoryMiddleware tracks request and their latency, and adds spans
|
||||
// to context.
|
||||
type keyRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo authn.KeyRepository
|
||||
repo auth.KeyRepository
|
||||
}
|
||||
|
||||
// New tracks request and their latency, and adds spans
|
||||
// to context.
|
||||
func New(repo authn.KeyRepository, tracer opentracing.Tracer) authn.KeyRepository {
|
||||
func New(repo auth.KeyRepository, tracer opentracing.Tracer) auth.KeyRepository {
|
||||
return keyRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
|
||||
func (krm keyRepositoryMiddleware) Save(ctx context.Context, key auth.Key) (string, error) {
|
||||
span := createSpan(ctx, krm.tracer, saveOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
@@ -44,7 +45,7 @@ func (krm keyRepositoryMiddleware) Save(ctx context.Context, key authn.Key) (str
|
||||
return krm.repo.Save(ctx, key)
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) Retrieve(ctx context.Context, owner, id string) (authn.Key, error) {
|
||||
func (krm keyRepositoryMiddleware) Retrieve(ctx context.Context, owner, id string) (auth.Key, error) {
|
||||
span := createSpan(ctx, krm.tracer, retrieveOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
-1666
File diff suppressed because it is too large
Load Diff
-49
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package mainflux;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service ThingsService {
|
||||
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
|
||||
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
|
||||
rpc Identify(Token) returns (ThingID) {}
|
||||
}
|
||||
|
||||
service AuthNService {
|
||||
rpc Issue(IssueReq) returns (Token) {}
|
||||
rpc Identify(Token) returns (UserID) {}
|
||||
}
|
||||
|
||||
message AccessByKeyReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ThingID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message AccessByIDReq {
|
||||
string thingID = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
// If a token is not carrying any information itself, the type
|
||||
// field can be used to determine how to validate the token.
|
||||
// Also, different tokens can be encoded in different ways.
|
||||
message Token {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message UserID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message IssueReq {
|
||||
string issuer = 1;
|
||||
uint32 type = 2;
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
# Authentication service
|
||||
|
||||
Authentication service provides an API for managing authentication keys.
|
||||
|
||||
There are *three types of authentication keys*:
|
||||
|
||||
- user key - keys issued to the user upon login request
|
||||
- API key - keys issued upon the user request
|
||||
- recovery key - password recovery key
|
||||
|
||||
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user. API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. Recovery key is the password recovery key. It's short-lived token used for password recovery process.
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
The following actions are supported:
|
||||
|
||||
- create (all key types)
|
||||
- verify (all key types)
|
||||
- obtain (API keys only; secret is never obtained)
|
||||
- revoke (API keys only)
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------|
|
||||
| MF_AUTHN_LOG_LEVEL | Service level (debug, info, warn, error) | error |
|
||||
| MF_AUTHN_DB_HOST | Database host address | localhost |
|
||||
| MF_AUTHN_DB_PORT | Database host port | 5432 |
|
||||
| MF_AUTHN_DB_USER | Database user | mainflux |
|
||||
| MF_AUTHN_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_AUTHN_DB | Name of the database used by the service | auth |
|
||||
| MF_AUTHN_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_AUTHN_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_AUTHN_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_AUTHN_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_AUTHN_HTTP_PORT | Authn service HTTP port | 8180 |
|
||||
| MF_AUTHN_GRPC_PORT | Authn service gRPC port | 8181 |
|
||||
| MF_AUTHN_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTHN_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTHN_SECRET | String used for signing tokens | auth |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
authn:
|
||||
image: mainflux/authn:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_AUTHN_LOG_LEVEL: [Service log level]
|
||||
MF_AUTHN_DB_HOST: [Database host address]
|
||||
MF_AUTHN_DB_PORT: [Database host port]
|
||||
MF_AUTHN_DB_USER: [Database user]
|
||||
MF_AUTHN_DB_PASS: [Database password]
|
||||
MF_AUTHN_DB: [Name of the database used by the service]
|
||||
MF_AUTHN_DB_SSL_MODE: [SSL mode to connect to the database with]
|
||||
MF_AUTHN_DB_SSL_CERT: [Path to the PEM encoded certificate file]
|
||||
MF_AUTHN_DB_SSL_KEY: [Path to the PEM encoded key file]
|
||||
MF_AUTHN_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
|
||||
MF_AUTHN_HTTP_PORT: [Service HTTP port]
|
||||
MF_AUTHN_GRPC_PORT: [Service gRPC port]
|
||||
MF_AUTHN_SECRET: [String used for signing tokens]
|
||||
MF_AUTHN_SERVER_CERT: [String path to server certificate in pem format]
|
||||
MF_AUTHN_SERVER_KEY: [String path to server key in pem format]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the service
|
||||
make authn
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_AUTHN_LOG_LEVEL=[Service log level] MF_AUTHN_DB_HOST=[Database host address] MF_AUTHN_DB_PORT=[Database host port] MF_AUTHN_DB_USER=[Database user] MF_AUTHN_DB_PASS=[Database password] MF_AUTHN_DB=[Name of the database used by the service] MF_AUTHN_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTHN_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTHN_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTHN_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTHN_HTTP_PORT=[Service HTTP port] MF_AUTHN_GRPC_PORT=[Service gRPC port] MF_AUTHN_SECRET=[String used for signing tokens] MF_AUTHN_SERVER_CERT=[Path to server certificate] MF_AUTHN_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-authn
|
||||
```
|
||||
|
||||
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthNServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthNServiceClient {
|
||||
return &grpcClient{
|
||||
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.AuthNService",
|
||||
"Issue",
|
||||
encodeIssueRequest,
|
||||
decodeIssueResponse,
|
||||
mainflux.UserID{},
|
||||
).Endpoint()),
|
||||
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.AuthNService",
|
||||
"Identify",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
mainflux.UserID{},
|
||||
).Endpoint()),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.issue(ctx, issueReq{issuer: req.GetIssuer(), keyType: req.Type})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.Token{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &mainflux.IssueReq{Issuer: req.issuer, Type: req.keyType}, nil
|
||||
}
|
||||
|
||||
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserID)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserID, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.UserID{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(identityReq)
|
||||
return &mainflux.Token{Value: req.token}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserID)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc contains implementation of AuthN service gRPC API.
|
||||
package grpc
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
key := authn.Key{
|
||||
Type: req.keyType,
|
||||
IssuedAt: now,
|
||||
}
|
||||
|
||||
k, err := svc.Issue(ctx, req.issuer, key)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
return identityRes{k.Secret, nil}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identifyEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
return identityRes{id, nil}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
)
|
||||
|
||||
var svc authn.Service
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc authn.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterAuthNServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
kind uint32
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
id: email,
|
||||
kind: authn.UserKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
id: email,
|
||||
kind: authn.RecoveryKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
id: userKey.Secret,
|
||||
kind: authn.APIKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
id: email,
|
||||
kind: 32,
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
kind: authn.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Issuer: tc.id, Type: tc.kind})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
|
||||
|
||||
apiKey, err := svc.Issue(context.Background(), userKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "identify user with recovery token",
|
||||
token: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with API token",
|
||||
token: apiKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
id: "",
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user that doesn't exist",
|
||||
token: "",
|
||||
id: "",
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.id, id.GetValue()))
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import "github.com/mainflux/mainflux/authn"
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
kind uint32
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
if req.kind != authn.UserKey &&
|
||||
req.kind != authn.APIKey &&
|
||||
req.kind != authn.RecoveryKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type issueReq struct {
|
||||
issuer string
|
||||
keyType uint32
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.issuer == "" {
|
||||
return authn.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.keyType != authn.UserKey &&
|
||||
req.keyType != authn.APIKey &&
|
||||
req.keyType != authn.RecoveryKey {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthNServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
issue kitgrpc.Handler
|
||||
identify kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new AuthnServiceServer instance.
|
||||
func NewServer(tracer opentracing.Tracer, svc authn.Service) mainflux.AuthNServiceServer {
|
||||
return &grpcServer{
|
||||
issue: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssueRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
identify: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
|
||||
decodeIdentifyRequest,
|
||||
encodeIdentifyResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) Issue(ctx context.Context, req *mainflux.IssueReq) (*mainflux.Token, error) {
|
||||
_, res, err := s.issue.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.Token), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.UserID, error) {
|
||||
_, res, err := s.identify.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.UserID), nil
|
||||
}
|
||||
|
||||
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.IssueReq)
|
||||
return issueReq{issuer: req.GetIssuer(), keyType: req.GetType()}, nil
|
||||
}
|
||||
|
||||
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(identityRes)
|
||||
return &mainflux.Token{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.Token)
|
||||
return identityReq{token: req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(identityRes)
|
||||
return &mainflux.UserID{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, authn.ErrMalformedEntity):
|
||||
return status.Error(codes.InvalidArgument, "received invalid token request")
|
||||
case errors.Contains(err, authn.ErrUnauthorizedAccess):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, authn.ErrKeyExpired):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
type issueKeyReq struct {
|
||||
issuer string
|
||||
Type uint32 `json:"type,omitempty"`
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
// It is not possible to issue Reset key using HTTP API.
|
||||
func (req issueKeyReq) validate() error {
|
||||
if req.Type == authn.UserKey {
|
||||
return nil
|
||||
}
|
||||
if req.issuer == "" || (req.Type != authn.APIKey) {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type keyReq struct {
|
||||
issuer string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req keyReq) validate() error {
|
||||
if req.issuer == "" || req.id == "" {
|
||||
return authn.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ authn.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc authn.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc authn.Service, logger log.Logger) authn.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Issue(ctx context.Context, issuer string, newKey authn.Key) (key authn.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
d := "infinite duration"
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
|
||||
}
|
||||
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Issue(ctx, issuer, newKey)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Revoke(ctx context.Context, owner, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Revoke(ctx, owner, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Retrieve(ctx context.Context, owner, id string) (key authn.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Retrieve(ctx, owner, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identify(ctx, key)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc authn.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc authn.Service, counter metrics.Counter, latency metrics.Histogram) authn.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Issue(ctx context.Context, issuer string, key authn.Key) (authn.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue").Add(1)
|
||||
ms.latency.With("method", "issue").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Issue(ctx, issuer, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Revoke(ctx context.Context, issuer, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke").Add(1)
|
||||
ms.latency.With("method", "revoke").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Revoke(ctx, issuer, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve").Add(1)
|
||||
ms.latency.With("method", "retrieve").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Retrieve(ctx, issuer, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identify").Add(1)
|
||||
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identify(ctx, key)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- authn
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyRequest"
|
||||
responses:
|
||||
201:
|
||||
description: Issued new key.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using already existing ID.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/keys/{id}:
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
secret:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
|
||||
description: API Key value.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ID:
|
||||
name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: e-mail
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
@@ -1,146 +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/pkg/errors"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeySave(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
id, _ := uuidProvider.New().ID()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a new key",
|
||||
key: authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with duplicate id",
|
||||
key: authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
},
|
||||
err: authn.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(context.Background(), tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyRetrieve(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
id, _ := uuidProvider.New().ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
}
|
||||
_, err := repo.Save(context.Background(), key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: key.ID,
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unauthorized",
|
||||
id: key.ID,
|
||||
issuer: "",
|
||||
err: authn.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unknown key",
|
||||
id: "",
|
||||
issuer: key.Issuer,
|
||||
err: authn.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Retrieve(context.Background(), tc.issuer, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyRemove(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
id, _ := uuidProvider.New().ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
}
|
||||
_, err := repo.Save(opentracing.ContextWithSpan(context.Background(), opentracing.StartSpan("")), key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing key",
|
||||
id: key.ID,
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove key that does not exist",
|
||||
id: key.ID,
|
||||
issuer: key.Issuer,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Remove(context.Background(), tc.issuer, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
loginDuration = 10 * time.Hour
|
||||
recoveryDuration = 5 * time.Minute
|
||||
issuerName = "mainflux.authn"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnauthorizedAccess represents unauthorized access.
|
||||
ErrUnauthorizedAccess = errors.New("unauthorized access")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid owner or ID).
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrNotFound indicates a non-existing entity request.
|
||||
ErrNotFound = errors.New("entity not found")
|
||||
|
||||
// ErrConflict indicates that entity already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new user key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
)
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Issue issues a new Key.
|
||||
Issue(context.Context, string, Key) (Key, error)
|
||||
|
||||
// Revoke removes the Key with the provided id that is
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(context.Context, string, string) error
|
||||
|
||||
// Retrieve retrieves data for the Key identified by the provided
|
||||
// ID, that is issued by the user identified by the provided key.
|
||||
Retrieve(context.Context, string, string) (Key, error)
|
||||
|
||||
// Identify validates token token. If token is valid, content
|
||||
// is returned. If token is invalid, or invocation failed for some
|
||||
// other reason, non-nil error value is returned in response.
|
||||
Identify(context.Context, string) (string, error)
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
uuidProvider mainflux.UUIDProvider
|
||||
tokenizer Tokenizer
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, up mainflux.UUIDProvider, tokenizer Tokenizer) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
uuidProvider: up,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Issue(ctx context.Context, issuer string, key Key) (Key, error) {
|
||||
if key.IssuedAt.IsZero() {
|
||||
return Key{}, ErrInvalidKeyIssuedAt
|
||||
}
|
||||
switch key.Type {
|
||||
case APIKey:
|
||||
return svc.userKey(ctx, issuer, key)
|
||||
case RecoveryKey:
|
||||
return svc.tmpKey(issuer, recoveryDuration, key)
|
||||
default:
|
||||
return svc.tmpKey(issuer, loginDuration, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, issuer, id string) error {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
if err := svc.keys.Remove(ctx, email, id); err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error) {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return svc.keys.Retrieve(ctx, email, id)
|
||||
}
|
||||
|
||||
func (svc service) Identify(ctx context.Context, token string) (string, error) {
|
||||
c, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errIdentify, err)
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case APIKey:
|
||||
k, err := svc.keys.Retrieve(ctx, c.Issuer, c.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Auto revoke expired key.
|
||||
if k.Expired() {
|
||||
svc.keys.Remove(ctx, c.Issuer, c.ID)
|
||||
return "", ErrKeyExpired
|
||||
}
|
||||
return c.Issuer, nil
|
||||
case RecoveryKey, UserKey:
|
||||
if c.Issuer != issuerName {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
return c.Secret, nil
|
||||
default:
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) tmpKey(issuer string, duration time.Duration, key Key) (Key, error) {
|
||||
key.Secret = issuer
|
||||
key.Issuer = issuerName
|
||||
key.ExpiresAt = key.IssuedAt.Add(duration)
|
||||
val, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
key.Secret = val
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, error) {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.Issuer = email
|
||||
|
||||
id, err := svc.uuidProvider.ID()
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.ID = id
|
||||
|
||||
value, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.Secret = value
|
||||
|
||||
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (svc service) login(token string) (string, error) {
|
||||
c, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Only user key token is valid for login.
|
||||
if c.Type != UserKey {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if c.Secret == "" {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
return c.Secret, nil
|
||||
}
|
||||
@@ -1,280 +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/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
)
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue user key",
|
||||
key: authn.Key{
|
||||
Type: authn.UserKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue user key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.UserKey,
|
||||
},
|
||||
issuer: email,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthorized",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "issue API key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
key: authn.Key{
|
||||
Type: authn.RecoveryKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.RecoveryKey,
|
||||
},
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Issue(context.Background(), tc.issuer, tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke user key",
|
||||
id: newKey.ID,
|
||||
issuer: loginKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke non-existing user key",
|
||||
id: newKey.ID,
|
||||
issuer: loginKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke unauthorized",
|
||||
id: newKey.ID,
|
||||
issuer: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Revoke(context.Background(), tc.issuer, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := authn.Key{
|
||||
ID: "id",
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
resetKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
|
||||
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
issuer string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve user key",
|
||||
id: newKey.ID,
|
||||
issuer: loginKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existing user key",
|
||||
id: "invalid",
|
||||
issuer: loginKey.Secret,
|
||||
err: authn.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unauthorized",
|
||||
id: newKey.ID,
|
||||
issuer: "wrong",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with user key",
|
||||
id: newKey.ID,
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with reset key",
|
||||
id: newKey.ID,
|
||||
issuer: resetKey.Secret,
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Retrieve(context.Background(), tc.issuer, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestIdentify(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
|
||||
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
exp1 := time.Now().Add(-2 * time.Second)
|
||||
expKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing expired user key expected to succeed: %s", err))
|
||||
|
||||
invalidKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: 22, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "identify login key",
|
||||
key: loginKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify recovery key",
|
||||
key: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify user key",
|
||||
key: userKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify expired user key",
|
||||
key: expKey.Secret,
|
||||
id: "",
|
||||
err: authn.ErrKeyExpired,
|
||||
},
|
||||
{
|
||||
desc: "identify expired key",
|
||||
key: invalidKey.Secret,
|
||||
id: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "identify invalid key",
|
||||
key: "invalid",
|
||||
id: "",
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := svc.Identify(context.Background(), tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.id, id))
|
||||
}
|
||||
}
|
||||
+9
-51
@@ -36,7 +36,7 @@ Thing configuration also contains the so-called `external ID` and `external key`
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-------------------------------|-------------------------------------------------------------------------|----------------------- |
|
||||
|-------------------------------|-------------------------------------------------------------------------|----------------------------------|
|
||||
| MF_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | error |
|
||||
| MF_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MF_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
@@ -63,55 +63,13 @@ The service is configured using the environment variables presented in the follo
|
||||
| MF_BOOTSTRAP_ES_DB | Bootstrap service event source database | 0 |
|
||||
| MF_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_AUTHN_GRPC_URL | AuthN service gRPC URL | localhost:8181 |
|
||||
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1s |
|
||||
| MF_AUTH_GRPC_URL | Auth service gRPC URL | localhost:8181 |
|
||||
| MF_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
|
||||
|
||||
## 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]
|
||||
```
|
||||
The service itself is distributed as Docker container. Check the [`boostrap`](https://github.com/mainflux/mainflux/blob/master/docker/addons/bootstrap/docker-compose.yml#L32-L56) service section in
|
||||
docker-compose to see how service is deployed.
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
@@ -147,8 +105,8 @@ 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] \
|
||||
MF_AUTH_GRPC_URL=[Auth service gRPC URL] \
|
||||
MF_AUTH_GRPC_TIMEOUT=[Auth service gRPC request timeout in seconds] \
|
||||
$GOBIN/mainflux-bootstrap
|
||||
```
|
||||
|
||||
@@ -157,6 +115,6 @@ Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. Thi
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yml).
|
||||
the [API documentation](https://api.mainflux.io/?urls.primaryName=bootstrap-openapi.yml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
[doc]: https://docs.mainflux.io
|
||||
|
||||
+18
-18
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
@@ -34,7 +34,7 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(req.token, config)
|
||||
saved, err := svc.Add(ctx, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,13 +49,13 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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 {
|
||||
if err := svc.UpdateCert(ctx, req.token, req.thingID, req.ClientCert, req.ClientKey, req.CACert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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)
|
||||
config, err := svc.View(ctx, req.token, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
@@ -116,7 +116,7 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(req.key, config); err != nil {
|
||||
if err := svc.Update(ctx, req.token, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -130,14 +130,14 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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 {
|
||||
if err := svc.UpdateConnections(ctx, req.token, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -151,14 +151,14 @@ func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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)
|
||||
page, err := svc.List(ctx, req.token, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,14 +197,14 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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 {
|
||||
if err := svc.Remove(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -213,13 +213,13 @@ func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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)
|
||||
cfg, err := svc.Bootstrap(ctx, req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -229,14 +229,14 @@ func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, sec
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx 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 {
|
||||
if err := svc.ChangeState(ctx, req.token, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+105
-100
@@ -4,6 +4,7 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
@@ -22,6 +23,9 @@ import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
bsapi "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
@@ -79,11 +83,14 @@ var (
|
||||
CACert: "newca",
|
||||
}
|
||||
|
||||
bsErrorRes = toJSON(errorRes{bootstrap.ErrBootstrap.Error()})
|
||||
unauthRes = toJSON(errorRes{bootstrap.ErrUnauthorizedAccess.Error()})
|
||||
malformedRes = toJSON(errorRes{bootstrap.ErrMalformedEntity.Error()})
|
||||
extKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrExternalKeyNotFound.Error()})
|
||||
extSecKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrSecureBootstrap.Error()})
|
||||
bsErrorRes = toJSON(apiutil.ErrorRes{Err: bootstrap.ErrBootstrap.Error()})
|
||||
authnRes = toJSON(apiutil.ErrorRes{Err: errors.ErrAuthentication.Error()})
|
||||
authzRes = toJSON(apiutil.ErrorRes{Err: errors.ErrAuthorization.Error()})
|
||||
malformedRes = toJSON(apiutil.ErrorRes{Err: errors.ErrMalformedEntity.Error()})
|
||||
extKeyRes = toJSON(apiutil.ErrorRes{Err: bootstrap.ErrExternalKey.Error()})
|
||||
extSecKeyRes = toJSON(apiutil.ErrorRes{Err: bootstrap.ErrExternalKeySecure.Error()})
|
||||
missingIDRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrMissingID.Error()})
|
||||
missingKeyRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrBearerKey.Error()})
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -92,6 +99,7 @@ type testRequest struct {
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
key string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
@@ -116,7 +124,10 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
}
|
||||
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.key != "" {
|
||||
req.Header.Set("Authorization", apiutil.ThingPrefix+tr.key)
|
||||
}
|
||||
|
||||
if tr.contentType != "" {
|
||||
@@ -147,7 +158,7 @@ func dec(in []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, bootstrap.ErrMalformedEntity
|
||||
return nil, errors.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
@@ -156,14 +167,14 @@ func dec(in []byte) ([]byte, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func newService(authn mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
ThingsURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(authn, things, sdk, encKey)
|
||||
return bootstrap.New(auth, things, sdk, encKey)
|
||||
}
|
||||
|
||||
func generateChannels() map[string]things.Channel {
|
||||
@@ -179,17 +190,19 @@ func generateChannels() map[string]things.Channel {
|
||||
return channels
|
||||
}
|
||||
|
||||
func newThingsService(authn mainflux.AuthNServiceClient) things.Service {
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), authn)
|
||||
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), auth)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := thingsapi.MakeHandler(mocktracer.New(), svc)
|
||||
logger := logger.NewMock()
|
||||
mux := thingsapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newBootstrapServer(svc bootstrap.Service) *httptest.Server {
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader(encKey))
|
||||
logger := logger.NewMock()
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader(encKey), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -199,10 +212,10 @@ func toJSON(data interface{}) string {
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
data := toJSON(addReq)
|
||||
@@ -224,11 +237,11 @@ func TestAdd(t *testing.T) {
|
||||
location string
|
||||
}{
|
||||
{
|
||||
desc: "add a config unauthorized",
|
||||
desc: "add a config with invalid token",
|
||||
req: data,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
location: "",
|
||||
},
|
||||
{
|
||||
@@ -313,6 +326,7 @@ func TestAdd(t *testing.T) {
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
@@ -323,10 +337,10 @@ func TestAdd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
c := newConfig([]bootstrap.Channel{})
|
||||
|
||||
@@ -339,7 +353,7 @@ func TestView(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var channels []channel
|
||||
@@ -366,10 +380,10 @@ func TestView(t *testing.T) {
|
||||
res config
|
||||
}{
|
||||
{
|
||||
desc: "view a config unauthorized",
|
||||
desc: "view a config with invalid token",
|
||||
auth: invalidToken,
|
||||
id: saved.MFThing,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: config{},
|
||||
},
|
||||
{
|
||||
@@ -390,7 +404,7 @@ func TestView(t *testing.T) {
|
||||
desc: "view a config with an empty token",
|
||||
auth: "",
|
||||
id: saved.MFThing,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: config{},
|
||||
},
|
||||
}
|
||||
@@ -420,15 +434,15 @@ func TestView(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
data := toJSON(updateReq)
|
||||
@@ -442,12 +456,12 @@ func TestUpdate(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "update unauthorized",
|
||||
desc: "update with invalid token",
|
||||
req: data,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update with an empty token",
|
||||
@@ -455,7 +469,7 @@ func TestUpdate(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update a valid config",
|
||||
@@ -514,15 +528,15 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
data := toJSON(updateReq)
|
||||
@@ -536,12 +550,12 @@ func TestUpdateCert(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "update unauthorized",
|
||||
desc: "update with invalid token",
|
||||
req: data,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update with an empty token",
|
||||
@@ -549,7 +563,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update a valid config",
|
||||
@@ -609,15 +623,15 @@ func TestUpdateCert(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
data := toJSON(updateReq)
|
||||
@@ -636,12 +650,12 @@ func TestUpdateConnections(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "update connections unauthorized",
|
||||
desc: "update connections with invalid token",
|
||||
req: data,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update connections with an empty token",
|
||||
@@ -649,7 +663,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update connections valid config",
|
||||
@@ -722,13 +736,13 @@ func TestList(t *testing.T) {
|
||||
var active, inactive []config
|
||||
list := make([]config, configNum)
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/configs")
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
for i := 0; i < configNum; i++ {
|
||||
c.ExternalID = strconv.Itoa(i)
|
||||
@@ -736,7 +750,7 @@ func TestList(t *testing.T) {
|
||||
c.Name = fmt.Sprintf("%s-%d", addName, i)
|
||||
c.ExternalKey = fmt.Sprintf("%s%s", addExternalKey, strconv.Itoa(i))
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var channels []channel
|
||||
@@ -762,7 +776,7 @@ func TestList(t *testing.T) {
|
||||
if i%2 == 0 {
|
||||
state = bootstrap.Inactive
|
||||
}
|
||||
err := svc.ChangeState(validToken, list[i].MFThing, state)
|
||||
err := svc.ChangeState(context.Background(), validToken, list[i].MFThing, state)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
|
||||
list[i].State = state
|
||||
if state == bootstrap.Inactive {
|
||||
@@ -780,17 +794,17 @@ func TestList(t *testing.T) {
|
||||
res configPage
|
||||
}{
|
||||
{
|
||||
desc: "view list unauthorized",
|
||||
desc: "view list with invalid token",
|
||||
auth: invalidToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 10),
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
desc: "view list with an empty token",
|
||||
auth: "",
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 10),
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
@@ -833,13 +847,8 @@ func TestList(t *testing.T) {
|
||||
desc: "view with limit greater than allowed",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 1000),
|
||||
status: http.StatusOK,
|
||||
res: configPage{
|
||||
Total: uint64(len(list)),
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Configs: list[:100],
|
||||
},
|
||||
status: http.StatusBadRequest,
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
desc: "view list with no specified limit and offset",
|
||||
@@ -892,7 +901,7 @@ func TestList(t *testing.T) {
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
desc: "view list with invalid query params",
|
||||
desc: "view list with invalid query parameters",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&state=%d&key=%%", path, 10, 10, bootstrap.Inactive),
|
||||
status: http.StatusBadRequest,
|
||||
@@ -970,15 +979,15 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -988,15 +997,15 @@ func TestRemove(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "remove unauthorized",
|
||||
desc: "remove with invalid token",
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
}, {
|
||||
desc: "remove with an empty token",
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existing config",
|
||||
@@ -1032,15 +1041,15 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
encExternKey, err := enc([]byte(c.ExternalKey))
|
||||
@@ -1092,23 +1101,23 @@ func TestBootstrap(t *testing.T) {
|
||||
externalID: "",
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusBadRequest,
|
||||
res: malformedRes,
|
||||
res: missingIDRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with unknown key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: unknown,
|
||||
status: http.StatusNotFound,
|
||||
res: extKeyNotFoundRes,
|
||||
status: http.StatusForbidden,
|
||||
res: extKeyRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with an empty key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "",
|
||||
status: http.StatusForbidden,
|
||||
res: unauthRes,
|
||||
status: http.StatusUnauthorized,
|
||||
res: missingKeyRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
@@ -1131,8 +1140,8 @@ func TestBootstrap(t *testing.T) {
|
||||
desc: "bootstrap secure with unencrypted key",
|
||||
externalID: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: extSecKeyNotFoundRes,
|
||||
status: http.StatusForbidden,
|
||||
res: extSecKeyRes,
|
||||
secure: true,
|
||||
},
|
||||
}
|
||||
@@ -1142,7 +1151,7 @@ func TestBootstrap(t *testing.T) {
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/things/bootstrap/%s", bs.URL, tc.externalID),
|
||||
token: tc.externalKey,
|
||||
key: tc.externalKey,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
@@ -1160,15 +1169,15 @@ func TestBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
inactive := fmt.Sprintf("{\"state\": %d}", bootstrap.Inactive)
|
||||
@@ -1183,12 +1192,12 @@ func TestChangeState(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "change state unauthorized",
|
||||
desc: "change state with invalid token",
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
state: active,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "change state with an empty token",
|
||||
@@ -1196,7 +1205,7 @@ func TestChangeState(t *testing.T) {
|
||||
auth: "",
|
||||
state: active,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "change state with invalid content type",
|
||||
@@ -1286,7 +1295,3 @@ type configPage struct {
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []config `json:"configs"`
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+28
-27
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -25,7 +26,7 @@ func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Se
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) Add(ctx context.Context, 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 {
|
||||
@@ -35,10 +36,10 @@ func (lm *loggingMiddleware) Add(token string, cfg bootstrap.Config) (saved boot
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(token, cfg)
|
||||
return lm.svc.Add(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, 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 {
|
||||
@@ -48,10 +49,10 @@ func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(token, id)
|
||||
return lm.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, 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 {
|
||||
@@ -61,10 +62,10 @@ func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err err
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(token, cfg)
|
||||
return lm.svc.Update(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, 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 {
|
||||
@@ -74,10 +75,10 @@ func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, c
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(token, thingID, clientCert, clientKey, caCert)
|
||||
return lm.svc.UpdateCert(ctx, token, thingID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, 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 {
|
||||
@@ -87,10 +88,10 @@ func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []s
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(token, id, connections)
|
||||
return lm.svc.UpdateConnections(ctx, token, id, connections)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, 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 {
|
||||
@@ -100,10 +101,10 @@ func (lm *loggingMiddleware) List(token string, filter bootstrap.Filter, offset,
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(token, filter, offset, limit)
|
||||
return lm.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Remove(token, id string) (err error) {
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, 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 {
|
||||
@@ -113,10 +114,10 @@ func (lm *loggingMiddleware) Remove(token, id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(token, id)
|
||||
return lm.svc.Remove(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, 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 {
|
||||
@@ -126,10 +127,10 @@ func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bo
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
func (lm *loggingMiddleware) ChangeState(ctx context.Context, 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 {
|
||||
@@ -139,10 +140,10 @@ func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(token, id, state)
|
||||
return lm.svc.ChangeState(ctx, token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, 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 {
|
||||
@@ -152,10 +153,10 @@ func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (er
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(channel)
|
||||
return lm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, 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 {
|
||||
@@ -165,10 +166,10 @@ func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(id)
|
||||
return lm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, 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 {
|
||||
@@ -178,10 +179,10 @@ func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(id)
|
||||
return lm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
func (lm *loggingMiddleware) DisconnectThingHandler(ctx context.Context, 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 {
|
||||
@@ -191,5 +192,5 @@ func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
return lm.svc.DisconnectThingHandler(ctx, channelID, thingID)
|
||||
}
|
||||
|
||||
+29
-29
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
@@ -20,8 +21,7 @@ type metricsMiddleware struct {
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
// 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,
|
||||
@@ -30,119 +30,119 @@ func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency m
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) Add(ctx context.Context, 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)
|
||||
return mm.svc.Add(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) View(ctx context.Context, 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)
|
||||
return mm.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
func (mm *metricsMiddleware) Update(ctx context.Context, 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)
|
||||
return mm.svc.Update(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, 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)
|
||||
return mm.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, 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)
|
||||
return mm.svc.UpdateConnections(ctx, token, id, connections)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, 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)
|
||||
return mm.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Remove(token, id string) (err error) {
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, 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)
|
||||
return mm.svc.Remove(ctx, token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, 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)
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, 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)
|
||||
return mm.svc.ChangeState(ctx, token, id, state)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, 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)
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, 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)
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, 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)
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(ctx context.Context, 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)
|
||||
return mm.svc.DisconnectThingHandler(ctx, channelID, thingID)
|
||||
}
|
||||
|
||||
+42
-33
@@ -3,7 +3,12 @@
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/bootstrap"
|
||||
import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
@@ -24,54 +29,58 @@ type addReq struct {
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" || req.ExternalKey == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
key string
|
||||
id string
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
key string
|
||||
token string
|
||||
thingID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
@@ -79,49 +88,49 @@ type updateCertReq struct {
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.thingID == "" {
|
||||
return bootstrap.ErrNotFound
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
key string
|
||||
token string
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.limit == 0 || req.limit > maxLimit {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -134,34 +143,34 @@ type bootstrapReq struct {
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrBootstrapState
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -21,21 +22,21 @@ func TestAddReqValidation(t *testing.T) {
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,28 +54,28 @@ func TestAddReqValidation(t *testing.T) {
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
@@ -84,29 +85,29 @@ func TestEntityReqValidation(t *testing.T) {
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
@@ -117,27 +118,27 @@ func TestUpdateReqValidation(t *testing.T) {
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
thingID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
thingID: "thingID",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty thing key",
|
||||
key: "key",
|
||||
desc: "empty thing id",
|
||||
token: "token",
|
||||
thingID: "",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
thingID: tc.thingID,
|
||||
}
|
||||
|
||||
@@ -148,29 +149,29 @@ func TestUpdateCertReqValidation(t *testing.T) {
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
@@ -182,36 +183,36 @@ func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
key string
|
||||
token string
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "too large limit",
|
||||
key: "key",
|
||||
token: "token",
|
||||
offset: 0,
|
||||
limit: maxLimit + 1,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
limit: maxLimitSize + 1,
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "zero limit",
|
||||
key: "key",
|
||||
desc: "default limit",
|
||||
token: "token",
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
limit: defLimit,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
@@ -232,13 +233,13 @@ func TestBootstrapReqValidation(t *testing.T) {
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -256,37 +257,37 @@ func TestBootstrapReqValidation(t *testing.T) {
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
token: "token",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
key: "key",
|
||||
token: "token",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrBootstrapState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
@@ -121,7 +121,3 @@ func (res stateRes) Headers() map[string]string {
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+88
-112
@@ -6,39 +6,37 @@ 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/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxLimit = 100
|
||||
defaultLimit = 10
|
||||
contentType = "application/json"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 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"}
|
||||
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 {
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger logger.Logger) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
r := bone.New()
|
||||
|
||||
@@ -102,7 +100,7 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version("bootstrap"))
|
||||
r.GetFunc("/health", mainflux.Health("bootstrap"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
@@ -110,12 +108,12 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{token: r.Header.Get("Authorization")}
|
||||
req := addReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -123,13 +121,15 @@ func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
req := updateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -137,16 +137,15 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
thingID: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -154,36 +153,41 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
req := updateConnReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
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)
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := parseFilter(q)
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errors.ErrInvalidQueryParams
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: filter,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
filter: parseFilter(q),
|
||||
offset: o,
|
||||
limit: l,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -192,7 +196,7 @@ func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := bootstrapReq{
|
||||
id: bone.GetValue(r, "external_id"),
|
||||
key: r.Header.Get("Authorization"),
|
||||
key: apiutil.ExtractThingKey(r),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -200,13 +204,15 @@ func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, er
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
req := changeStateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -214,8 +220,8 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := entityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -250,76 +256,46 @@ func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interfac
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken,
|
||||
err == apiutil.ErrBearerKey:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errors.Contains(err, errors.ErrInvalidQueryParams),
|
||||
errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrBootstrapState,
|
||||
err == apiutil.ErrLimitSize:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, bootstrap.ErrExternalKey),
|
||||
errors.Contains(err, bootstrap.ErrExternalKeySecure),
|
||||
errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, bootstrap.ErrThings):
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
case errors.Contains(err, errors.ErrCreateEntity),
|
||||
errors.Contains(err, errors.ErrUpdateEntity),
|
||||
errors.Contains(err, errors.ErrViewEntity),
|
||||
errors.Contains(err, errors.ErrRemoveEntity):
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func parseUint(s string) (uint64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user