Compare commits

..

162 Commits

Author SHA1 Message Date
Dušan Borovčanin 92d8fb99bf NOISSUE - Update changelog for release 0.13.0 (#1595)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-04-15 15:51:35 +02:00
Dušan Borovčanin ad80cf868b Update VerneMQ release (#1593)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-04-15 15:25:39 +02:00
Dušan Borovčanin f0dac5c6ac NOISSUE - Update changelog and readme for release 0.13.0 (#1592)
* Update release example

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update changelog and examples for 0.13.0 release

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-04-15 12:37:55 +02:00
Manuel Imperiale 48d6a95a21 MF-1582 - Fix lora-adapter MQTT client (#1583)
* MF-1582 - Fix lora-adapter MQTT clien

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add timeout config to the mqtt subscriber

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm comment

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add sub timeout

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-04-11 13:52:20 +02:00
Dušan Borovčanin c6f7c69798 NOISSUE - Fix CoAP adapter (#1572)
* Revert "NOISSUE - Add nats wrapper for COAP (#1569)"

This reverts commit cc5d5195ab.

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix CoAP adapter

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update CoAP observation cancel

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix observe

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix GET handling

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Revert authorization

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use constants instead of magic numbers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove an empty line

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Extract special observe value to constant

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-03-30 16:52:10 +02:00
Manuel Imperiale ce46723a8f NOISSUE - Unify MF_INFLUX_READER_DB_HOST and MF_INFLUX_WRITER_DB_HOST envars (#1585)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-03-29 11:53:27 +02:00
Hasan98-git 29f9cbb10a MF-1580 - Influxdb Writer changes format of update-time to string (#1581)
* - MF-1580
- Modified consumers/writers/influxdb/fields.go
- influxdb-writer used to update data type of update-time to string
- Commented line 12 of consumers/writers/influxdb/fields.go to resolve
  uneccessary data type conversion issue

Signed-off-by: Hasan Tariq <hasantariqashraf@gmail.com>

* - MF-1580
- Removed strconv package from consumers/writers/influxdb/fields.go since it is no longer needed
- Removed line 12 from consumers/writers/influxdb/fields.go
- Replaced retrun value of updateTime with msg.UpdateTime (line 16 in
  fields.go)

Signed-off-by: Hasan Tariq <hasantariqashraf@gmail.com>

* Fix InflxuDB readers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Hasan Tariq <hasant@plcgroup.com>
Co-authored-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-03-23 13:19:59 +01:00
ibalboteo 127044efad MF-1575 Add 'Name' field to ListMembers response in things svc (#1576)
Signed-off-by: Ivan Balboteo <ivan@submer.com>

Co-authored-by: Ivan Balboteo <ivan@submer.com>
2022-03-14 11:03:07 +01:00
Manuel Imperiale f3ed852b36 MF-1565 - Document Bearer, Thing and Basic Authorization header (#1566)
* MF-1565 - Document Bearer Authorization header

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix auth, bootstrap, http and readers openapi

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix openapi

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add enc key for bootstrap

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use global security

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix bearer formats

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Polish descriptions

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix boostrap and typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2022-03-06 01:58:47 +01:00
Manuel Imperiale 0a6b2f135a MF-1567 - Use Bearer, Thing or Basic scheme in Authorization header (#1568)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-03-06 01:49:34 +01:00
Manuel Imperiale e5278c463f MF-1348 - Add transport errors logging (#1544)
* MF-1348 - Add go-kit transport level logging

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix merge

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix remark

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix go test flags

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use httputil errors in things and http service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix SDK tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use httputil errors in certs and provision service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use httputil errors in consumers service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* General renaming and add ErrMissingToken

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename httputil -> apiutil and use errors in users servive

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use apiutil errors in auth, bootstrap, readers, things and twins

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Replace errors.Contain by comparison

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix remarks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify validateID

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify validateID

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify and rename ExtractAuthToken -> ExtractBearerToken

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix readers

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix auth key test and remarks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Improve comment

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify validateUUID check

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-03-03 17:13:46 +01:00
b1ackd0t cc5d5195ab NOISSUE - Add nats wrapper for COAP (#1569)
* Add nats wrapper for COAP

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Pass pubsub as argument

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Defer close connection

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Defer close connection

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Rename endpoint to topic

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>
2022-03-02 23:37:54 +01:00
stefankovacevic123 5ca8495f35 MF-1469 - Indicate proper authentication scheme in Authorization header (#1523)
* MF-1469 - Indicate proper authentication scheme in Authorization header

Signed-off-by: Stefan Kovacevic <jen2tri@gmail.com>

* Fixing the remarks on the last push

Signed-off-by: Stefan Kovacevic <jen2tri@gmail.com>

* Remove Bearer prefix in all services and fix tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix remarks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-02-18 14:56:01 +01:00
Manuel Imperiale 9972d1d1a4 MF-1240 - Return to service transport layer only service errors (#1559)
* MF-1240 - Return to service transport layer only service errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove unecessary errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm duplicated errors and fix transport

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert http endpoint_test

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix conflict

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-02-14 22:49:23 +01:00
Dušan Borovčanin 8b94199785 Update dependencies (#1564)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-02-12 19:07:48 +01:00
Ivan Milošević d51a79c538 NOISSUE - Separate Keto hosts for read and write (#1563)
* Separate keto hosts for read and write

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* update readme with new envars

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* rename read connection name

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2022-02-11 14:27:33 +01:00
Manuel Imperiale 09cbc3f14b MF-1551 - Fix Cobra usage commands and clean unnecessary struct types (#1558)
* MF-1551 - Fix Cobra usage commands and clean unnecessary struct types

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use linux syntax for cmd usage description

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix cmd.Use

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-02-11 13:22:57 +01:00
Mirko Teodorovic 655ac838be MF-1257 - Access messages from readers endpoint with user access token (#1470)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add user auth for db reader

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* add user auth for db reader

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* enable mongodb reader for user token reading

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* use uuid check for auth switch between thing key and user tok

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* enable user token reading

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* revert to correct version

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix endpoint test, add additional tests

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove logs,dead code

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix logging messages

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove auth interface, add authorization header type

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* update api doc

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove unused package

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* some refactor of cases for authorization switch

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* correct description in openapi

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix endpoint test to match auth service change

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* some rename

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* initialize auth url

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* add env variables for auth service

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix spelling

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* Things prefix and no prefix for Thing authorization, Bearer for user

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* update readme file

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix default things grpc port

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* enable user reading for timescaledb

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove not used error

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* improve errors

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* refactor authorize

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* add chanID check

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* inline some error checking

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fixing errors

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fixing errors

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* improve test case description

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove test code

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* dont inline

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* refactor a bit encodeError

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove unused error

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* remove unused error

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix things auth grpc url

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* rename variables for header prefix

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-02-09 21:25:34 +01:00
__touk__ f8ce94e9bb NOISSUE - Refactor MQTT subscriber (#1561)
* correct suscriber interface validator + refactore token error handling

Signed-off-by: tzzed <zerouali.t@gmail.com>

* apply review suggestion

Signed-off-by: tzzed <zerouali.t@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-02-09 10:19:09 +01:00
Ivan Milošević 1f8a221c22 MF-1059 - Add TLS support for email (#1560)
* Use gomail package for sending emails

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove print err

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Add vendor

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Rename email structure
remove logger

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* typo in var name

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* rename var

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove MF_EMAIL_SECRET

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2022-02-07 13:51:43 +01:00
Manuel Imperiale 9e0947a355 MF-1261 - Use StatusUnauthorized for authn and StatusForbidden for authz (#1538)
* MF-1261 - Use StatusUnauthorized for authn and StatusForbidden for authz

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* ErrExternalKey typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename ErrUnauthorizedAcces -> ErrAuthentication

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix bootstrap error

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix status code in openapi

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test description

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test description

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test description

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add errors cases

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix status codes

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add gRPC stutus code

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix tests description

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix openapi and encodeError

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix grpc message

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test descriptions

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert sdk error

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-02-01 17:33:23 +01:00
Manuel Imperiale e8a51842aa NOISSUE - Fix auth members list response (#1555)
* NOISSUE - Fix auth members list response

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Move group type next to page details

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm membersRes

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-01-27 18:39:35 +01:00
Manuel Imperiale 6ad654d7cb MF-1263 - Move repeating errors to the separate package (#1540)
* MF-1263 - Mv duplicated errors to pkg/errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert test build flags

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix merge

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix comment

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-01-27 17:03:57 +01:00
Filip Bugarski 4f56986c8e NOISSUE - Add API keys functions to CLI (#1537)
* Add API keys to cli

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix typo

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix issue func, and sdk

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Remove email, hide type and fix name

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Remove type and add README

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix issue response

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix magic number and move struct to private

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Remove const and import auth

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Removing auth

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-01-27 15:42:31 +01:00
b1ackd0t 81b2a1a2d3 Fix SDK for group members (#1553)
Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>
2022-01-27 11:08:27 +01:00
Ivan Milošević 89061d33e5 NOISSUE - Fix Swagger UI (#1552)
* test github action

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* separate openapi and asyncapi

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove openapi from old location

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* change cname for production use

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2022-01-26 10:33:08 +01:00
b1ackd0t 2abf9da27e MF-1008 - Make token duration configurable (#1550)
* Make token duration configurable

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change env variable name

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Add MF_AUTH_LOGIN_TOKEN_DURATION in environment variable

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change tests

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Document MF_AUTH_LOGIN_TOKEN_DURATION

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change default login duration

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change declaration of loginduration

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Add space after port

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change time to hours

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Remove constant login duration

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Keep back recovery duration

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change docs for login token duration

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Change login description

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>

* Remove blank lines

Signed-off-by: 0x6f736f646f <blackd0t@protonmail.com>
2022-01-25 19:42:41 +01:00
Manuel Imperiale 42dd813521 MF-1308 - Use IETF Health Check standard (#1541)
* MF-1308 - Use IETF Health Check standard

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add nginx health endpoint

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm github.com/nelkinda dependency

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Check error

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Replace Version by Health in the CLI and SDK

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use new build flag go:build

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert wrong renaming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* sdk health test

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add /health endpoint to openapi doc

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use const for description message

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add version and build time during build

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Time format

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add version and commit using git and build args

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add missing api properties

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix api

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use ./schemas/HealthInfo.yml as

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix example

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use content type application/health+json

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Set Makefile variables only if empty

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-01-24 21:18:53 +01:00
Dušan Borovčanin bcc8cf7119 Fix user listing access control (#1546)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-01-19 16:46:10 +01:00
Dušan Borovčanin d86a4dee92 Update dependencies (#1545)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2022-01-18 15:28:46 +01:00
JohnC 864ad144e6 MF-1478 - TimescaleDB writer and reader add-on (#1542)
* MF-1478 - TimescaleDb Reader/Writer Add-on

Signed-off-by: John Cleasby <john.cleasby@gmail.com>

* pull request 1542 change requests

Signed-off-by: corp0529 <john.cleasby@gmail.com>

* pull request 1542 change requests 2

Signed-off-by: corp0529 <john.cleasby@gmail.com>

* removed unused separator const from timescale reader and writer cmd

Signed-off-by: corp0529 <john.cleasby@gmail.com>

* Fixed naming of timescaleRepo instance

Signed-off-by: corp0529 <john.cleasby@gmail.com>

* Fixed indentation and renamed repo to tr

Signed-off-by: corp0529 <john.cleasby@gmail.com>
2022-01-18 12:08:54 +01:00
Manuel Imperiale bea09d97dd MF-1149 - Add AsyncAPI MQTT API doc (#1539)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-01-14 23:53:35 +01:00
Filip Bugarski 6483969927 MF-1535 - Add API keys functions to SDK (#1536)
* Add API keys to sdk.

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Add issue key

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix typo

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix comments

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Remove unused code

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix typo

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>
2022-01-12 13:24:51 +01:00
Manuel Imperiale aa014c2191 NOISSUE - Add view and list serials endpoints in certs service (#1483)
* NOISSUE - Add view and list serials endpoints in certs service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix vault-unseal.sh script

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename Cert field days_valid into hours_valid

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix provision service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use ownerID, rename daysValid -> hoursValid

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add key_type to api

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix tabulation

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add expiration date in view response

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename HoursValid -> Expiration and remove unecessary expiration convertion

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add ListSerials tests and fix mocks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix RetrieveByThing count

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add ViewCert tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add missing error check

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify API

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert Makefile

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* NOISSUE - Add view and list serials endpoints in certs service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix vault-unseal.sh script

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename Cert field days_valid into hours_valid

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix provision service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use ownerID, rename daysValid -> hoursValid

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add key_type to api

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix tabulation

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add expiration date in view response

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename HoursValid -> Expiration and remove unecessary expiration convertion

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add ListSerials tests and fix mocks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix RetrieveByThing count

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add ViewCert tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add missing error check

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify API

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert Makefile

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm if else

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename HoursValid -> TTL

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* revert typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* revert typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename hoursValid -> ttl

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2022-01-04 19:42:13 +01:00
Filip Bugarski dd7d52ef10 MF-1516 - Fix API key issuing (#1530)
* Fix API keys saving

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix API key creation

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix tests

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Delete empty lines

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Remove empty lines

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Fix typo

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Change user key to login key

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>
2021-12-24 14:53:06 +01:00
Manuel Imperiale ad8b7ddf5a NOISSUE - Add disconnect endpoint in nginx conf (#1528)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-12-15 13:20:56 +01:00
Manuel Imperiale b18c9e79dc NOISSUE - Add timestamp transformation rules for specifc JSON fields (#1514)
* NOISSUE - Add timestamp transformation rules for specifc JSON fields

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add tests and defaults

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix Created

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix READMEs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix Config file

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Improve the timestamp Config structure

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typos

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix smpp

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm duplicated Unmarshal

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Extract the error var

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Return error if time field transformation fails

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-12-14 11:17:23 +01:00
qs-wang 7e9ab453d4 MF-1425 - Support external UUIDs for Things and Channels (#1518)
* MF-1425 - Rebase mainflux master to resolve conflicts

MF-1425 Enhancement to supply an external UUID for Things and Channels.
Resolve conflicts.




Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

* MF-1425 - Test cases changes for SDK

MF-1425 Enhancement to supply an external UUID for Things and Channels.
These are the new testcases added for
- Things Service Testcases
- SDK Things and Channel Testcases

Signed-off-by: Anand Sivaram Palassery <aspnair@gmail.com>
Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

* MF-1425 - Fixing Testcases

MF-1425 Enhancement to supply an external UUID for Things and Channels.
Because of the previous commits, the testcases were getting failed
because the way ID was modified.  This change is to make sure that
all testcases are revisited to get them fixed.

Signed-off-by: Anand Sivaram Palassery <aspnair@gmail.com>
Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

* MF-1425 - Fixing review comments

Fixing the review comments provided.

Signed-off-by: Anand Sivaram Palassery <aspnair@gmail.com>
Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

* MF-1425 - Fixing more review comments

Signed-off-by: Anand Sivaram Palassery <aspnair@gmail.com>
Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

* MF-1425 - Fixing conflicts

MF-1425 Enhancement to supply an external UUID for Things and Channels.
Fixing the conflicts between aspnair master, and the mainflux master.

Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

* MF-1425 Fix the comment and code format per review comments

MF-1425 Enhancement to supply an external UUID for Things and Channels.
1. Remove un-valued comment for a private function
2. Format the code for better readibility

Signed-off-by: Q.S. Wang <wangqs_eclipse@yahoo.com>

* MF-1425 Enhancement to supply an external UUID for Things and Channels.

Fix the format of the API document

Signed-off-by: Q.S. Wang <wangqs_eclipse@yahoo.com>

* MF-1425 Enhancement to supply an external UUID for Things and Channels.

Rename the variable to make it readible.

Signed-off-by: Q.s <wangqs_eclipse@yahoo.com>

Co-authored-by: Anand Sivaram Palassery <aspnair@gmail.com>
Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2021-12-08 12:46:59 +01:00
Ivan Milošević 309ef512cb MF-1521 - Fix email headers (#1522)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-12-08 10:48:05 +01:00
Dušan Borovčanin db6fab961e Fix SenML lib dependency version (#1519)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-12-06 19:03:09 +01:00
Ivan Milošević f99f5d228d Bump vernemq to 1.12.3 (#1520)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-12-06 16:13:34 +01:00
Filip Bugarski 5112ef681a NOISSUE - Remove auth URL from SDK (#1511)
* Remove auth URL from SDK

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Update CLi/main.go authURL to GroupsURL

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Add authURL back

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Refactor group structures

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Add AuthURL back to cli/main.go

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>
2021-12-02 13:50:39 +01:00
Burak Sekili 31d30b204d NOISSUE - Apply policies to Channels (#1505)
* Add policies for channels

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update single channel retrieval

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update indentation

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

update indentation

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-11-26 14:12:08 +01:00
Burak Sekili be3e98f677 remove dead code (#1503)
Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-11-24 16:55:29 +01:00
Burak Sekili 87510288c2 NOISSUE - Fix listing (#1502)
Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-11-19 17:25:48 +01:00
Burak Sekili b78928c998 NOISSUE - Listing Policies (#1498)
* allow admin to fetch all things

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* enable users to fetch their own things via owner field in db

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* add listpolicies RPC

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* add listPolicies gRPC methods for client and server, and update keto initialization

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update fetching things method

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* remove log

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update retrieving policies

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* fix linter error

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update mock

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* remove checking subject set while parsing subject sets

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* move subject declaration to constant value

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-11-19 14:32:38 +01:00
Dušan Borovčanin 39133b06a4 Fix standalone mode (#1497)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-11-16 13:02:31 +01:00
Burak Sekili 3042d6b40b MF-1489 - Add API for deleting policies (#1491)
* MF-1489 - Add API for deleting policies

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update request and decoder naming

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update swagger doc summary for the endpoint

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-11-08 14:45:38 +01:00
Burak Sekili c70fb576b6 NOISSUE - Update group sharing policies (#1494)
* Update group sharing policies

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Replace string with constant

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-11-08 12:55:18 +01:00
__touk__ 9f5a319519 NOISSUE - Refactor InfluxDB Reader: explicit check event + add safe conversion (#1460)
* refactore(reader/influxdb): explicit check event + add safe conversion

Signed-off-by: tzzed <zerouali.t@gmail.com>

* apply review suggestion

Signed-off-by: tzzed <zerouali.t@gmail.com>

* Delete useless space.

Signed-off-by: tzzed <zerouali.t@gmail.com>

* apply review suggestion

Signed-off-by: tzzed <zerouali.t@gmail.com>

* apply review suggestion, replace require.NoError(t, err) by assert.Nil

Signed-off-by: tzzed <zerouali.t@gmail.com>

* Fix tests when message time is randomly equal to 0 in tests cases.

Signed-off-by: tzzed <zerouali.t@gmail.com>

* apply review suggestion use constant

Signed-off-by: tzzed <zerouali.t@gmail.com>

* use const offset

Signed-off-by: tzzed <zerouali.t@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2021-11-08 12:26:11 +01:00
Burak Sekili b570c38ed0 NOISSUE - Update users create command for CLI (#1495)
* NOISSUE - Update users create command for CLI

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* update if check

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-11-08 11:40:27 +01:00
Burak Sekili dc935858bd NOISSUE - Update self register environment variable name (#1493)
* NOISSUE - Update naming of env variable (#1484)

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-11-03 10:30:52 +01:00
Drasko DRASKOVIC 90e4561491 Bring back the job add 2021-11-01 00:48:57 +01:00
Burak Sekili 30912e5a45 NOISSUE - Fix assigning invalid group policy (#1487)
Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-10-28 13:10:03 +02:00
Dušan Borovčanin 27d4646db4 MF-1443 - Add policies (#1482)
* MF-1443 - add policies

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* fix users create

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* MF-1454 - Add Policies for sharing a Thing (#1463)

* MF-1454 - Add policies for sharing a Thing

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Add a test case for sharing thing and update mock of AddPolicy

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update ShareThing parameter naming

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* MF-1454 - Policy Removal  (#1466)

* Add DeletePolicy gRPC endpoint in auth package

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update default admin creation

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Add policy addition endpoint (#1479)

* NOISSUE - Add policy addition endpoint

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update name of the method

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

remove build tag

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Add tests for AddPolicies (#1480)

* NOISSUE - Add tests for adding policy and update authz check

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Add more tests and update request body validation

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update test case structure and utilize mock prefix for test ids

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* MF-1454 - Add initial policies for Group access control (#1467)

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Resolve PR comments

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Author: Burak Sekili <buraksekili@gmail.com>
2021-10-27 00:38:28 +02:00
Burak Sekili 042ff98509 NOISSUE - Fix retrieving all users (#1477)
Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-10-18 17:00:15 +02:00
smartyfi 5e9a91bd03 MF-1468 - Fix ThingsURL in Certs Service (#1474)
* fix issue #1468

Signed-off-by: smartyfi <a.shabrawy@smarty-fi.com>

* update env variable to MF_THINGS_URL

Signed-off-by: smartyfi <a.shabrawy@smarty-fi.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-10-16 00:26:18 +02:00
Dušan Borovčanin 39ae7bdfa1 NOISSUE - Refactor single-user mode (#1471)
* Refactor single user mode

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Twins dependency

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename `single-user` to `standalone`

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-10-11 16:08:26 +02:00
Dušan Borovčanin 94cba4aab2 Fix UpdateChannelHandler for Redis producer (#1473)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-10-11 15:36:35 +02:00
Ivan Milošević 66d3da0531 NOISSUE - Add SMPP notifier (#1464)
* Add SMPP notifier

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix readme
remove env file

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* resolve conversations

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Remove debug log

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Rename transmiter and transformer fields in struct

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix typo

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-10-06 14:34:23 +02:00
Dušan Borovčanin f4312aef8a NOISSUE - Update dependencies (#1453)
* Fix Dockerfile base Go version

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update Go version and dependencies

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-08-24 13:41:57 +02:00
Dušan Borovčanin caa4bda1a1 NOISSUE - Fix security warnings for dependencies (#1452)
* Fix security warnings for dependencies

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Omit `.mod` and `.sum` files in dependencies

This feature is introduced here: https://tip.golang.org/doc/go1.17#vendor.

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-08-23 15:26:35 +02:00
Drasko DRASKOVIC e1a66b3268 Bump docker-compose version in prereq (#1449)
Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>
2021-08-13 18:30:14 +02:00
Mirko Teodorovic 7b3c26f60f NOISSUE - Fix bootstraping (#1448)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix url for sdk

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* revert init.go

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>
2021-08-13 13:56:05 +02:00
Drasko DRASKOVIC d73a5d53fe MF 1413 - Use per-service URL in SDK (#1444)
* Use per-service URL in SDK

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Fix CLI

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Fix CLI messaging

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Fix message tests

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Simplify Bootstrap

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Update API doc and responses

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* fix failing certs, bootstrap tests

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* fix failing certs, bootstrap tests

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* Fix tests and rename to auth service

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Clean the code

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Remove unnecessary Repository logs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Always return error in case of repo failure

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Cleanup SDK and CLI

Update tests, remove linter warnings, remove dead code.

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Clean the code

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Undo Bootstrap changes

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Fix tests

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

* Fix linter

Signed-off-by: Drasko Draskovic <drasko.draskovic@gmail.com>

Co-authored-by: mteodor <mirko.teodorovic@gmail.com>
Co-authored-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-08-11 16:58:10 +02:00
Dušan Borovčanin d6a3830ef4 MF-1439 - Add support for Basic Authentication in HTTP Adapter (#1441)
* Add support for basic auth for HTTP adapter

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix API docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Inline BasicAuth check

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-07-22 15:47:55 +02:00
Burak Sekili 5ac1203b55 MF-1421 - Make flattening of JSON transformer only available on InfluxDB (#1432)
* MF-1421 - Add a flag for making flattening JSON transformer optional

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Add test cases for JSON transformer without flattening

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Add a comment for Transform

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Separate TestTransformJSON into two tests

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Replace flatten flag

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Remove unnecessary flattening while reading a message

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-07-22 11:20:47 +02:00
Burak Sekili 19f0437f57 NOISSUE - Update the /disconnect endpoint HTTP method as PUT (#1438)
* NOISSUE - Update the /disconnect endpoint HTTP method as POST

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update disconnection request name in spec

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

Update description

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Replaced POST with PUT

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-07-13 13:31:20 +02:00
Burak Sekili 2cfff01979 MF-1389 - Add /disconnect endpoint in Things service (#1433)
* MF-1389 - Add /disconnect endpoint in Things service

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Add db transaction in Postgres' Disconnect

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Reformat things mock and things http api

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update naming of /disconnect endpoint decoder

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update naming for /connect endpoint

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

Update naming based on new endpoint

Signed-off-by: Burak Sekili <buraksekili@gmail.com>

* Update disconnect endpoint test case descriptions

Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-07-10 00:59:12 +02:00
Manuel Imperiale bb072b8ad2 NOISSUE - Fix httputil implementation in users service (#1434)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-07-08 16:14:49 +02:00
Burak Sekili af0162f0df Fix fetching user members of an empty group (#1436)
Signed-off-by: Burak Sekili <buraksekili@gmail.com>
2021-07-08 13:51:37 +02:00
Ivan Milošević 68af0e32b5 Change to user friendly docs urls (#1430)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-06-23 14:52:29 +02:00
Ivan Milošević cddfdf4038 NOISSUE - Use github action for showing OpenAPI spec with Swagger UI (#1427)
* init swaggerui action

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove dep

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix filename

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add checkout master

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* try diffrent action

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* change path for search

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* change path

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* change path

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* put almost all openapi spec to one folder

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix pattern

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* try servers changing

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* test k8s

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove servers

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* use blokovi action

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add cname

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Parameters ready for PR to upstream

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Fix errors in Auth openapi spec

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove white line

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix link in README

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* change docs and api links in readme files

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-06-23 13:31:46 +02:00
Dušan Borovčanin 0a89f1dae1 Fix JSON Transformer empty format handling (#1429)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-06-22 12:32:16 +02:00
Drasko DRASKOVIC d3e34b1662 Update README
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-06-14 23:39:05 +02:00
Manuel Imperiale 9e5e50b347 NOISSUE - Update docker-compose images to latest release (#1419)
* MF-1390 - Fix docker-compose env_file

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Update README

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* NOISSUE - Update docker-compose images to latest release

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use Postgres 13.3-alpine

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-05-24 18:35:07 +02:00
Dušan Borovčanin 516c02bebe MF-1378 - Update dependencies (#1379)
* Update dependencies

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix compose files and configs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Upgrade image versions

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update Postgres version

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update test dependencies

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix fkey error handling

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-05-20 20:53:56 +02:00
Manuel Imperiale 38ca7f761b NOISSUE - Update version.go with 0.12.1 (#1418)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-05-11 11:49:41 +02:00
Manuel Imperiale 9ce31b65a6 NOISSUE - Update CHANGELOG for release 0.12.1 (#1415)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-05-07 09:55:10 +02:00
Mirko Teodorovic 79af1ba9a8 NOISSUE - Refactor SDK memberships and fix openapi for memberships. (#1412)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix groups sdk

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move Memberships from users to groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove tree param for memberships

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add memberships endpoint

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert file to master ver

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-05-03 22:27:10 +02:00
Dušan Borovčanin e04d94ecc7 MF-1288 - Add tests for JSON messages in message writers and readers (#1407)
* Add JSON Writer tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Posgres Reader JSON tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add ID comment

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add MongoDB Reader tests for JSON

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename test message

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add tests for InfluxDB JSON messages

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use test file for constants block

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename MongoDB imports

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Cassandra reader JSON tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Simplify test payload

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Handle wrong format uniformly across Readers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-04-20 21:33:26 +02:00
chengyou tang e02e9c2387 MF-1408-Fix error handling for Thing update SQL(#1408) (#1409)
Signed-off-by: Chengyou Tang <tcy277813009@gmail.com>

Co-authored-by: Chengyou Tang <tcy277813009@gmail.com>
Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2021-04-20 14:26:16 +02:00
Mirko Teodorovic 063f73076d fix influx pass (#1410)
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-04-20 12:11:42 +02:00
Dušan Borovčanin 84978c034b Fix order (#1406)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-04-19 11:54:08 +02:00
Manuel Imperiale 0fe9f55bae NOISSUE - Add tests and connection route-map to lora-adapter (#1404)
* NOISSUE - Add tests and connection route-map to lora-adapter

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename LoraServer -> ChirpStack and fix links

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove ctx arg from Publish method

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert LoRa Server

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix links

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Check ids before to modify connection route-map

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Log conn errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-04-14 22:26:49 +02:00
Mirko Teodorovic 00dc197fa6 NOISSUE - Fix nginx configuration for groups (#1405)
* fix configuration for groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add location header

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix for https

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-04-09 17:25:20 +02:00
Ivan Milošević cd11728073 Change vernemq building source revision (#1402)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-04-08 18:16:21 +02:00
Manuel Imperiale 6770c19279 NOISSUE - Rm content-type check from list endpoint (#1400)
* NOISSUE - Rm content-type check from list endpoint

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix decode func naming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-03-30 11:32:28 +02:00
Drasko DRASKOVIC 4efd25eed5 Bump Mainflux version to 0.12.0 (#1399)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-03-29 12:13:20 +02:00
Drasko DRASKOVIC 6b6f18317f Add CHANGELOG for 0.12.0 (#1398)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-03-29 00:57:08 +02:00
Mirko Teodorovic 0cdcf28683 MF-1394 - SDK groups (#1396)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix groups sdk

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix groups sdk

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix groups sdk

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* small fix, revert some changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix test fail

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix unassign request

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* decouple util method form group repo

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix param parsing

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-26 22:51:02 +01:00
Mirko Teodorovic f9f51470b1 NOISSUE - fix response for passwd endpoints (#1393)
* fix response for passwd endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix test for passwd related responses

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix test for passwd related responses

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-23 17:43:06 +01:00
Mirko Teodorovic e87715ba31 dont retrieve groups (#1392)
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-23 12:20:56 +01:00
Manuel Imperiale 9245e2593b MF-1368 - Add internal http api package for query params reading (#1384)
* MF-1368 - Add internal http api package for query params reading

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use internal/http and internalhttp alias

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv errors types to pkg

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use httputil/query.go and remove aliases

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add blank lines after error definitions

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add ReadBoolValueQuery

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv readBoolValueQuery

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* User ErrNotFoundParam instead of pointer

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert ReadUintQuery to use default values

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use default values for all query readers

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-03-23 11:48:05 +01:00
Manuel Imperiale bcdc6d21f6 MF-1390 - Fix docker-compose env_file (#1391)
* MF-1390 - Fix docker-compose env_file

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Update README

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-03-23 11:39:40 +01:00
Ivan Milošević 0f3a262ca3 put order direction in response body (#1387)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-03-15 19:55:57 +01:00
Mirko Teodorovic 74aa93fbb6 NOISSUE - Certs service refactor (#1369)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add certs mock

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove not wanted changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* refactor certs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* addint tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* addint tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add certs test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add certs test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add cert test, remove default implementation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix default value for vault host

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add cert test, remove default implementation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* linter cleaning

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix comments, and logging

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* use mocks from other services

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename struct and url path params

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve minor comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* align url params naming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix typo

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove struct revoke

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* refactor certRes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-15 12:27:32 +01:00
Ivan Milošević 30ba38c919 MF-1357 - Add new endpoint for searching things (#1383)
* Add new enpoint for thing search

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Rename endpoint to /search
Use same request as list endpoint

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Add optional parameters in body (offset, limit)
Add swagger file

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* move all parameters into body

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix swagger

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix error description

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Add tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove dead code
fix tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove unused var

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix sdk tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add url endpoint for search test

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* description in swagger
fix tracer string
change test offset

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* rename in tests searchThReq to searchThingReq

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-03-11 10:28:44 +01:00
Ivan Milošević a1e18a770a init commmit (#1385)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-03-10 11:52:33 +01:00
Mirko Teodorovic 259950b009 NOISSUE - Add missing auth port in nginx enrypoint.sh (#1380)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix auth http port

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-04 19:02:59 +01:00
Mirko Teodorovic 530f925c4d MF-1346 - Create Groups API - add grouping of entities (#1334)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add users endpoint for retrieving users from group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove  groups from things and users

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups into auth

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* separate endpoints for users and things

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix problems with retrieving members

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove groups from users

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove groups from things

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename constant

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add new errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unnecessary constants

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix validation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* create groups db mock

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert changes to docker related files

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove groups endpoints from users openapi

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove groups endpoints from users openapi

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move constant from postgres to groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move constant from postgres to groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move constant from postgres to groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove testing group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renam typ to groupType

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add error for max level

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove print

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove groups.Member interface

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix query building and add test cases

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* uncomment tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups package

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove group type, add bulk assign and unassign

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update openapi, remove parentID from create request, reorder endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update openapi

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update openapi for users and things

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix groups test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix linter errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename assignReq structure

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* refactor mocks, response, remove type from endpoint

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* some refactor, renaming, errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* simplify check

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove package alias

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix naming and comment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* additional comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add members grpc endpoint test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix retrieving members for different types

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix retrieving members for different types

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unecessary structure

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix api grpc

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename const

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* refactore retrieve parents and children with common function

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* small changes for errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix compile error

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix sorting in mock

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove regexp for groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert as change is made by mistake

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert as change is made by mistake

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* refactor groups and keys package

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix naming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix naming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix test for timestamp compare

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix error handling

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove errors not being used

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* var renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* minor changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add endpoints for groups into nginx

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* reorganize endpoints, remove some errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* reorganize endpoints, remove some errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* small fix

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix linter errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* minor changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix group save path problem

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* description constant

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename variables

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix validation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* get back return

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix compile

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-04 10:29:03 +01:00
Mirko Teodorovic 6b1f4d54f8 NOISSUE - Fix certs and vault deployment, reorganize and remove unnecessary vars (#1368)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix certs, fix scripts, reorganize env vars

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* set pki path vars

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add certs mock

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove not wanted changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update readme

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* correct rsa_bits to key_bits

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix tabulation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* hardcode vault version

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add env desc

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renam variables

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* use Mainflux Labs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace BG with Belgrade

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-03-02 12:20:52 +01:00
Ivan Milošević 7bcaa323d4 MF-1317 - Configurable regexp rule for password (#1355)
* read and validate regex envar

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* pass regexp to user/api

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* resolve conflicts

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* use exported regexp variable

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* move password validation from users package

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove dead code

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add password change request

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* move regexp from api to users package

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove commented code

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add regexp as field in userService, remove it as user exported global var

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add passwd validation in service

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Add psswd validation for change password in service

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add password validation in password reset

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Remove password validation from user validation test

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Replace email and passwords in test with constants

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* compile error not fail silently

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix tempate path

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-03-01 15:22:57 +01:00
Dušan Borovčanin e334569d81 Fix CoAP Adapter README (#1376)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-03-01 12:25:39 +01:00
Mirko Teodorovic 0432b2aa03 NOISSUE - Fix default values for port and x509 provision (#1367)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix default port and certs settings

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* small typo fix

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* refactor env vars

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert users

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-02-27 01:01:48 +01:00
Filip Bugarski 56d04cda64 NOISSUE - Added missing endpoints for users service (#1372)
* Added missing endpoints for user's service

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>

* Type fix

Signed-off-by: Filip Bugarski <filipbugarski@gmail.com>
2021-02-25 17:19:35 +01:00
Nikola Marčetić e01874e557 MF-1365 - Add ADOPTERS.md file (#1371)
* Added ADOPTERS.md file

Signed-off-by: Nikola Marcetic <n.marcetic86@gmail.com>

* Add Note

Signed-off-by: Nikola Marcetic <n.marcetic86@gmail.com>

* Remarks resolved

Signed-off-by: Nikola Marcetic <n.marcetic86@gmail.com>
2021-02-25 13:46:07 +01:00
Darko Draskovic 9e532d2a72 Fix grpc endpoint parameter permutation (#1370)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2021-02-24 13:23:50 +01:00
Darko Draskovic 7ba3fdb3bf NOISSUE - Add IsChannelOwner grpc endpoint (#1366)
* Add CanAccessChannelByOwner grpc endpoint

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Rename grpc endpoint to IsChannelOwner

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add tests for IsChannelOwner grpc

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Bump protoc version

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Make proto

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2021-02-22 19:41:59 +01:00
Manuel Imperiale 7834cc48b3 MF-1362 - Sort Things and Channels connections by name (#1363)
* MF-1362 - Sort Things and Channels connections by name

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove test

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add order and direction tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-02-17 16:21:40 +01:00
Manuel Imperiale f0f60e2d2a MF-1314 - Add value comparison filters for readers (#1353)
* MF-1314 - Add value comparison filters for readers

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Check if comparison parameter is valid

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use eq, lt, lte, gt, gte as comparison operator keys

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use consts for comparison operators

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use comparator naming instead of comparison

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix openapi.yml

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-02-09 22:44:04 +01:00
Dušan Borovčanin 24b902d049 Fix env configuration and documentation (#1360)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-02-08 22:38:52 +01:00
Dušan Borovčanin 13c426c09c NOISSUE - Support disabling Email Agent authentication (#1356)
* Fix mailer configuration

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove unused Driver option

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Email Agent README

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-02-08 13:21:52 +01:00
Manuel Imperiale ca5ff63dfb NOISSUE - Upgrade Mongo, Cassandra and Influx docker images (#1354)
* NOISSUE - Upgrade Mongo, Cassandra and Influx docker images

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use cassandra version 3.11.9

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add MF_INFLUXDB_HTTP_AUTH_ENABLED envar

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use influxdb 1.8.4-alpine

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use influxdb 1.8.4

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-02-05 12:32:21 +01:00
Darko Draskovic 8b2ae46324 Add READMEs to pkg packages (#1352)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2021-02-04 20:04:41 +01:00
Drasko DRASKOVIC 241ccf8fb4 Correct README (#1349)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2021-02-02 17:18:12 +01:00
Mirko Teodorovic 39a649c1bb MF-1342 - Use environment variables in docker-compose to use tagged version of image (#1343)
* add MF_RELEASE_TAG

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update readme file for MF_RELEASE_TAG

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update readme file for MF_RELEASE_TAG

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix explanation, space

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bad search/replace

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bad search/replace

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* minor changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix readme

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* improve explanation for tag

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace snippet with link to code section

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* improve explanation for tag

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace snippet with link to code section

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove MF_RELEASE_TAG from service readme

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2021-02-02 16:26:02 +01:00
Dušan Borovčanin a3dbfecb2d MF-1311 - Add Notifications service (#1324)
* Add repo to Notify

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove notify

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add initial repo implementation

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Subscriptions repo implementation

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add working version of main method

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Notifier API

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add API layer

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Notifier interface

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix SMTP notifier main

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix email sending

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix main log

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Readme and fix env vars

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update README

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove commented code

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename notifiers to notify

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Move Consumer API to writers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix contact naming

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix spacing in init.go

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix API

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add mocks

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add initial service test

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use pagination

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update List method

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix listing

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename SubscriptionPage to Page

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Finish service tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add DB tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add API tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add more API tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove Things dependency

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add API docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Things references

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix AuthN typo

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix linter warnings

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add license comments

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix test order

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use service error for wrappers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix repo setup test

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use total const in tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use constant contacts

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix code formatting

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename notifier implementation

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename Notify to Notifiers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Resolve remarks

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix naming and docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix docker-compose

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename Subscriptions to Notifiers service

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix logging

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix formatting and typo

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Resolve remarks

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix topic request

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-02-01 14:08:10 +01:00
Jonathan Dreyer 73c175020e MF-1344 - Fix links to API documentations #1345
Signed-off-by: Jonathan Dreyer <jonathan.dreyer@he-arc.ch>
2021-01-31 20:25:19 +01:00
Manuel Imperiale 1bf485b71b NOISSUE - Upgrade influxdb and postgres docker images (#1341)
* NOISSUE - Upgrade influxdb and postgres docker images

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Upgrade grafana version

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Upgrade influxdb to 1.8.3-alpine

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-29 12:37:50 +01:00
Manuel Imperiale 8c87ee8328 NOISSUE - Revert cli to use user token from command args (#1339)
* NOISSUE - Revert cli to use user token from command args

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove CLI binary

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add offset, limit, name and rawOutput to config file

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert cli.ParseConfig()

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Check config values before to use them

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-28 11:29:32 +01:00
Manuel Imperiale 0631900d5c MF-1276 - Fix openapi IDs and Keys format (#1338)
* MF-1276 - Fix openapi IDs and Keys format

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix jwt

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix acces-by-key

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-27 11:42:55 +01:00
Manuel Imperiale 4619576e94 MF-1061 - Add PageMetadata to readers (#1333)
* MF-1061 - Add PageMetadata to readers

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix merge conflicts

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv Total to MessagesPage

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix readers mock and add filters tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add Total check and allow combinations of query parameters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use slices length as Total

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify readers mock

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add empty lines

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-26 12:23:15 +01:00
Dušan Borovčanin 31f5bf714a NOISSUE - Fix run script and compiler warnings (#1336)
* Fix NATS server naming in run script

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix warnings

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix typo

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-01-26 01:07:06 +01:00
Dušan Borovčanin bf5ede086a Fix Postgres writer transaction handling (#1335)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-01-23 02:03:33 +01:00
Dušan Borovčanin 5cfc9305e5 Make Transformer type configurable (#1331)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-01-20 13:10:40 +01:00
Manuel Imperiale 85f73a87b6 MF-1061 - Implement v, vb, vs, vd and from/to mongodb-reader filters (#1326)
* MF-1061 - Implement v, vb, vs, vd and from/to mongodb-reader filters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm duplicated test parameters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use pubID2 naming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix merge

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix merge

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm unecessary *bson.D pointer

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use from >= and to <= conditions and add tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert to half open interva

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-19 09:30:48 +01:00
Manuel Imperiale 0f856f5667 NOISSUE - Rename package aliases uuidProvider into uuid (#1323)
* NOISSUE - Rename pkg aliases uuidProvider into uuid and fix authn typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add missing error checks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use global uuidProvider

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use expTime globally

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix user uuid provider

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use idProvider name

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use idProvider instead of uuidProvider

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use idProvider instead of uuidProvider

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-17 23:12:45 +01:00
Ivan Milošević 0516fe2fd7 MF-1034 - Wrapping MQTT client (#1318)
* use subscribe interface

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* delete old implementation

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add subscribeToLoRaBroker method
change declaration

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove alias for package

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-01-15 23:22:18 +01:00
Manuel Imperiale a8c652f96c MF-1061 - Fix cassandra-reader count for json format (#1327)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-13 10:23:05 +01:00
Manuel Imperiale 8e5a9cfc9a MF-1061 - Implement v, vb, vs, vd and from/to cassandra-reader filters (#1325)
* MF-1061 - Implement v, vb, vs, vd and from/to cassandra-reader filters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert JSON implementation

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-12 20:39:31 +01:00
Dušan Borovčanin 6b7dc54c8b NOISSUE - Switch to Consumers interface (#1316)
* Replace Writer with Consumer

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Notifications package

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update Consumer Start

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Readers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Consumer naming

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add repo to Notify

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove notify

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename consumer field in middlewares

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix remarks and add Readme

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-01-11 23:55:34 +01:00
Manuel Imperiale 973ca177ea MF-1061 - Implement protocol, name, v, vb, vs, vd and from/to Postgres reader… (#1322)
* MF-1061 - Implement protocol, name, v, vb, vs, vd and from/to postgres-reader filters and tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-11 13:17:12 +01:00
Manuel Imperiale a185855c06 MF-1061 - Add name, protocol and publisher tests to influxdb-reader (#1320)
* MF-1061 - Add name, protocol and publisher tests to influxdb-reader

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use short package aliases

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-11 10:53:38 +01:00
Manuel Imperiale ac09815457 NOISSUE - Fix Auth typo (#1319)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-11 10:00:13 +01:00
Ivan Milošević 3eca6920e1 NOISSUE - Add health check for MQTT broker (#1305)
* Add health check

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* update readme
use constants

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* close body after each HEAD request

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* log and close health check response body

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* use healthcheck endpoint instead ping for docker healthcheck

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove for loop, only one check and exit if failed healthcheck

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* changed cmd for healthcheck
new version of verne image

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* use false instead exit 1

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* return for loop

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* review

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add continue after unsuccesful http.get

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Use backoff package

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* extracting healtcheck function outside main()

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* go mod vendor

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* go mod tidy

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* go mod vendor

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-01-05 19:45:09 +01:00
Dušan Borovčanin e326494166 MF-1264 - Add support for JSON readers (#1295)
* MF-1254 - Create universal JSON writer (#1260)

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add JSON support to Readers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Influx Reader tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix messages format query

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Postgres reader

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Cassandra Readers and writers

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Mongo reader

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Extract utility method to the JSON transformer

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Influx and Postgres count

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update JSON transformer

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Influxdb Reader total count

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Refactor init.go for Cassandra writer

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Create a Payload type

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add comments for defaults

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix variable declarations

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Replace interface{} with a new type

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Don't set channel just to overwrite it later

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix range search

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Rename Messages field

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-30 15:43:04 +01:00
Mirko Teodorovic 47217cb5b9 NOISSUE - Merge authz and authn into new service auth (#1313)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move authz into authn and merge into new service

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add group type

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding mocks, some renaming, refactor

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* update proto

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding mocks, some renaming, refactor

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix linter err,and comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* undo renaming, add interface for authn and authz

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renam some variables

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove extra slashes from comment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolving small remarks

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-29 23:02:35 +01:00
Manuel Imperiale b2ccbaec27 MF-1061 - Implement InfluxDB filters value, v, vb, vs, vd, from, to (#1312)
* MF-1061 - Implement InfluxDB filters value, v, vb, vs, vd, from, to

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use time filters as float64 instead of int64

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove unnecessary cast

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use a const for limit in tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert float64 cast when dividing

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove value filter in favour to v

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use v, vb, vs, vd

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use v, vb, vs, vd

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm unecessary cast

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-25 19:23:54 +01:00
Manuel Imperiale cb9985d160 NOISSUE - Correct readers openapi.yml (#1310)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-21 11:45:37 +01:00
Dušan Borovčanin 1810cec82b NOISSUE - Fix MQTT Forwarder client id (#1309)
* Fix MQTT Forwarder client ID

Don't set client ID for MQTT client in MQTT forwarder. It results in
error in case of multiple instances of the MQTT adapter because they all
share MQTT client ID, which makes broker disconnect all the other
adapters.

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Inline broker address option

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add MQTT client username

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2020-12-17 17:17:33 +01:00
Mirko Teodorovic d2af0602a0 NOISSUE - Fix dates not being init properly on save, change path construction, replace UUID with ULID for group ID (#1300)
* fix path, group saving with parent

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change path, enable name change

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert changes for port

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace UUID with ULID

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix migrations

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert user groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid provider

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* enable group name change

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* use null string for parent id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove migrations, disable group delete if group not empty

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert docker compose

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid provider

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove dash character replacment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid lib

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename UUIDProvider

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename package alias

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unused error

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-17 17:08:28 +01:00
Mirko Teodorovic 8aadc0ff40 NOISSUE - Remove authz from docker comp (#1307)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove authz from docker

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert init.go

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-16 18:15:27 +01:00
Darko Draskovic c7d2feb434 Shorten descriptions and add formats (#1306)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-12-16 16:00:56 +01:00
Mirko Teodorovic 18dd8967cc NOISSUE - remove owner id from user table and object (#1303)
* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-15 15:57:37 +01:00
Darko Draskovic 9864b27271 NOISSUE - Add missing fields to openapi specs and enclose http codes in single quotes (#1302)
* Add req body to token req

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Shorten summaries and add/remove keys

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add single quotes to html response codes

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-12-15 00:20:42 +01:00
Manuel Imperiale 3653e6b5d2 MF-1290 - Sort Things and Channels by name (#1293)
* MF-1290 - Sort Things and Channels by name

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add sort HTTP parameter and use PageMetadata to pass filters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix channels conn list

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix logs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Replace sort naming by order

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add order in responses

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Improve tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Improve order tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Improve getNameQuery func

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix commits

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add name-asc and name-desc

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add dir query parameter

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix openapi

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix openapi format

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test messages and openapi.yml

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix openapi uuid format

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Check query parameters in endpoint layer

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add asc endpoint tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename pageMeta into pageMetadata

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-08 21:30:47 +01:00
Mirko Teodorovic fbba7aaa1a MF-1248 - Add access policies for users (#1246)
* authz service init

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* authz service init

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add proto

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add proto

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* authorize method

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add casbib

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add casbib

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add casbib

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add policy grpc

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add policy grpc

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix db connection, add environment variables, docker-compose

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add model.conf policy

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add model.conf policy

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests, and token for auth

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix auth service

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* small changes, add model conf env var

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove users grpc for now

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix error

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* some cosmetics

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolvin minor comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change endpoint, add file to fix test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix docker env

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix alignment and path for model.conf

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-08 17:04:09 +01:00
Ivan Savčić 20f5290d7a Fixes, without spaces. (#1296)
Signed-off-by: Ivan Savcic <isavcic@gmail.com>
2020-12-04 18:59:16 +01:00
Darko Draskovic 23bc822433 Add different CNs for CA and certs (#1292)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-11-26 15:32:52 +01:00
Mirko Teodorovic 7195cad0f6 MF-397 - Introduce Thing Groups (#1259)
* add things group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add things group repository

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add things group repository

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add things group repository

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add parents and children methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add parents and children methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix logging message and temporary test fix

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bootstrap test fail

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups to pkg

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups to pkg

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups to pkg

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups to internal

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move groups to internal

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix import

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix linter errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments, add hierarchy info when retrieving groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* code refactor, separate http into multiple files

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* code refactor, separate http into multiple files

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix group update

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* use user id when saving

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* use user id when saving

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move code

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* move code

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove temporary test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix comments, fix responses in api for groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add checks for name length

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

fix validation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

small change to response

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add comment for Level

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add level to limit hierarchy retrieval

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

tidy vendor

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add level to limit hierarchy retrieval

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

mod tidy

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

revert

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

small naming and code organize refactor

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

revert Member type

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

fix typo

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

use ltree

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

use ltree

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add level for retrieving

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add level

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add ltre

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

upgrade postgres version in test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

add ltre

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

remove test for now

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

minor fixes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

fix id setting

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

fix tree endpoint

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

minor style changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* small changes, adding new lines

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* minor changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change function signature

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change primary key

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change function signature

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* simplufy code

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove groups test for now

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix tabulation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix whitespace

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove white space, fix grammar

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix sqls so that not additional retrieve is needed

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix sqls so that not additional retrieve is needed

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

fix primary key

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

remove retrive by name

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove name and parent update

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix cound sql

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add line

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* simplify sql, fix table name

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add date

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add date

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add timestamp to api

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix var name

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix var name

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-11-23 11:34:29 +01:00
Dušan Borovčanin 663bd4e18d Add Enhancement section to the issue template (#1284)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-11-17 22:52:56 +01:00
Darko Draskovic c03644524e Fix hardcoded env var values (#1283)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-11-16 20:44:25 +01:00
Dušan Borovčanin f1aa32d89c NOISSUE - Improve AuthN service docs (#1282)
* Update AuthN service README

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update Authn service docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-11-13 21:46:04 +01:00
João Matos 3273c30d8b MF-1268 - CLI improvements (#1274)
* Prefix error messages in CLI with a bold "error: ".

Signed-off-by: Joao Matos <joao@tritao.eu>

* Remove duplicated "Usage: " from groups command help.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Add a raw output mode for CLI and use it on logCreated.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Add CLI global flag for user auth token.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Add CLI config flag and parsing logic.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Refactor CLI users commands outside array structure.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Refactor CLI certificates commands using flags.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Refactor CLI things create command using flags.

Signed-off-by: Joao Matos <joao@tritao.eu>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2020-11-01 00:29:06 +01:00
João Matos 46c675cd5f NOISSSUE - Vault integration as an addon. (#1266)
Integrates Vault PKI service as a service addon.

Also adds some helper scripts to help setup the CA in Vault, as well as
some docs to explain how to use them.

Originally based from https://github.com/mteodor/vault.

Signed-off-by: Joao Matos <joao@tritao.eu>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2020-10-31 21:44:25 +01:00
Dušan Borovčanin 02db4066b1 Fix naming in Authn API tests (#1275)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-10-27 20:08:16 +01:00
Dušan Borovčanin f6b1ae735c MF-1244 - Return UserID alongside with user Email in Identify response (#1245)
* Add both an ID and an Email to API key requests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use return UserIdentity response

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Replace GetValue with GetEmail

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Refactor Mainflux Key

Add `Subject` field and reorganize Key manipulation.
**Remove backward compatibility**

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix service test

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix DB tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix API tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix JWT tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Uncomment and fix API tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix SQL statements alignment

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Issue method docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Fix Retrieve API and API docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-10-27 19:42:53 +01:00
Manuel Imperiale 926e9799d6 NOISSUE - Fix ViewGroup and UpdateGroup (#1269)
* NOISSUE - Fix ViewGroup and UpdateGroup

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add ID check and fix naming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix id in groupUpdateReq

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-27 11:26:32 +01:00
Manuel Imperiale 1c298d8f27 NOISSUE - Add ListUsers, ViewUser and ViewProfile methods (#1262)
* NOISSUE - Add admin method in users service to return users list

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix loggings and metrics

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add email and metadata filters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add comment

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Retrieve User infos by ID if Admin

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove admin checks and fix comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix missing query

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use generic funccs to create email and metadata queries

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add /users/profile endpoint

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify db helpers

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix View, List, Retrieve prefix methods naming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix tracer endpoints naming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix comment

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add tests and remove TODO comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-26 10:17:08 +01:00
Manuel Imperiale 420b598ac7 NOISSUE - Rm users http package (#1256)
* NOISSUE - Mv users http gokit layers to http package

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove users/api/http dir

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-16 18:00:24 +02:00
Manuel Imperiale 86fe3f7cdb NOISSUE - Remove content-type check from decodeListUserGroupsRequest (#1255)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-16 12:54:27 +02:00
3524 changed files with 305468 additions and 118533 deletions
+10
View File
@@ -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).
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+14 -13
View File
@@ -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
+37 -17
View File
@@ -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
+92
View File
@@ -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
+5
View File
@@ -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)
+735
View File
@@ -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: []
+257
View File
@@ -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: []
+220
View File
@@ -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: []
+56 -21
View File
@@ -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: []
+121 -16
View File
@@ -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: []
+23
View File
@@ -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
+375 -157
View File
@@ -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: []
+62 -47
View File
@@ -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: []
+158 -181
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+129
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
+334
View File
@@ -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
}
+163
View File
@@ -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
}
}
+463
View File
@@ -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")
}
}
+138
View File
@@ -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
}
+40
View File
@@ -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
}
+258
View File
@@ -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
+370
View File
@@ -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
}
+177
View File
@@ -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))
}
}
+191
View File
@@ -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
}
+174
View File
@@ -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
}
+354
View File
@@ -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 {
+48
View File
@@ -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)
}
}
+38
View File
@@ -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
}
}
+336
View File
@@ -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))
}
}
+64
View File
@@ -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
}
+43
View File
@@ -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
}
+107
View File
@@ -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)
}
}
}
+28
View File
@@ -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
}
+313
View File
@@ -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)
}
+222
View File
@@ -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
View File
@@ -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 {
+5
View File
@@ -0,0 +1,5 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package keto contains PolicyAgent implementation using Keto.
package keto
+172
View File
@@ -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
}
+79
View File
@@ -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))
}
+15 -5
View File
@@ -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() {
+8 -8
View File
@@ -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,
},
+319
View File
@@ -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
}
+13 -12
View File
@@ -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
+78
View File
@@ -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
}
+72
View File
@@ -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)
}
+742
View File
@@ -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
}
+777
View File
@@ -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))
}
+117
View File
@@ -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
}
+25 -22
View File
@@ -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 {
+160
View File
@@ -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
View File
@@ -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)
}
+1269
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 {
+129
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-49
View File
@@ -1,49 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package mainflux;
import "google/protobuf/empty.proto";
service ThingsService {
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
rpc Identify(Token) returns (ThingID) {}
}
service AuthNService {
rpc Issue(IssueReq) returns (Token) {}
rpc Identify(Token) returns (UserID) {}
}
message AccessByKeyReq {
string token = 1;
string chanID = 2;
}
message ThingID {
string value = 1;
}
message AccessByIDReq {
string thingID = 1;
string chanID = 2;
}
// If a token is not carrying any information itself, the type
// field can be used to determine how to validate the token.
// Also, different tokens can be encoded in different ways.
message Token {
string value = 1;
}
message UserID {
string value = 1;
}
message IssueReq {
string issuer = 1;
uint32 type = 2;
}
-106
View File
@@ -1,106 +0,0 @@
# Authentication service
Authentication service provides an API for managing authentication keys.
There are *three types of authentication keys*:
- user key - keys issued to the user upon login request
- API key - keys issued upon the user request
- recovery key - password recovery key
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user. API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. Recovery key is the password recovery key. It's short-lived token used for password recovery process.
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
The following actions are supported:
- create (all key types)
- verify (all key types)
- obtain (API keys only; secret is never obtained)
- revoke (API keys only)
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|---------------------------|--------------------------------------------------------------------------|---------------|
| MF_AUTHN_LOG_LEVEL | Service level (debug, info, warn, error) | error |
| MF_AUTHN_DB_HOST | Database host address | localhost |
| MF_AUTHN_DB_PORT | Database host port | 5432 |
| MF_AUTHN_DB_USER | Database user | mainflux |
| MF_AUTHN_DB_PASSWORD | Database password | mainflux |
| MF_AUTHN_DB | Name of the database used by the service | auth |
| MF_AUTHN_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| MF_AUTHN_DB_SSL_CERT | Path to the PEM encoded certificate file | |
| MF_AUTHN_DB_SSL_KEY | Path to the PEM encoded key file | |
| MF_AUTHN_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
| MF_AUTHN_HTTP_PORT | Authn service HTTP port | 8180 |
| MF_AUTHN_GRPC_PORT | Authn service gRPC port | 8181 |
| MF_AUTHN_SERVER_CERT | Path to server certificate in pem format | |
| MF_AUTHN_SERVER_KEY | Path to server key in pem format | |
| MF_AUTHN_SECRET | String used for signing tokens | auth |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
services:
authn:
image: mainflux/authn:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_AUTHN_LOG_LEVEL: [Service log level]
MF_AUTHN_DB_HOST: [Database host address]
MF_AUTHN_DB_PORT: [Database host port]
MF_AUTHN_DB_USER: [Database user]
MF_AUTHN_DB_PASS: [Database password]
MF_AUTHN_DB: [Name of the database used by the service]
MF_AUTHN_DB_SSL_MODE: [SSL mode to connect to the database with]
MF_AUTHN_DB_SSL_CERT: [Path to the PEM encoded certificate file]
MF_AUTHN_DB_SSL_KEY: [Path to the PEM encoded key file]
MF_AUTHN_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
MF_AUTHN_HTTP_PORT: [Service HTTP port]
MF_AUTHN_GRPC_PORT: [Service gRPC port]
MF_AUTHN_SECRET: [String used for signing tokens]
MF_AUTHN_SERVER_CERT: [String path to server certificate in pem format]
MF_AUTHN_SERVER_KEY: [String path to server key in pem format]
MF_JAEGER_URL: [Jaeger server URL]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the service
make authn
# copy binary to bin
make install
# set the environment variables and run the service
MF_AUTHN_LOG_LEVEL=[Service log level] MF_AUTHN_DB_HOST=[Database host address] MF_AUTHN_DB_PORT=[Database host port] MF_AUTHN_DB_USER=[Database user] MF_AUTHN_DB_PASS=[Database password] MF_AUTHN_DB=[Name of the database used by the service] MF_AUTHN_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTHN_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTHN_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTHN_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTHN_HTTP_PORT=[Service HTTP port] MF_AUTHN_GRPC_PORT=[Service gRPC port] MF_AUTHN_SECRET=[String used for signing tokens] MF_AUTHN_SERVER_CERT=[Path to server certificate] MF_AUTHN_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-authn
```
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
-93
View File
@@ -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
}
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package grpc contains implementation of AuthN service gRPC API.
package grpc
-50
View File
@@ -1,50 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"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
}
}
-167
View File
@@ -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()))
}
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import "github.com/mainflux/mainflux/authn"
type identityReq struct {
token string
kind uint32
}
func (req identityReq) validate() error {
if req.token == "" {
return authn.ErrMalformedEntity
}
if req.kind != authn.UserKey &&
req.kind != authn.APIKey &&
req.kind != authn.RecoveryKey {
return authn.ErrMalformedEntity
}
return nil
}
type issueReq struct {
issuer string
keyType uint32
}
func (req issueReq) validate() error {
if req.issuer == "" {
return authn.ErrUnauthorizedAccess
}
if req.keyType != authn.UserKey &&
req.keyType != authn.APIKey &&
req.keyType != authn.RecoveryKey {
return authn.ErrMalformedEntity
}
return nil
}
-91
View File
@@ -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")
}
}
-39
View File
@@ -1,39 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
import (
"time"
"github.com/mainflux/mainflux/authn"
)
type issueKeyReq struct {
issuer string
Type uint32 `json:"type,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
}
// It is not possible to issue Reset key using HTTP API.
func (req issueKeyReq) validate() error {
if req.Type == authn.UserKey {
return nil
}
if req.issuer == "" || (req.Type != authn.APIKey) {
return authn.ErrMalformedEntity
}
return nil
}
type keyReq struct {
issuer string
id string
}
func (req keyReq) validate() error {
if req.issuer == "" || req.id == "" {
return authn.ErrMalformedEntity
}
return nil
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
package api
import (
"context"
"fmt"
"time"
"github.com/mainflux/mainflux/authn"
log "github.com/mainflux/mainflux/logger"
)
var _ authn.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc authn.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc authn.Service, logger log.Logger) authn.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Issue(ctx context.Context, issuer string, newKey authn.Key) (key authn.Key, err error) {
defer func(begin time.Time) {
d := "infinite duration"
if !key.ExpiresAt.IsZero() {
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
}
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Issue(ctx, issuer, newKey)
}
func (lm *loggingMiddleware) Revoke(ctx context.Context, owner, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Revoke(ctx, owner, id)
}
func (lm *loggingMiddleware) Retrieve(ctx context.Context, owner, id string) (key authn.Key, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Retrieve(ctx, owner, id)
}
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Identify(ctx, key)
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/authn"
)
var _ authn.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc authn.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc authn.Service, counter metrics.Counter, latency metrics.Histogram) authn.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) Issue(ctx context.Context, issuer string, key authn.Key) (authn.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "issue").Add(1)
ms.latency.With("method", "issue").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Issue(ctx, issuer, key)
}
func (ms *metricsMiddleware) Revoke(ctx context.Context, issuer, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "revoke").Add(1)
ms.latency.With("method", "revoke").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Revoke(ctx, issuer, id)
}
func (ms *metricsMiddleware) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "retrieve").Add(1)
ms.latency.With("method", "retrieve").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Retrieve(ctx, issuer, id)
}
func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "identify").Add(1)
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Identify(ctx, key)
}
-147
View File
@@ -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"
-146
View File
@@ -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))
}
}
-193
View File
@@ -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
}
-280
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+76 -75
View File
@@ -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,
}
-4
View File
@@ -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
View File
@@ -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