Compare commits

..

77 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
3370 changed files with 279102 additions and 109422 deletions
+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
+89 -9
View File
@@ -11,17 +11,97 @@ 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
- 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
+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 auth twins mqtt provision certs smtp-notifier
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
+14 -15
View File
@@ -16,10 +16,6 @@ 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)
@@ -43,7 +39,7 @@ which maintains the project and offers professional services around it.
The following are needed to run Mainflux:
- [Docker](https://docs.docker.com/install/) (version 20.10)
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.28)
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.29)
Developing Mainflux will also require:
@@ -69,7 +65,7 @@ If you want to run services from specific release checkout code from github and
```bash
git checkout tags/<release_number> -b <release_number>
# e.g. `git checkout tags/0.12.0 -b 0.12.0`
# e.g. `git checkout tags/0.13.0 -b 0.13.0`
```
Check that `.env` file contains:
@@ -78,7 +74,7 @@ Check that `.env` file contains:
MF_RELEASE_TAG=<release_number>
```
>`docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://mainflux.readthedocs.io/en/latest/kubernetes/).
>`docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://docs.mainflux.io/kubernetes).
## Usage
@@ -91,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].
@@ -119,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!
@@ -129,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
@@ -149,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
+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)
+168 -53
View File
@@ -33,14 +33,13 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ApiKeyId"
responses:
'200':
$ref: "#/components/responses/KeyRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
@@ -51,12 +50,11 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ApiKeyId"
responses:
'204':
description: Key revoked.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
@@ -68,7 +66,6 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/requestBodies/GroupCreateReq"
responses:
'201':
@@ -85,12 +82,11 @@ paths:
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
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/Authorization"
parameters:
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Tree"
@@ -99,7 +95,7 @@ paths:
$ref: "#/components/responses/GroupsPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
@@ -113,14 +109,13 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
responses:
'200':
$ref: "#/components/responses/GroupRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
@@ -133,7 +128,6 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
requestBody:
$ref: "#/components/requestBodies/GroupUpdateReq"
@@ -142,7 +136,7 @@ paths:
description: Group updated.
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
@@ -152,12 +146,11 @@ paths:
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
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/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
@@ -167,7 +160,7 @@ paths:
description: Group removed.
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
@@ -183,7 +176,6 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
@@ -193,7 +185,7 @@ paths:
$ref: "#/components/responses/GroupsPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
@@ -209,7 +201,6 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
@@ -219,7 +210,7 @@ paths:
$ref: "#/components/responses/GroupsPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
@@ -233,7 +224,6 @@ paths:
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
requestBody:
$ref: "#/components/requestBodies/MembersReq"
@@ -242,7 +232,7 @@ paths:
$ref: "#/components/responses/GroupCreateRes"
'400':
description: Failed due to malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'409':
description: Failed due to using an existing email address.
@@ -255,25 +245,44 @@ paths:
description: |
Array of member ids that are in the group specified with groupID.
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/MemberType"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Limit"
responses:
'200':
$ref: "#/components/responses/MembersRes"
'403':
'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/Authorization"
- $ref: "#/components/parameters/MemberId"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Limit"
@@ -281,10 +290,66 @@ paths:
responses:
'200':
$ref: "#/components/responses/GroupRes"
'403':
'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:
@@ -329,7 +394,7 @@ components:
Free-form group name. Group name is unique on the given hierarchy level.
description:
type: string
description: Group description, free form text.
description: Group description, free form text.
parent_id:
type: string
format: ulid
@@ -346,7 +411,7 @@ components:
Free-form group name. Group name is unique on the given hierarchy level.
description:
type: string
description: Group description, free form text.
description: Group description, free form text.
metadata:
type: object
description: Arbitrary, object-encoded group's data.
@@ -407,6 +472,13 @@ components:
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:
@@ -446,15 +518,28 @@ components:
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:
Authorization:
name: Authorization
description: User's access token.
in: header
schema:
type: string
format: jwt
required: true
ApiKeyId:
name: id
description: API Key ID.
@@ -463,6 +548,14 @@ components:
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.
@@ -479,14 +572,6 @@ components:
type: string
format: uuid | ulid
required: true
MemberType:
name: type
description: Member type association.
in: path
schema:
type: string
enum: [users, things]
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
@@ -531,6 +616,7 @@ components:
schema:
type: boolean
default: false
requestBodies:
KeyRequest:
description: JSON-formatted document describing key request.
@@ -544,30 +630,25 @@ components:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
token:
type: string
format: jwt
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI"
description: JWT for the entity that's sending Key request.
duration:
type: number
format: integer
example: 23456
description: Number of seconds issued token is valid for.
GroupCreateReq:
GroupCreateReq:
description: JSON-formatted document describing group create request.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GroupReqSchema"
$ref: "#/components/schemas/GroupReqSchema"
GroupUpdateReq:
description: JSON-formatted document describing group create request.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GroupUpdateSchema"
$ref: "#/components/schemas/GroupUpdateSchema"
MembersReq:
description: JSON array of member IDs.
required: true
@@ -575,6 +656,21 @@ components:
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.
@@ -594,6 +690,8 @@ components:
type: string
description: Created group's relative URL.
example: /groups/{groupId}
ShareAccessRightRes:
description: User group shared with thing group.
GroupRes:
description: Data retrieved.
content:
@@ -618,3 +716,20 @@ components:
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,8 +13,6 @@ paths:
the provided access token.
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ConfigCreateReq"
responses:
@@ -22,7 +20,7 @@ paths:
$ref: "#/components/responses/ConfigCreateRes"
'400':
description: Failed due to malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'415':
description: Missing or invalid content type.
@@ -38,7 +36,6 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/State"
@@ -48,7 +45,7 @@ paths:
$ref: "#/components/responses/ConfigListRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
@@ -58,12 +55,11 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
responses:
'200':
$ref: "#/components/responses/ConfigRes"
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Config does not exist.
@@ -78,7 +74,6 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigUpdateReq"
@@ -87,7 +82,7 @@ paths:
description: Config updated.
'400':
description: Failed due to malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Config does not exist.
@@ -103,14 +98,13 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
responses:
'204':
description: Config removed.
'400':
description: Failed due to malformed config ID.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
@@ -123,7 +117,6 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
@@ -132,7 +125,7 @@ paths:
description: Config updated.
'400':
description: Failed due to malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Config does not exist.
@@ -149,7 +142,6 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
@@ -158,7 +150,7 @@ paths:
description: Config updated.
'400':
description: Failed due to malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Config does not exist.
@@ -173,15 +165,19 @@ 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':
$ref: "#/components/responses/BootstrapConfigRes"
'400':
description: Failed due to malformed JSON.
'401':
description: Missing or invalid external key provided.
'404':
description: |
Failed to retrieve corresponding config.
description: Failed to retrieve corresponding config.
'500':
$ref: "#/components/responses/ServiceError"
/things/bootstrap/secure/{externalId}:
@@ -191,8 +187,9 @@ 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':
@@ -211,7 +208,6 @@ paths:
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
requestBody:
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
@@ -220,10 +216,20 @@ paths:
description: Config removed.
'400':
description: Failed due to malformed config's ID.
'403':
'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:
@@ -331,31 +337,6 @@ components:
- content
parameters:
Authorization:
name: Authorization
description: User's access token.
in: header
schema:
type: string
format: jwt
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.
@@ -514,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: []
@@ -10,8 +10,6 @@ paths:
description: Creates a new subscription give a topic and contact.
tags:
- notifiers
security:
- Authorization: []
requestBody:
$ref: "#/components/requestBodies/Create"
responses:
@@ -30,8 +28,6 @@ paths:
description: List subscriptions given list parameters.
tags:
- notifiers
security:
- Authorization: []
parameters:
- $ref: "#/components/parameters/Topic"
- $ref: "#/components/parameters/Contact"
@@ -42,7 +38,7 @@ paths:
$ref: "#/components/responses/Page"
"400":
description: Failed due to malformed query parameters.
"403":
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
@@ -52,14 +48,12 @@ paths:
description: Retrieves a subscription with the provided id.
tags:
- notifiers
security:
- Authorization: []
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/View"
"403":
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
@@ -68,24 +62,27 @@ paths:
description: Removes a subscription with the provided id.
tags:
- notifiers
security:
- Authorization: []
parameters:
- $ref: "#/components/parameters/Id"
responses:
"204":
description: Subscription removed
"403":
"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:
securitySchemes:
Authorization:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Subscription:
type: object
@@ -204,3 +201,20 @@ components:
$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: []
+55 -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.
$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,14 +105,6 @@ components:
$ref: "#/components/schemas/SenMLRecord"
parameters:
Authorization:
name: Authorization
description: Access token.
in: header
schema:
type: string
format: jwt
required: true
ID:
name: id
description: Unique channel identifier.
@@ -116,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,8 +11,6 @@ paths:
description: Adds new device to proxy
tags:
- provision
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ProvisionReq"
responses:
@@ -20,10 +18,10 @@ paths:
description: Created
'400':
description: Failed due to malformed JSON.
'403':
description: Unauthorized.
"401":
description: Missing or invalid access token provided.
'500':
description: Unexpected server-side error ocurred.
$ref: "#/components/responses/ServiceError"
get:
summary: Gets current mapping.
description: Gets current mapping. This can be used in UI
@@ -31,28 +29,25 @@ paths:
configuration created with provision service.
tags:
- provision
parameters:
- $ref: "#/components/parameters/Authorization"
responses:
'200':
$ref: "#/components/responses/ProvisionRes"
'403':
description: Unauthorized.
"401":
description: Missing or invalid access token provided.
'500':
description: Unexpected server-side error ocurred.
$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
format: jwt
required: false
requestBodies:
ProvisionReq:
description: MAC address of device or other identifier
@@ -72,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: []
+36 -10
View File
@@ -16,7 +16,6 @@ paths:
tags:
- messages
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
@@ -33,10 +32,20 @@ paths:
$ref: "#/components/responses/MessagesPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
'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:
@@ -97,13 +106,6 @@ 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.
@@ -210,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
+109 -35
View File
@@ -13,8 +13,6 @@ paths:
the provided access token.
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ThingCreateReq"
responses:
@@ -42,7 +40,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Name"
@@ -73,8 +70,6 @@ paths:
increasing the subset size of the initial request.
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ThingsSearchReq"
responses:
@@ -98,8 +93,6 @@ paths:
the provided access token.
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ThingsCreateReq"
responses:
@@ -119,7 +112,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingId"
responses:
'200':
@@ -141,7 +133,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingId"
requestBody:
$ref: "#/components/requestBodies/ThingUpdateReq"
@@ -166,7 +157,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingId"
responses:
'204':
@@ -177,6 +167,31 @@ paths:
description: Missing or invalid access token provided.
'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:
summary: Updates thing key
@@ -185,7 +200,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingId"
requestBody:
$ref: "#/components/requestBodies/KeyUpdateReq"
@@ -212,8 +226,6 @@ paths:
be the channel's owner.
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ChannelCreateReq"
responses:
@@ -239,7 +251,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Name"
@@ -265,8 +276,6 @@ paths:
the provided access token.
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ChannelsCreateReq"
responses:
@@ -288,7 +297,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
responses:
'200':
@@ -312,7 +320,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
requestBody:
$ref: "#/components/requestBodies/ChannelCreateReq"
@@ -337,7 +344,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
responses:
'204':
@@ -356,8 +362,6 @@ 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:
@@ -375,6 +379,29 @@ paths:
description: Missing or invalid content type.
'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: List of channels connected to specified thing
@@ -384,7 +411,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingId"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Limit"
@@ -411,7 +437,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Limit"
@@ -438,7 +463,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
- $ref: "#/components/parameters/ThingId"
responses:
@@ -460,7 +484,6 @@ paths:
tags:
- channels
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ChanId"
- $ref: "#/components/parameters/ThingId"
responses:
@@ -550,7 +573,6 @@ paths:
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
@@ -570,6 +592,17 @@ paths:
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:
@@ -585,7 +618,10 @@ components:
id:
type: string
format: uuid
description: Thing unique identifier
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:
@@ -731,16 +767,21 @@ 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
format: jwt
required: true
ChanId:
name: chanId
description: Unique channel identifier.
@@ -912,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
@@ -938,6 +986,13 @@ components:
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:
@@ -993,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.
@@ -1013,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: []
+33 -21
View File
@@ -13,8 +13,6 @@ paths:
the provided access token.
tags:
- twins
parameters:
- $ref: '#/components/parameters/Authorization'
requestBody:
$ref: "#/components/requestBodies/TwinReq"
responses:
@@ -22,7 +20,7 @@ paths:
$ref: "#/components/responses/TwinCreateRes"
'400':
description: Failed due to malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'415':
description: Missing or invalid content type.
@@ -37,7 +35,6 @@ paths:
tags:
- twins
parameters:
- $ref: '#/components/parameters/Authorization'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Name'
@@ -47,7 +44,7 @@ paths:
$ref: '#/components/responses/TwinsPageRes'
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: '#/components/responses/ServiceError'
@@ -58,14 +55,13 @@ paths:
tags:
- twins
parameters:
- $ref: '#/components/parameters/Authorization'
- $ref: '#/components/parameters/TwinID'
responses:
'200':
$ref: '#/components/responses/TwinRes'
'400':
description: Failed due to malformed twin's ID.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Twin does not exist.
@@ -79,7 +75,6 @@ paths:
tags:
- twins
parameters:
- $ref: '#/components/parameters/Authorization'
- $ref: '#/components/parameters/TwinID'
requestBody:
$ref: '#/components/requestBodies/TwinReq'
@@ -88,7 +83,7 @@ paths:
description: Twin updated.
'400':
description: Failed due to malformed twin's ID or malformed JSON.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Twin does not exist.
@@ -102,14 +97,13 @@ paths:
tags:
- twins
parameters:
- $ref: '#/components/parameters/Authorization'
- $ref: '#/components/parameters/TwinID'
responses:
'204':
description: Twin removed.
'400':
description: Failed due to malformed twin's ID.
'403':
'401':
description: Missing or invalid access token provided
'404':
description: Twin does not exist.
@@ -126,7 +120,6 @@ paths:
- states
parameters:
- $ref: '#/components/parameters/TwinID'
- $ref: '#/components/parameters/Authorization'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
responses:
@@ -134,23 +127,25 @@ paths:
$ref: '#/components/responses/StatesPageRes'
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'404':
description: Twin does not exist.
'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
format: uuid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
@@ -365,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: []
+37 -27
View File
@@ -20,12 +20,14 @@ paths:
$ref: "#/components/responses/UserCreateRes"
'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"
$ref: "#/components/responses/ServiceError"
get:
summary: Retrieves users
description: |
@@ -36,7 +38,6 @@ paths:
tags:
- users
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Metadata"
@@ -46,7 +47,9 @@ paths:
'400':
description: Failed due to malformed query parameters.
'401':
description: Missing or invalid access token provided.
description: |
Missing or invalid access token provided.
This endpoint is available only for administrators.
'404':
description: A non-existent entity request.
'422':
@@ -60,8 +63,6 @@ paths:
authorization token and the new received info.
tags:
- users
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/UserUpdateReq"
responses:
@@ -71,7 +72,7 @@ paths:
description: Failed due to malformed JSON.
'404':
description: Failed due to non existing user.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
@@ -83,14 +84,12 @@ paths:
authorization token
tags:
- users
security:
- Authorization: []
responses:
'200':
$ref: "#/components/responses/UserRes"
'400':
description: Failed due to malformed query parameters.
'403':
'401':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
@@ -105,7 +104,6 @@ paths:
tags:
- users
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
@@ -144,7 +142,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
'401':
description: Failed due to using invalid credentials.
content:
application/json:
@@ -206,8 +204,6 @@ paths:
When authenticated user wants to change password.
tags:
- users
security:
- Authorization: []
requestBody:
$ref: '#/components/requestBodies/PasswordChange'
responses:
@@ -219,14 +215,18 @@ paths:
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:
securitySchemes:
Authorization:
type: http
scheme: bearer
bearerFormat: jwt
schemas:
Token:
type: object
@@ -301,15 +301,8 @@ components:
error:
type: string
description: Error message
parameters:
Authorization:
name: Authorization
description: User's access token.
in: header
schema:
type: string
format: jwt
required: true
Referer:
name: Referer
description: Host being sent by browser.
@@ -452,3 +445,20 @@ components:
$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: []
+1553 -44
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -18,6 +18,9 @@ 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) {}
}
@@ -73,6 +76,36 @@ 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;
+26 -21
View File
@@ -55,24 +55,29 @@ 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_JAEGER_URL | Jaeger server URL | localhost:6831|
| 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
@@ -95,7 +100,7 @@ make auth
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] $GOBIN/mainflux-auth
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.
@@ -103,6 +108,6 @@ If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but passw
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](openapi.yml).
the [API documentation](https://api.mainflux.io/?urls.primaryName=auth-openapi.yml).
[doc]: http://mainflux.readthedocs.io
[doc]: https://docs.mainflux.io
+116 -8
View File
@@ -23,12 +23,15 @@ const (
var _ mainflux.AuthServiceClient = (*grpcClient)(nil)
type grpcClient struct {
issue endpoint.Endpoint
identify endpoint.Endpoint
authorize endpoint.Endpoint
assign endpoint.Endpoint
members endpoint.Endpoint
timeout time.Duration
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.
@@ -58,6 +61,30 @@ func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Du
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,
@@ -129,9 +156,9 @@ func (client grpcClient) Authorize(ctx context.Context, req *mainflux.AuthorizeR
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.authorize(ctx, authReq{Act: req.Act, Obj: req.Obj, Sub: req.Sub})
res, err := client.authorize(ctx, authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()})
if err != nil {
return &mainflux.AuthorizeRes{Authorized: false}, err
return &mainflux.AuthorizeRes{}, err
}
ar := res.(authorizeRes)
@@ -152,6 +179,87 @@ func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}
}, 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()
+41 -5
View File
@@ -62,17 +62,53 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
return authorizeRes{}, err
}
_, err := svc.Identify(ctx, req.token)
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
}
}
authorized, err := svc.Authorize(ctx, req.token, req.Sub, req.Obj, req.Obj)
if err != nil {
return authorizeRes{}, 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
}
return authorizeRes{authorized: authorized}, 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
}
}
+215 -14
View File
@@ -34,6 +34,10 @@ const (
numOfThings = 5
numOfUsers = 5
authoritiesObj = "authorities"
memberRelation = "member"
loginDuration = 30 * time.Minute
)
var svc auth.Service
@@ -42,9 +46,14 @@ 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)
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
}
func startGRPCServer(svc auth.Service, port int) {
@@ -71,7 +80,7 @@ func TestIssue(t *testing.T) {
desc: "issue for user with valid token",
id: id,
email: email,
kind: auth.UserKey,
kind: auth.LoginKey,
err: nil,
code: codes.OK,
},
@@ -100,11 +109,12 @@ func TestIssue(t *testing.T) {
code: codes.InvalidArgument,
},
{
desc: "issue for user that exist",
id: "",
kind: auth.APIKey,
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
desc: "issue for user that exist",
id: "",
email: "",
kind: auth.APIKey,
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
code: codes.Unauthenticated,
},
}
@@ -117,7 +127,7 @@ func TestIssue(t *testing.T) {
}
func TestIdentify(t *testing.T) {
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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})
@@ -162,15 +172,15 @@ func TestIdentify(t *testing.T) {
desc: "identify user with invalid user token",
token: "invalid",
idt: mainflux.UserIdentity{},
err: status.Error(codes.Unauthenticated, "unauthorized access"),
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
code: codes.Unauthenticated,
},
{
desc: "identify user that doesn't exist",
desc: "identify user with empty token",
token: "",
idt: mainflux.UserIdentity{},
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
code: codes.Unauthenticated,
},
}
@@ -185,8 +195,194 @@ func TestIdentify(t *testing.T) {
}
}
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.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -196,10 +392,13 @@ func TestMembers(t *testing.T) {
var things []string
for i := 0; i < numOfThings; i++ {
id, err := uuid.New().ID()
thID, err := uuid.New().ID()
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
things = append(things, id)
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
@@ -212,6 +411,8 @@ func TestMembers(t *testing.T) {
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))
+46 -22
View File
@@ -5,6 +5,7 @@ package grpc
import (
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/internal/apiutil"
)
type identityReq struct {
@@ -14,12 +15,12 @@ type identityReq struct {
func (req identityReq) validate() error {
if req.token == "" {
return auth.ErrMalformedEntity
return apiutil.ErrBearerToken
}
if req.kind != auth.UserKey &&
if req.kind != auth.LoginKey &&
req.kind != auth.APIKey &&
req.kind != auth.RecoveryKey {
return auth.ErrMalformedEntity
return apiutil.ErrInvalidAuthKey
}
return nil
@@ -33,12 +34,12 @@ type issueReq struct {
func (req issueReq) validate() error {
if req.email == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrMissingEmail
}
if req.keyType != auth.UserKey &&
if req.keyType != auth.LoginKey &&
req.keyType != auth.APIKey &&
req.keyType != auth.RecoveryKey {
return auth.ErrMalformedEntity
return apiutil.ErrInvalidAuthKey
}
return nil
@@ -53,10 +54,10 @@ type assignReq struct {
func (req assignReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.groupID == "" || req.memberID == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingID
}
return nil
}
@@ -71,13 +72,13 @@ type membersReq struct {
func (req membersReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.groupID == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingID
}
if req.memberType == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingMemberType
}
return nil
}
@@ -87,28 +88,51 @@ func (req membersReq) validate() error {
// 2. object - an entity over which action will be executed
// 3. action - type of action that will be executed (read/write)
type authReq struct {
token string
Sub string
Obj string
Act string
Sub string
Obj string
Act string
}
func (req authReq) validate() error {
if req.token == "" {
return auth.ErrMalformedEntity
}
if req.Sub == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingPolicySub
}
if req.Obj == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingPolicyObj
}
if req.Act == "" {
return auth.ErrMalformedEntity
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
}
+13
View File
@@ -15,6 +15,19 @@ type issueRes struct {
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
+97 -15
View File
@@ -11,6 +11,7 @@ import (
"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"
@@ -20,11 +21,14 @@ import (
var _ mainflux.AuthServiceServer = (*grpcServer)(nil)
type grpcServer struct {
issue kitgrpc.Handler
identify kitgrpc.Handler
authorize kitgrpc.Handler
assign kitgrpc.Handler
members kitgrpc.Handler
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.
@@ -45,6 +49,21 @@ func NewServer(tracer opentracing.Tracer, svc auth.Service) mainflux.AuthService
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,
@@ -74,14 +93,38 @@ func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*main
return res.(*mainflux.UserIdentity), nil
}
func (s *grpcServer) Authorize(ctx context.Context, token *mainflux.AuthorizeReq) (*mainflux.AuthorizeRes, error) {
_, res, err := s.authorize.ServeGRPC(ctx, token)
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 {
@@ -120,12 +163,22 @@ func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}
func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.AuthorizeReq)
return authReq{Act: req.Act, Obj: req.Obj, Sub: req.Sub}, nil
return authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()}, nil
}
func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.AuthorizeRes)
return res, nil
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) {
@@ -133,6 +186,26 @@ func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, e
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{
@@ -164,12 +237,21 @@ func encodeError(err error) error {
switch {
case errors.Contains(err, nil):
return nil
case errors.Contains(err, auth.ErrMalformedEntity):
return status.Error(codes.InvalidArgument, "received invalid token request")
case errors.Contains(err, auth.ErrUnauthorizedAccess):
return status.Error(codes.Unauthenticated, err.Error())
case errors.Contains(err, auth.ErrKeyExpired):
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")
}
+19 -4
View File
@@ -141,6 +141,20 @@ func listMemberships(svc auth.Service) endpoint.Endpoint {
}
}
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)
@@ -236,7 +250,7 @@ func listMembersEndpoint(svc auth.Service) endpoint.Endpoint {
return memberPageRes{}, err
}
return buildUsersResponse(page), nil
return buildUsersResponse(page, req.groupType), nil
}
}
@@ -336,7 +350,7 @@ func buildGroupsResponse(gp auth.GroupPage) groupPageRes {
return res
}
func buildUsersResponse(mp auth.MemberPage) memberPageRes {
func buildUsersResponse(mp auth.MemberPage, groupType string) memberPageRes {
res := memberPageRes{
pageRes: pageRes{
Total: mp.Total,
@@ -344,11 +358,12 @@ func buildUsersResponse(mp auth.MemberPage) memberPageRes {
Limit: mp.Limit,
Name: mp.Name,
},
Members: []interface{}{},
Type: groupType,
Members: []string{},
}
for _, m := range mp.Members {
res.Members = append(res.Members, m)
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))
}
}
+49 -19
View File
@@ -2,7 +2,7 @@ package groups
import (
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/internal/apiutil"
)
type createGroupReq struct {
@@ -15,10 +15,10 @@ type createGroupReq struct {
func (req createGroupReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if len(req.Name) > maxNameSize || req.Name == "" {
return errors.Wrap(auth.ErrMalformedEntity, auth.ErrBadGroupName)
return apiutil.ErrNameSize
}
return nil
@@ -34,11 +34,11 @@ type updateGroupReq struct {
func (req updateGroupReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.id == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingID
}
return nil
@@ -56,11 +56,11 @@ type listGroupsReq struct {
func (req listGroupsReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.level > auth.MaxLevel || req.level < auth.MinLevel {
return auth.ErrMaxLevelExceeded
return apiutil.ErrMaxLevelExceeded
}
return nil
@@ -78,11 +78,11 @@ type listMembersReq struct {
func (req listMembersReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.id == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingID
}
return nil
@@ -98,11 +98,11 @@ type listMembershipsReq struct {
func (req listMembershipsReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.id == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingID
}
return nil
@@ -117,11 +117,37 @@ type assignReq struct {
func (req assignReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.Type == "" || req.groupID == "" || len(req.Members) == 0 {
return auth.ErrMalformedEntity
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
@@ -133,11 +159,15 @@ type unassignReq struct {
func (req unassignReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.groupID == "" || len(req.Members) == 0 {
return auth.ErrMalformedEntity
if req.groupID == "" {
return apiutil.ErrMissingID
}
if len(req.Members) == 0 {
return apiutil.ErrEmptyList
}
return nil
@@ -150,11 +180,11 @@ type groupReq struct {
func (req groupReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.id == "" {
return auth.ErrMalformedEntity
return apiutil.ErrMissingID
}
return nil
+17 -5
View File
@@ -18,7 +18,8 @@ var (
type memberPageRes struct {
pageRes
Members []interface{}
Type string `json:"type"`
Members []string `json:"members"`
}
func (res memberPageRes) Code() int {
@@ -33,6 +34,21 @@ 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"`
@@ -156,7 +172,3 @@ func (res unassignRes) Headers() map[string]string {
func (res unassignRes) Empty() bool {
return true
}
type errorRes struct {
Err string `json:"error"`
}
+77 -51
View File
@@ -3,7 +3,6 @@ package groups
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
@@ -12,16 +11,12 @@ import (
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/internal/httputil"
"github.com/mainflux/mainflux/internal/apiutil"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/opentracing/opentracing-go"
)
var (
errInvalidQueryParams = errors.New("invalid query params")
errUnsupportedContentType = errors.New("unsupported content type")
)
const (
contentType = "application/json"
maxNameSize = 254
@@ -37,9 +32,9 @@ const (
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
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.Post("/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "create_group")(createGroupEndpoint(svc)),
@@ -69,6 +64,13 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo
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,
@@ -119,27 +121,42 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo
))
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 := httputil.ReadUintQuery(r, levelKey, defLevel)
l, err := apiutil.ReadUintQuery(r, levelKey, defLevel)
if err != nil {
return nil, err
}
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
if err != nil {
return nil, err
}
t, err := httputil.ReadBoolQuery(r, treeKey, false)
t, err := apiutil.ReadBoolQuery(r, treeKey, false)
if err != nil {
return nil, err
}
req := listGroupsReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
level: l,
metadata: m,
tree: t,
@@ -149,33 +166,33 @@ func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, e
}
func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) {
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
if err != nil {
return nil, err
}
tree, err := httputil.ReadBoolQuery(r, treeKey, false)
tree, err := apiutil.ReadBoolQuery(r, treeKey, false)
if err != nil {
return nil, err
}
t, err := httputil.ReadStringQuery(r, groupType, "")
t, err := apiutil.ReadStringQuery(r, groupType, "")
if err != nil {
return nil, err
}
req := listMembersReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
id: bone.GetValue(r, "groupID"),
groupType: t,
offset: o,
@@ -187,23 +204,23 @@ func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{},
}
func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface{}, error) {
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
if err != nil {
return nil, err
}
req := listMembershipsReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
id: bone.GetValue(r, "memberID"),
offset: o,
limit: l,
@@ -215,36 +232,36 @@ func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, auth.ErrUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
var req createGroupReq
req := createGroupReq{token: apiutil.ExtractBearerToken(r)}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.ErrFailedDecode, err)
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
req.token = r.Header.Get("Authorization")
return req, nil
}
func decodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, auth.ErrUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
var req updateGroupReq
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(auth.ErrFailedDecode, err)
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
req.id = bone.GetValue(r, "groupID")
req.token = r.Header.Get("Authorization")
return req, nil
}
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := groupReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
id: bone.GetValue(r, "groupID"),
}
@@ -253,12 +270,12 @@ func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error)
func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := assignReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
groupID: bone.GetValue(r, "groupID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
return req, nil
@@ -267,13 +284,13 @@ func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error
func decodeUnassignRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := unassignReq{
assignReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
groupID: bone.GetValue(r, "groupID"),
},
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
return req, nil
@@ -299,29 +316,38 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
switch {
case errors.Contains(err, auth.ErrMalformedEntity):
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, auth.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, auth.ErrNotFound):
case errors.Contains(err, errors.ErrAuthentication):
w.WriteHeader(http.StatusUnauthorized)
case errors.Contains(err, errors.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, auth.ErrConflict):
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, 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)
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)
}
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)
}
}
+39 -30
View File
@@ -18,16 +18,19 @@ import (
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 = "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 {
@@ -50,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)
@@ -65,11 +68,17 @@ func newService() auth.Service {
groupRepo := mocks.NewGroupRepository()
idProvider := uuid.NewMock()
t := jwt.New(secret)
return auth.New(repo, groupRepo, idProvider, 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 auth.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc, mocktracer.New())
logger := logger.NewMock()
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
return httptest.NewServer(mux)
}
@@ -80,14 +89,14 @@ func toJSON(data interface{}) string {
func TestIssue(t *testing.T) {
svc := newService()
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
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: auth.UserKey}
lk := issueRequest{Type: auth.LoginKey}
ak := issueRequest{Type: auth.APIKey, Duration: time.Hour}
rk := issueRequest{Type: auth.RecoveryKey}
@@ -99,11 +108,11 @@ 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",
@@ -117,11 +126,11 @@ func TestIssue(t *testing.T) {
req: toJSON(rk),
ct: contentType,
token: loginSecret,
status: http.StatusBadRequest,
status: http.StatusCreated,
},
{
desc: "issue user key wrong content type",
req: toJSON(uk),
desc: "issue login key wrong content type",
req: toJSON(lk),
ct: "",
token: loginSecret,
status: http.StatusUnsupportedMediaType,
@@ -134,38 +143,38 @@ func TestIssue(t *testing.T) {
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,7 +196,7 @@ func TestIssue(t *testing.T) {
func TestRetrieve(t *testing.T) {
svc := newService()
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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}
@@ -217,10 +226,10 @@ func TestRetrieve(t *testing.T) {
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()
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
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))
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
k, _, err := svc.Issue(context.Background(), loginSecret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
ts := newServer(svc)
defer ts.Close()
@@ -269,10 +278,10 @@ func TestRevoke(t *testing.T) {
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 {
+15 -6
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/internal/apiutil"
)
type issueKeyReq struct {
@@ -17,12 +18,16 @@ type issueKeyReq struct {
// It is not possible to issue Reset key using HTTP API.
func (req issueKeyReq) validate() error {
if req.Type == auth.UserKey {
return nil
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.token == "" || (req.Type != auth.APIKey) {
return auth.ErrMalformedEntity
if req.Type != auth.LoginKey &&
req.Type != auth.RecoveryKey &&
req.Type != auth.APIKey {
return apiutil.ErrInvalidAPIKey
}
return nil
}
@@ -32,8 +37,12 @@ type keyReq struct {
}
func (req keyReq) validate() error {
if req.token == "" || req.id == "" {
return auth.ErrMalformedEntity
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
-4
View File
@@ -69,7 +69,3 @@ func (res revokeKeyRes) Headers() map[string]string {
func (res revokeKeyRes) Empty() bool {
return true
}
type errorRes struct {
Err string `json:"error"`
}
+23 -25
View File
@@ -6,7 +6,6 @@ package keys
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
@@ -15,17 +14,18 @@ import (
"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"
var errUnsupportedContentType = errors.New("unsupported content type")
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
// 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(encodeError),
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
}
mux.Post("/keys", kithttp.NewServer(
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
@@ -53,13 +53,12 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := issueKeyReq{
token: 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(auth.ErrMalformedEntity, err)
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
return req, nil
@@ -67,7 +66,7 @@ func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
req := keyReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
id: bone.GetValue(r, "id"),
}
return req, nil
@@ -93,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, auth.ErrMalformedEntity):
case errors.Contains(err, errors.ErrMalformedEntity),
err == apiutil.ErrMissingID,
err == apiutil.ErrInvalidAPIKey:
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, auth.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, auth.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, auth.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)
}
}
}
+8 -4
View File
@@ -10,15 +10,19 @@ import (
"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"
)
func MakeHandler(svc auth.Service, tracer opentracing.Tracer) http.Handler {
// 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)
mux = groups.MakeHandler(svc, mux, tracer)
mux.GetFunc("/version", mainflux.Version("auth"))
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
}
+77 -3
View File
@@ -1,7 +1,7 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
//go:build !test
package api
@@ -26,6 +26,19 @@ 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"
@@ -82,7 +95,7 @@ func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id auth.
return lm.svc.Identify(ctx, key)
}
func (lm *loggingMiddleware) Authorize(ctx context.Context, token, sub, obj, act string) (auth bool, err error) {
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 {
@@ -91,8 +104,56 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, token, sub, obj, act
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Authorize(ctx, pr)
}
return lm.svc.Authorize(ctx, token, sub, obj, act)
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) {
@@ -237,3 +298,16 @@ func (lm *loggingMiddleware) Unassign(ctx context.Context, token string, groupID
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)
}
+54 -2
View File
@@ -1,6 +1,8 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
//go:build !test
package api
import (
@@ -28,6 +30,15 @@ func MetricsMiddleware(svc auth.Service, counter metrics.Counter, latency metric
}
}
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)
@@ -64,13 +75,45 @@ func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.I
return ms.svc.Identify(ctx, token)
}
func (ms *metricsMiddleware) Authorize(ctx context.Context, token, sub, obj, act string) (auth bool, err error) {
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)
}
return ms.svc.Authorize(ctx, token, sub, obj, act)
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) {
@@ -168,3 +211,12 @@ func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID string
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)
}
+20 -35
View File
@@ -6,46 +6,20 @@ import (
"time"
)
const MaxLevel = uint64(5)
const MinLevel = uint64(1)
const (
// MaxLevel represents the maximum group hierarchy level.
MaxLevel = uint64(5)
// MinLevel represents the minimum group hierarchy level.
MinLevel = uint64(1)
)
var (
// ErrMaxLevelExceeded malformed entity.
ErrMaxLevelExceeded = errors.New("level must be less than or equal 5")
// ErrBadGroupName malformed entity.
ErrBadGroupName = errors.New("incorrect group name")
// ErrGroupConflict group conflict.
ErrGroupConflict = errors.New("group already exists")
// ErrCreateGroup indicates failure to create group.
ErrCreateGroup = errors.New("failed to create group")
// ErrFetchGroups indicates failure to fetch groups.
ErrFetchGroups = errors.New("failed to fetch groups")
// ErrUpdateGroup indicates failure to update group.
ErrUpdateGroup = errors.New("failed to update group")
// ErrDeleteGroup indicates failure to delete group.
ErrDeleteGroup = errors.New("failed to delete group")
// ErrGroupNotFound indicates failure to find group.
ErrGroupNotFound = errors.New("failed to find group")
// 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")
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
ErrUnsupportedContentType = errors.New("unsupported content type")
// ErrFailedDecode indicates failed to decode request body
ErrFailedDecode = errors.New("failed to decode request body")
// ErrMissingParent indicates that parent can't be found
ErrMissingParent = errors.New("failed to retrieve parent")
@@ -54,18 +28,18 @@ var (
// ErrMemberAlreadyAssigned indicates that members is already assigned.
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
// ErrSelectEntity indicates error while reading entity from database
ErrSelectEntity = errors.New("select entity from db error")
)
// 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
@@ -85,6 +59,7 @@ type Group struct {
UpdatedAt time.Time
}
// PageMetadata contains page metadata that helps navigation.
type PageMetadata struct {
Total uint64
Offset uint64
@@ -96,16 +71,22 @@ type PageMetadata struct {
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)
@@ -139,8 +120,12 @@ type GroupService interface {
// 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)
+2 -2
View File
@@ -21,7 +21,7 @@ func key() auth.Key {
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
return auth.Key{
ID: "id",
Type: auth.UserKey,
Type: auth.LoginKey,
Subject: "user@email.com",
IssuerID: "",
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
@@ -83,7 +83,7 @@ func TestParse(t *testing.T) {
desc: "parse ivalid key",
key: auth.Key{},
token: "invalid",
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "parse expired key",
+4 -4
View File
@@ -6,7 +6,7 @@ package jwt
import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang-jwt/jwt/v4"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
@@ -21,7 +21,7 @@ type claims struct {
func (c claims) Valid() error {
if c.Type == nil || *c.Type > auth.APIKey || c.Issuer != issuerName {
return auth.ErrMalformedEntity
return errors.ErrMalformedEntity
}
return c.StandardClaims.Valid()
@@ -62,7 +62,7 @@ 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, auth.ErrUnauthorizedAccess
return nil, errors.ErrAuthentication
}
return []byte(svc.secret), nil
})
@@ -75,7 +75,7 @@ func (svc tokenizer) Parse(token string) (auth.Key, error) {
}
return auth.Key{}, errors.Wrap(auth.ErrKeyExpired, err)
}
return auth.Key{}, errors.Wrap(auth.ErrUnauthorizedAccess, err)
return auth.Key{}, errors.Wrap(errors.ErrAuthentication, err)
}
return c.toKey(), nil
+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))
}
+2 -2
View File
@@ -22,8 +22,8 @@ var (
)
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.
+11 -10
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
var _ auth.GroupRepository = (*groupRepositoryMock)(nil)
@@ -55,14 +56,14 @@ func (grm *groupRepositoryMock) Save(ctx context.Context, group auth.Group) (aut
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[group.ID]; ok {
return auth.Group{}, auth.ErrGroupConflict
return auth.Group{}, errors.ErrConflict
}
path := group.ID
if group.ParentID != "" {
parent, ok := grm.groups[group.ParentID]
if !ok {
return auth.Group{}, auth.ErrCreateGroup
return auth.Group{}, errors.ErrCreateEntity
}
if _, ok := grm.children[group.ParentID]; !ok {
grm.children[group.ParentID] = make(map[string]auth.Group)
@@ -84,7 +85,7 @@ func (grm *groupRepositoryMock) Update(ctx context.Context, group auth.Group) (a
defer grm.mu.Unlock()
up, ok := grm.groups[group.ID]
if !ok {
return auth.Group{}, auth.ErrNotFound
return auth.Group{}, errors.ErrNotFound
}
up.Name = group.Name
up.Description = group.Description
@@ -99,7 +100,7 @@ func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[id]; !ok {
return auth.ErrGroupNotFound
return errors.ErrNotFound
}
if len(grm.members[id]) > 0 {
@@ -131,7 +132,7 @@ func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (au
val, ok := grm.groups[id]
if !ok {
return auth.Group{}, auth.ErrGroupNotFound
return auth.Group{}, errors.ErrNotFound
}
return val, nil
}
@@ -155,13 +156,13 @@ func (grm *groupRepositoryMock) Unassign(ctx context.Context, groupID string, me
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[groupID]; !ok {
return auth.ErrGroupNotFound
return errors.ErrNotFound
}
for _, memberID := range memberIDs {
for typ, m := range grm.members[groupID] {
_, ok := m[memberID]
if !ok {
return auth.ErrGroupNotFound
return errors.ErrNotFound
}
delete(grm.members[groupID][typ], memberID)
delete(grm.memberships[memberID], groupID)
@@ -175,7 +176,7 @@ func (grm *groupRepositoryMock) Assign(ctx context.Context, groupID, groupType s
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[groupID]; !ok {
return auth.ErrGroupNotFound
return errors.ErrNotFound
}
if _, ok := grm.members[groupID]; !ok {
@@ -229,7 +230,7 @@ func (grm *groupRepositoryMock) Members(ctx context.Context, groupID, groupType
var items []auth.Member
members, ok := grm.members[groupID][groupType]
if !ok {
return auth.MemberPage{}, auth.ErrGroupNotFound
return auth.MemberPage{}, errors.ErrNotFound
}
first := uint64(pm.Offset)
@@ -259,7 +260,7 @@ func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID
group, ok := grm.groups[groupID]
if !ok {
return auth.GroupPage{}, auth.ErrGroupNotFound
return auth.GroupPage{}, errors.ErrNotFound
}
groups := make([]auth.Group, 0)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"sync"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
var _ auth.KeyRepository = (*keyRepositoryMock)(nil)
@@ -29,7 +30,7 @@ func (krm *keyRepositoryMock) Save(ctx context.Context, key auth.Key) (string, e
defer krm.mu.Unlock()
if _, ok := krm.keys[key.ID]; ok {
return "", auth.ErrConflict
return "", errors.ErrConflict
}
krm.keys[key.ID] = key
@@ -43,7 +44,7 @@ func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuerID, id string)
return key, nil
}
return auth.Key{}, auth.ErrNotFound
return auth.Key{}, errors.ErrNotFound
}
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuerID, id string) error {
krm.mu.Lock()
+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)
}
+32 -33
View File
@@ -17,11 +17,10 @@ import (
"github.com/lib/pq"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/users"
)
var (
errStringToUUID = errors.New("error converting string")
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")
@@ -46,13 +45,13 @@ func NewGroupRepo(db Database) auth.GroupRepository {
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)
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)
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`
}
@@ -67,15 +66,15 @@ func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, e
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
case errFK:
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, err)
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, err)
case errDuplicate:
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
}
}
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, errors.New(pqErr.Message))
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, errors.New(pqErr.Message))
}
defer row.Close()
@@ -89,12 +88,12 @@ func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, e
}
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
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(auth.ErrUpdateGroup, err)
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, err)
}
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
@@ -103,19 +102,19 @@ func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group,
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
case errDuplicate:
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
}
}
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
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(auth.ErrUpdateGroup, err)
return g, errors.Wrap(errors.ErrUpdateEntity, err)
}
return toGroup(dbu)
@@ -128,7 +127,7 @@ func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
}
dbg, err := toDBGroup(group)
if err != nil {
return errors.Wrap(auth.ErrUpdateGroup, err)
return errors.Wrap(errors.ErrUpdateEntity, err)
}
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
@@ -137,25 +136,25 @@ func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(auth.ErrMalformedEntity, err)
return errors.Wrap(errors.ErrMalformedEntity, err)
case errFK:
switch pqErr.Constraint {
case groupIDFkeyy:
return errors.Wrap(auth.ErrGroupNotEmpty, err)
}
return errors.Wrap(auth.ErrGroupConflict, err)
return errors.Wrap(errors.ErrConflict, err)
}
}
return errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
return errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(auth.ErrDeleteGroup, err)
return errors.Wrap(errors.ErrRemoveEntity, err)
}
if cnt != 1 {
return errors.Wrap(auth.ErrDeleteGroup, err)
return errors.Wrap(errors.ErrRemoveEntity, err)
}
return nil
}
@@ -167,10 +166,10 @@ func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Gro
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(auth.ErrGroupNotFound, err)
return auth.Group{}, errors.Wrap(errors.ErrNotFound, err)
}
return auth.Group{}, errors.Wrap(auth.ErrSelectEntity, err)
return auth.Group{}, errors.Wrap(errors.ErrViewEntity, err)
}
return toGroup(dbu)
}
@@ -186,7 +185,7 @@ func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata)
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
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)
@@ -240,7 +239,7 @@ func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string
}
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
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`
@@ -370,7 +369,7 @@ func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm a
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
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)
@@ -426,7 +425,7 @@ func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string,
return errors.Wrap(auth.ErrAssignToGroup, err)
}
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
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 {
@@ -444,9 +443,9 @@ func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string,
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(auth.ErrMalformedEntity, err)
return errors.Wrap(errors.ErrMalformedEntity, err)
case errFK:
return errors.Wrap(auth.ErrConflict, errors.New(pqErr.Detail))
return errors.Wrap(errors.ErrConflict, errors.New(pqErr.Detail))
case errDuplicate:
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pqErr.Detail))
}
@@ -483,9 +482,9 @@ func (gr groupRepository) Unassign(ctx context.Context, groupID string, ids ...s
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(auth.ErrMalformedEntity, err)
return errors.Wrap(errors.ErrMalformedEntity, err)
case errDuplicate:
return errors.Wrap(auth.ErrConflict, err)
return errors.Wrap(errors.ErrConflict, err)
}
}
@@ -719,7 +718,7 @@ func (m *dbMetadata) Scan(value interface{}) error {
b, ok := value.([]byte)
if !ok {
return users.ErrScanMetadata
return errors.ErrScanMetadata
}
if err := json.Unmarshal(b, m); err != nil {
+9 -9
View File
@@ -72,7 +72,7 @@ func TestGroupSave(t *testing.T) {
OwnerID: usrID,
Name: groupName,
},
err: auth.ErrGroupConflict,
err: errors.ErrConflict,
},
{
desc: "create group with invalid name",
@@ -81,7 +81,7 @@ func TestGroupSave(t *testing.T) {
OwnerID: usrID,
Name: invalidName,
},
err: auth.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
},
{
desc: "create group with invalid description",
@@ -91,7 +91,7 @@ func TestGroupSave(t *testing.T) {
Name: groupName,
Description: invalidDesc,
},
err: auth.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
},
{
desc: "create group with parent",
@@ -121,7 +121,7 @@ func TestGroupSave(t *testing.T) {
OwnerID: usrID,
Name: "wrongParent",
},
err: auth.ErrCreateGroup,
err: errors.ErrCreateEntity,
},
}
@@ -182,7 +182,7 @@ func TestGroupRetrieveByID(t *testing.T) {
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, auth.ErrGroupNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", auth.ErrGroupNotFound, err))
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) {
@@ -245,7 +245,7 @@ func TestGroupUpdate(t *testing.T) {
ID: "wrong",
Name: groupName + "-2",
},
err: auth.ErrUpdateGroup,
err: errors.ErrUpdateEntity,
},
{
desc: "update group for invalid name",
@@ -253,7 +253,7 @@ func TestGroupUpdate(t *testing.T) {
ID: groupID,
Name: invalidName,
},
err: auth.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
},
{
desc: "update group for invalid description",
@@ -261,7 +261,7 @@ func TestGroupUpdate(t *testing.T) {
ID: groupID,
Description: invalidDesc,
},
err: auth.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
},
}
@@ -356,7 +356,7 @@ func TestGroupDelete(t *testing.T) {
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, auth.ErrGroupNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
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) {
+2 -2
View File
@@ -43,7 +43,7 @@ func (kr repo) Save(ctx context.Context, key auth.Key) (string, error) {
pqErr, ok := err.(*pq.Error)
if ok {
if pqErr.Code.Name() == errDuplicate {
return "", errors.Wrap(auth.ErrConflict, pqErr)
return "", errors.Wrap(errors.ErrConflict, pqErr)
}
}
@@ -59,7 +59,7 @@ func (kr repo) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, err
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 auth.Key{}, errors.Wrap(auth.ErrNotFound, err)
return auth.Key{}, errors.Wrap(errors.ErrNotFound, err)
}
return auth.Key{}, errors.Wrap(errRetrieve, err)
+5 -5
View File
@@ -59,7 +59,7 @@ func TestKeySave(t *testing.T) {
ID: id,
IssuerID: id,
},
err: auth.ErrConflict,
err: errors.ErrConflict,
},
}
@@ -98,16 +98,16 @@ func TestKeyRetrieve(t *testing.T) {
err: nil,
},
{
desc: "retrieve unauthorized",
desc: "retrieve key with empty issuer id",
id: key.ID,
owner: "",
err: auth.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "retrieve unknown key",
desc: "retrieve non-existent key",
id: "",
owner: key.IssuerID,
err: auth.ErrNotFound,
err: errors.ErrNotFound,
},
}
+1 -1
View File
@@ -30,7 +30,7 @@ func TestMain(m *testing.M) {
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.8-alpine", cfg)
container, err := pool.Run("postgres", "13.3-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
+168 -61
View File
@@ -5,6 +5,7 @@ package auth
import (
"context"
"fmt"
"time"
"github.com/mainflux/mainflux"
@@ -13,27 +14,14 @@ import (
)
const (
loginDuration = 10 * time.Hour
recoveryDuration = 5 * time.Minute
thingsGroupType = "things"
authoritiesObject = "authorities"
memberRelation = "member"
)
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")
// ErrGenerateGroupID indicates error in creating group.
ErrGenerateGroupID = errors.New("failed to generate group id")
// ErrConflict indicates that entity already exists.
ErrConflict = errors.New("entity already exists")
// ErrFailedToRetrieveMembers failed to retrieve group members.
ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members")
@@ -49,7 +37,7 @@ var (
// ErrFailedToRetrieveChildren failed to retrieve groups.
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
errIssueUser = errors.New("failed to issue new user key")
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")
@@ -68,7 +56,7 @@ type Authn interface {
// issued by the user identified by the provided key.
Revoke(ctx context.Context, token, id string) error
// Retrieve retrieves data for the Key identified by the provided
// 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)
@@ -78,14 +66,7 @@ type Authn interface {
Identify(ctx context.Context, token string) (Identity, error)
}
// Authz specifies an API for the authorization and will be implemented
// by evaluation of policies.
type Authz interface {
// Authorize checks access rights
Authorize(ctx context.Context, token, sub, obj, act string) (bool, error)
}
// Service specifies an API that must be fullfiled by the domain service
// 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.
@@ -93,28 +74,32 @@ type Service interface {
Authn
Authz
// Implements groups API, creating groups, assigning members
// 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
tokenizer Tokenizer
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) Service {
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(),
tokenizer: tokenizer,
keys: keys,
groups: groups,
idProvider: idp,
ulidProvider: ulid.New(),
agent: policyAgent,
loginDuration: duration,
}
}
@@ -128,7 +113,7 @@ func (svc service) Issue(ctx context.Context, token string, key Key) (Key, strin
case RecoveryKey:
return svc.tmpKey(recoveryDuration, key)
default:
return svc.tmpKey(loginDuration, key)
return svc.tmpKey(svc.loginDuration, key)
}
}
@@ -163,15 +148,91 @@ func (svc service) Identify(ctx context.Context, token string) (Identity, error)
}
switch key.Type {
case APIKey, RecoveryKey, UserKey:
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{}, ErrUnauthorizedAccess
return Identity{}, errors.ErrAuthentication
}
}
func (svc service) Authorize(ctx context.Context, token, sub, obj, act string) (bool, error) {
return true, nil
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) {
@@ -218,9 +279,9 @@ func (svc service) login(token string) (string, string, error) {
if err != nil {
return "", "", err
}
// Only user key token is valid for login.
if key.Type != UserKey || key.IssuerID == "" {
return "", "", ErrUnauthorizedAccess
// Only login key token is valid for login.
if key.Type != LoginKey || key.IssuerID == "" {
return "", "", errors.ErrAuthentication
}
return key.IssuerID, key.Subject, nil
@@ -229,12 +290,12 @@ func (svc service) login(token string) (string, string, error) {
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
user, err := svc.Identify(ctx, token)
if err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
return Group{}, err
}
ulid, err := svc.ulidProvider.ID()
if err != nil {
return Group{}, errors.Wrap(ErrGenerateGroupID, err)
return Group{}, err
}
timestamp := getTimestmap()
@@ -249,33 +310,37 @@ func (svc service) CreateGroup(ctx context.Context, token string, group Group) (
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{}, errors.Wrap(ErrUnauthorizedAccess, err)
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{}, errors.Wrap(ErrUnauthorizedAccess, err)
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{}, errors.Wrap(ErrUnauthorizedAccess, err)
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{}, errors.Wrap(ErrUnauthorizedAccess, err)
return MemberPage{}, err
}
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
if err != nil {
@@ -286,14 +351,14 @@ func (svc service) ListMembers(ctx context.Context, token string, groupID, group
func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
if _, err := svc.Identify(ctx, token); err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
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{}, errors.Wrap(ErrUnauthorizedAccess, err)
return Group{}, err
}
group.UpdatedAt = getTimestmap()
@@ -302,28 +367,70 @@ func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
if _, err := svc.Identify(ctx, token); err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
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 errors.Wrap(ErrUnauthorizedAccess, err)
return err
}
return svc.groups.Assign(ctx, groupID, groupType, memberIDs...)
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 errors.Wrap(ErrUnauthorizedAccess, err)
return err
}
return svc.groups.Unassign(ctx, groupID, memberIDs...)
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{}, errors.Wrap(ErrUnauthorizedAccess, err)
return GroupPage{}, err
}
return svc.groups.Memberships(ctx, memberID, pm)
}
+342 -56
View File
@@ -26,19 +26,28 @@ const (
id = "testID"
groupName = "mfx"
description = "Description"
memberRelation = "member"
authoritiesObj = "authorities"
loginDuration = 30 * time.Minute
)
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)
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
}
func TestIssue(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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))
cases := []struct {
@@ -48,18 +57,18 @@ func TestIssue(t *testing.T) {
err error
}{
{
desc: "issue user key",
desc: "issue login key",
key: auth.Key{
Type: auth.UserKey,
Type: auth.LoginKey,
IssuedAt: time.Now(),
},
token: secret,
err: nil,
},
{
desc: "issue user key with no time",
desc: "issue login key with no time",
key: auth.Key{
Type: auth.UserKey,
Type: auth.LoginKey,
},
token: secret,
err: auth.ErrInvalidKeyIssuedAt,
@@ -74,13 +83,13 @@ func TestIssue(t *testing.T) {
err: nil,
},
{
desc: "issue API key unauthorized",
desc: "issue API key with an invalid token",
key: auth.Key{
Type: auth.APIKey,
IssuedAt: time.Now(),
},
token: "invalid",
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "issue API key with no time",
@@ -117,7 +126,7 @@ func TestIssue(t *testing.T) {
func TestRevoke(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
Type: auth.APIKey,
@@ -135,22 +144,22 @@ func TestRevoke(t *testing.T) {
err error
}{
{
desc: "revoke user key",
desc: "revoke login key",
id: newKey.ID,
token: secret,
err: nil,
},
{
desc: "revoke non-existing user key",
desc: "revoke non-existing login key",
id: newKey.ID,
token: secret,
err: nil,
},
{
desc: "revoke unauthorized",
desc: "revoke with empty login key",
id: newKey.ID,
token: "",
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -162,7 +171,7 @@ func TestRevoke(t *testing.T) {
func TestRetrieve(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), Subject: email, IssuerID: id})
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), Subject: email, IssuerID: id})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := auth.Key{
ID: "id",
@@ -172,11 +181,11 @@ func TestRetrieve(t *testing.T) {
IssuedAt: time.Now(),
}
_, userToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
_, userToken, 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))
apiKey, apiToken, err := svc.Issue(context.Background(), secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
assert.Nil(t, err, fmt.Sprintf("Issuing login's key expected to succeed: %s", err))
_, resetToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
@@ -188,34 +197,34 @@ func TestRetrieve(t *testing.T) {
err error
}{
{
desc: "retrieve user key",
desc: "retrieve login key",
id: apiKey.ID,
token: userToken,
err: nil,
},
{
desc: "retrieve non-existing user key",
desc: "retrieve non-existing login key",
id: "invalid",
token: userToken,
err: auth.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "retrieve unauthorized",
desc: "retrieve with wrong login key",
id: apiKey.ID,
token: "wrong",
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "retrieve with API token",
id: apiKey.ID,
token: apiToken,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "retrieve with reset token",
id: apiKey.ID,
token: resetToken,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -228,21 +237,21 @@ func TestRetrieve(t *testing.T) {
func TestIdentify(t *testing.T) {
svc := newService()
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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))
_, 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 reset key expected to succeed: %s", err))
_, apiSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: auth.APIKey, IssuerID: id, Subject: email, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
exp1 := time.Now().Add(-2 * time.Second)
_, expSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
assert.Nil(t, err, fmt.Sprintf("Issuing expired user key expected to succeed: %s", err))
assert.Nil(t, err, fmt.Sprintf("Issuing expired login key expected to succeed: %s", err))
_, invalidSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: 22, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
cases := []struct {
desc string
@@ -278,13 +287,13 @@ func TestIdentify(t *testing.T) {
desc: "identify expired key",
key: invalidSecret,
idt: auth.Identity{},
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "identify invalid key",
key: "invalid",
idt: auth.Identity{},
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -297,7 +306,7 @@ func TestIdentify(t *testing.T) {
func TestCreateGroup(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -324,6 +333,9 @@ func TestCreateGroup(t *testing.T) {
parent, err := svc.CreateGroup(context.Background(), apiToken, parentGroup)
assert.Nil(t, err, fmt.Sprintf("Creating parent group expected to succeed: %s", err))
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: parent.ID, Relation: memberRelation, Subject: id})
assert.Nil(t, err, fmt.Sprintf("Checking parent group owner's policy expected to succeed: %s", err))
cases := []struct {
desc string
group auth.Group
@@ -353,19 +365,24 @@ func TestCreateGroup(t *testing.T) {
Name: groupName,
ParentID: "xxxxxxxxxx",
},
err: auth.ErrCreateGroup,
err: errors.ErrCreateEntity,
},
}
for _, tc := range cases {
_, err := svc.CreateGroup(context.Background(), apiToken, tc.group)
g, err := svc.CreateGroup(context.Background(), apiToken, tc.group)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
authzErr := svc.Authorize(context.Background(), auth.PolicyReq{Object: g.ID, Relation: memberRelation, Subject: g.OwnerID})
assert.Nil(t, authzErr, fmt.Sprintf("%s - Checking group owner's policy expected to succeed: %s", tc.desc, authzErr))
}
}
}
func TestUpdateGroup(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -422,7 +439,7 @@ func TestUpdateGroup(t *testing.T) {
func TestViewGroup(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -461,16 +478,16 @@ func TestViewGroup(t *testing.T) {
err: nil,
},
{
desc: "view group with unauthorized token",
desc: "view group with invalid token",
token: "wrongtoken",
groupID: group.ID,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "view group for wrong id",
token: apiToken,
groupID: "wrong",
err: auth.ErrGroupNotFound,
err: errors.ErrNotFound,
},
}
@@ -482,7 +499,7 @@ func TestViewGroup(t *testing.T) {
func TestListGroups(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -535,7 +552,7 @@ func TestListGroups(t *testing.T) {
token: "wrongToken",
level: 5,
size: 0,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -550,7 +567,7 @@ func TestListGroups(t *testing.T) {
func TestListChildren(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -601,7 +618,7 @@ func TestListChildren(t *testing.T) {
token: "wrongToken",
level: 5,
size: 0,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -615,7 +632,7 @@ func TestListChildren(t *testing.T) {
func TestListParents(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -666,7 +683,7 @@ func TestListParents(t *testing.T) {
token: "wrongToken",
level: 5,
size: 0,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -680,7 +697,7 @@ func TestListParents(t *testing.T) {
func TestListMembers(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -743,7 +760,7 @@ func TestListMembers(t *testing.T) {
offset: 0,
limit: n,
size: 0,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -758,7 +775,7 @@ func TestListMembers(t *testing.T) {
func TestListMemberships(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -788,6 +805,7 @@ func TestListMemberships(t *testing.T) {
g, err := svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
_ = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: memberID, Relation: "owner"})
err = svc.Assign(context.Background(), apiToken, g.ID, "things", memberID)
require.Nil(t, err, fmt.Sprintf("Assign member expected to succeed: %s\n", err))
}
@@ -822,7 +840,7 @@ func TestListMemberships(t *testing.T) {
offset: 0,
limit: n,
size: 0,
err: auth.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
@@ -836,7 +854,7 @@ func TestListMemberships(t *testing.T) {
func TestRemoveGroup(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -865,10 +883,10 @@ func TestRemoveGroup(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
err = svc.RemoveGroup(context.Background(), "wrongToken", group.ID)
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
assert.True(t, errors.Contains(err, errors.ErrAuthentication), fmt.Sprintf("Unauthorized access: expected %v got %v", errors.ErrAuthentication, err))
err = svc.RemoveGroup(context.Background(), apiToken, "wrongID")
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Remove group with wrong id: expected %v got %v", auth.ErrGroupNotFound, err))
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Remove group with wrong id: expected %v got %v", errors.ErrNotFound, err))
gp, err := svc.ListGroups(context.Background(), apiToken, auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("list groups unexpected error: %s", err))
@@ -885,7 +903,7 @@ func TestRemoveGroup(t *testing.T) {
func TestAssign(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -919,18 +937,27 @@ func TestAssign(t *testing.T) {
err = svc.Assign(context.Background(), apiToken, group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
// check access control policies things members.
subjectSet := fmt.Sprintf("%s:%s#%s", "members", group.ID, memberRelation)
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: mid, Relation: "read", Subject: subjectSet})
require.Nil(t, err, fmt.Sprintf("entites having an access to group %s must have %s policy on %s: %s", group.ID, "read", mid, err))
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: mid, Relation: "write", Subject: subjectSet})
require.Nil(t, err, fmt.Sprintf("entites having an access to group %s must have %s policy on %s: %s", group.ID, "write", mid, err))
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: mid, Relation: "delete", Subject: subjectSet})
require.Nil(t, err, fmt.Sprintf("entites having an access to group %s must have %s policy on %s: %s", group.ID, "delete", mid, err))
mp, err := svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Offset: 0, Limit: 10})
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 = svc.Assign(context.Background(), "wrongToken", group.ID, "things", mid)
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
assert.True(t, errors.Contains(err, errors.ErrAuthentication), fmt.Sprintf("Unauthorized access: expected %v got %v", errors.ErrAuthentication, err))
}
func TestUnassign(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
_, 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{
@@ -976,8 +1003,267 @@ func TestUnassign(t *testing.T) {
assert.True(t, mp.Total == 0, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 0, mp.Total))
err = svc.Unassign(context.Background(), "wrongToken", group.ID, mid)
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
assert.True(t, errors.Contains(err, errors.ErrAuthentication), fmt.Sprintf("Unauthorized access: expected %v got %v", errors.ErrAuthentication, err))
err = svc.Unassign(context.Background(), apiToken, group.ID, mid)
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
}
func TestAuthorize(t *testing.T) {
svc := newService()
pr := auth.PolicyReq{Object: authoritiesObj, Relation: memberRelation, Subject: id}
err := svc.Authorize(context.Background(), pr)
require.Nil(t, err, fmt.Sprintf("authorizing initial %v policy expected to succeed: %s", pr, err))
}
func TestAddPolicy(t *testing.T) {
svc := newService()
pr := auth.PolicyReq{Object: "obj", Relation: "rel", Subject: "sub"}
err := svc.AddPolicy(context.Background(), pr)
require.Nil(t, err, fmt.Sprintf("adding %v policy expected to succeed: %v", pr, err))
err = svc.Authorize(context.Background(), pr)
require.Nil(t, err, fmt.Sprintf("checking shared %v policy expected to be succeed: %#v", pr, err))
}
func TestDeletePolicy(t *testing.T) {
svc := newService()
pr := auth.PolicyReq{Object: authoritiesObj, Relation: memberRelation, Subject: id}
err := svc.DeletePolicy(context.Background(), pr)
require.Nil(t, err, fmt.Sprintf("deleting %v policy expected to succeed: %s", pr, err))
}
func TestAssignAccessRights(t *testing.T) {
svc := newService()
_, 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))
userGroupID := "user-group"
thingGroupID := "thing-group"
err = svc.AssignGroupAccessRights(context.Background(), apiToken, thingGroupID, userGroupID)
require.Nil(t, err, fmt.Sprintf("sharing the user group with thing group expected to succeed: %v", err))
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: thingGroupID, Relation: memberRelation, Subject: fmt.Sprintf("%s:%s#%s", "members", userGroupID, memberRelation)})
require.Nil(t, err, fmt.Sprintf("checking shared group access policy expected to be succeed: %#v", err))
}
func TestAddPolicies(t *testing.T) {
svc := newService()
_, 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))
thingID, err := idProvider.ID()
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
tmpID := "tmpid"
readPolicy := "read"
writePolicy := "write"
deletePolicy := "delete"
// Add read policy to users.
err = svc.AddPolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{readPolicy})
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
// Add write and delete policies to users.
err = svc.AddPolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{writePolicy, deletePolicy})
assert.Nil(t, err, fmt.Sprintf("adding multiple policies expected to succeed: %s", err))
cases := []struct {
desc string
policy auth.PolicyReq
err error
}{
{
desc: "check valid 'read' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: id},
err: nil,
},
{
desc: "check valid 'write' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: id},
err: nil,
},
{
desc: "check valid 'delete' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: id},
err: nil,
},
{
desc: "check valid 'read' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: tmpID},
err: nil,
},
{
desc: "check valid 'write' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: tmpID},
err: nil,
},
{
desc: "check valid 'delete' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: tmpID},
err: nil,
},
{
desc: "check invalid 'access' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: "access", Subject: id},
err: errors.ErrAuthorization,
},
{
desc: "check invalid 'access' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: "access", Subject: tmpID},
err: errors.ErrAuthorization,
},
}
for _, tc := range cases {
err := svc.Authorize(context.Background(), tc.policy)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.err, err))
}
}
func TestDeletePolicies(t *testing.T) {
svc := newService()
_, 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))
thingID, err := idProvider.ID()
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
tmpID := "tmpid"
readPolicy := "read"
writePolicy := "write"
deletePolicy := "delete"
memberPolicy := "member"
// Add read, write and delete policies to users.
err = svc.AddPolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{readPolicy, writePolicy, deletePolicy, memberPolicy})
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
// Delete multiple policies from single user.
err = svc.DeletePolicies(context.Background(), apiToken, thingID, []string{id}, []string{readPolicy, writePolicy})
assert.Nil(t, err, fmt.Sprintf("deleting policies from single user expected to succeed: %s", err))
// Delete multiple policies from multiple user.
err = svc.DeletePolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{deletePolicy, memberPolicy})
assert.Nil(t, err, fmt.Sprintf("deleting policies from multiple user expected to succeed: %s", err))
cases := []struct {
desc string
policy auth.PolicyReq
err error
}{
{
desc: "check non-existing 'read' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: id},
err: errors.ErrAuthorization,
},
{
desc: "check non-existing 'write' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: id},
err: errors.ErrAuthorization,
},
{
desc: "check non-existing 'delete' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: id},
err: errors.ErrAuthorization,
},
{
desc: "check non-existing 'member' policy of user with id",
policy: auth.PolicyReq{Object: thingID, Relation: memberPolicy, Subject: id},
err: errors.ErrAuthorization,
},
{
desc: "check non-existing 'delete' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: tmpID},
err: errors.ErrAuthorization,
},
{
desc: "check non-existing 'member' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: memberPolicy, Subject: tmpID},
err: errors.ErrAuthorization,
},
{
desc: "check valid 'read' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: tmpID},
err: nil,
},
{
desc: "check valid 'write' policy of user with tmpid",
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: tmpID},
err: nil,
},
}
for _, tc := range cases {
err := svc.Authorize(context.Background(), tc.policy)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.err, err))
}
}
func TestListPolicies(t *testing.T) {
svc := newService()
_, 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))
readPolicy := "read"
pageLen := 15
// Add arbitrary policies to the user.
for i := 0; i < pageLen; i++ {
err = svc.AddPolicies(context.Background(), apiToken, fmt.Sprintf("thing-%d", i), []string{id}, []string{readPolicy})
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
}
page, err := svc.ListPolicies(context.Background(), auth.PolicyReq{Subject: id, Relation: readPolicy})
assert.Nil(t, err, fmt.Sprintf("listing policies expected to succeed: %s", err))
assert.Equal(t, pageLen, len(page.Policies), fmt.Sprintf("unexpected listing page size, expected %d, got %d: %v", pageLen, len(page.Policies), err))
}
+2 -2
View File
@@ -115,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](openapi.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
}
+100 -95
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:]
@@ -159,7 +170,7 @@ func dec(in []byte) ([]byte, error) {
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
things := mocks.NewConfigsRepository()
config := mfsdk.Config{
BaseURL: url,
ThingsURL: url,
}
sdk := mfsdk.NewSDK(config)
@@ -184,12 +195,14 @@ func newThingsService(auth mainflux.AuthServiceClient) things.Service {
}
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",
@@ -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)
}
+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"
"time"
"github.com/go-kit/kit/metrics"
@@ -29,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"`
}
+82 -105
View File
@@ -6,37 +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 (
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()
@@ -100,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
@@ -111,9 +111,9 @@ func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
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
@@ -124,10 +124,12 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
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
@@ -139,12 +141,11 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, e
}
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
@@ -155,33 +156,38 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
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) {
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
}
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errors.ErrInvalidQueryParams
}
offset, limit, err := parsePagePrams(q)
if err != nil {
return nil, err
}
filter := parseFilter(q)
req := listReq{
key: r.Header.Get("Authorization"),
filter: filter,
offset: offset,
limit: limit,
token: apiutil.ExtractBearerToken(r),
filter: parseFilter(q),
offset: o,
limit: l,
}
return req, nil
@@ -190,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
@@ -201,10 +207,12 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
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
@@ -212,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
@@ -248,77 +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, errors.ErrUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case errors.Contains(errorVal, errors.ErrInvalidQueryParams):
w.WriteHeader(http.StatusBadRequest)
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)
case errors.Contains(errorVal, bootstrap.ErrMalformedEntity):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, bootstrap.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(errorVal, bootstrap.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(errorVal, bootstrap.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(errorVal, bootstrap.ErrThings):
w.WriteHeader(http.StatusServiceUnavailable)
case errors.Contains(errorVal, io.EOF):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, io.ErrUnexpectedEOF):
w.WriteHeader(http.StatusBadRequest)
default:
w.WriteHeader(http.StatusInternalServerError)
}
if errorVal.Msg() != "" {
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
default:
w.WriteHeader(http.StatusInternalServerError)
}
}
func parseUint(s string) (uint64, error) {
if s == "" {
return 0, nil
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, errors.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 {
+11 -10
View File
@@ -10,6 +10,7 @@ import (
"sync"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/pkg/errors"
)
const (
@@ -40,7 +41,7 @@ func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []str
for _, v := range crm.configs {
if v.MFThing == config.MFThing || v.ExternalID == config.ExternalID {
return "", bootstrap.ErrConflict
return "", errors.ErrConflict
}
}
@@ -69,10 +70,10 @@ func (crm *configRepositoryMock) RetrieveByID(token, id string) (bootstrap.Confi
c, ok := crm.configs[id]
if !ok {
return bootstrap.Config{}, bootstrap.ErrNotFound
return bootstrap.Config{}, errors.ErrNotFound
}
if c.Owner != token {
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
return bootstrap.Config{}, errors.ErrAuthentication
}
return c, nil
@@ -137,7 +138,7 @@ func (crm *configRepositoryMock) RetrieveByExternalID(externalID string) (bootst
}
}
return bootstrap.Config{}, bootstrap.ErrNotFound
return bootstrap.Config{}, errors.ErrNotFound
}
func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
@@ -146,7 +147,7 @@ func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
cfg, ok := crm.configs[config.MFThing]
if !ok || cfg.Owner != config.Owner {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
cfg.Name = config.Name
@@ -167,7 +168,7 @@ func (crm *configRepositoryMock) UpdateCert(owner, thingID, clientCert, clientKe
}
}
if _, ok := crm.configs[forUpdate.MFThing]; !ok {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
forUpdate.ClientCert = clientCert
forUpdate.ClientKey = clientKey
@@ -183,7 +184,7 @@ func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []
config, ok := crm.configs[id]
if !ok {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
for _, ch := range channels {
@@ -194,7 +195,7 @@ func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []
for _, conn := range connections {
ch, ok := crm.channels[conn]
if !ok {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
config.MFChannels = append(config.MFChannels, ch)
}
@@ -223,10 +224,10 @@ func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.S
config, ok := crm.configs[id]
if !ok {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
if config.Owner != token {
return bootstrap.ErrUnauthorizedAccess
return errors.ErrAuthentication
}
config.State = state
+36 -28
View File
@@ -9,6 +9,7 @@ import (
"sync"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/things"
)
@@ -40,7 +41,7 @@ func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return []things.Thing{}, things.ErrUnauthorizedAccess
return []things.Thing{}, errors.ErrAuthentication
}
for i := range ths {
svc.counter++
@@ -59,7 +60,7 @@ func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (thing
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.Thing{}, things.ErrUnauthorizedAccess
return things.Thing{}, errors.ErrAuthentication
}
if t, ok := svc.things[id]; ok && t.Owner == userID.Email {
@@ -67,7 +68,7 @@ func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (thing
}
return things.Thing{}, things.ErrNotFound
return things.Thing{}, errors.ErrNotFound
}
func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs []string) error {
@@ -76,11 +77,11 @@ func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.ErrUnauthorizedAccess
return errors.ErrAuthentication
}
for _, chID := range chIDs {
if svc.channels[chID].Owner != userID.Email {
return things.ErrUnauthorizedAccess
return errors.ErrAuthentication
}
for _, thID := range thIDs {
svc.connections[chID] = append(svc.connections[chID], thID)
@@ -90,35 +91,38 @@ func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs
return nil
}
func (svc *mainfluxThings) Disconnect(_ context.Context, owner, chanID, thingID string) error {
func (svc *mainfluxThings) Disconnect(_ context.Context, owner string, chIDs, thIDs []string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil || svc.channels[chanID].Owner != userID.Email {
return things.ErrUnauthorizedAccess
if err != nil {
return errors.ErrAuthentication
}
ids := svc.connections[chanID]
i := 0
for _, t := range ids {
if t == thingID {
break
for _, chID := range chIDs {
if svc.channels[chID].Owner != userID.Email {
return errors.ErrAuthentication
}
i++
}
if i == len(ids) {
return things.ErrNotFound
}
ids := svc.connections[chID]
var count int
var newConns []string
for _, thID := range thIDs {
for _, id := range ids {
if id == thID {
count++
continue
}
newConns = append(newConns, id)
}
var tmp []string
if i != len(ids)-2 {
tmp = ids[i+1:]
if len(newConns)-len(ids) != count {
return errors.ErrNotFound
}
svc.connections[chID] = newConns
}
}
ids = append(ids[:i], tmp...)
svc.connections[chanID] = ids
return nil
}
@@ -128,11 +132,11 @@ func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) erro
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.ErrUnauthorizedAccess
return errors.ErrAuthentication
}
if t, ok := svc.things[id]; !ok || t.Owner != userID.Email {
return things.ErrNotFound
return errors.ErrNotFound
}
delete(svc.things, id)
@@ -156,7 +160,7 @@ func (svc *mainfluxThings) ViewChannel(_ context.Context, owner, id string) (thi
if c, ok := svc.channels[id]; ok {
return c, nil
}
return things.Channel{}, things.ErrNotFound
return things.Channel{}, errors.ErrNotFound
}
func (svc *mainfluxThings) UpdateThing(context.Context, string, things.Thing) error {
@@ -185,7 +189,7 @@ func (svc *mainfluxThings) CreateChannels(_ context.Context, owner string, chs .
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return []things.Channel{}, things.ErrUnauthorizedAccess
return []things.Channel{}, errors.ErrAuthentication
}
for i := range chs {
svc.counter++
@@ -225,6 +229,10 @@ func (svc *mainfluxThings) Identify(context.Context, string) (string, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ShareThing(ctx context.Context, token, thingID string, actions, userIDs []string) error {
panic("not implemented")
}
func findIndex(list []string, val string) int {
for i, v := range list {
if v == val {
+17 -5
View File
@@ -8,7 +8,7 @@ import (
"github.com/golang/protobuf/ptypes/empty"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/pkg/errors"
"google.golang.org/grpc"
)
@@ -18,8 +18,8 @@ type serviceMock struct {
users map[string]string
}
// NewUsersService creates mock of users service.
func NewUsersService(users map[string]string) mainflux.AuthServiceClient {
// NewAuthClient creates mock of users service.
func NewAuthClient(users map[string]string) mainflux.AuthServiceClient {
return &serviceMock{users}
}
@@ -27,7 +27,7 @@ func (svc serviceMock) Identify(ctx context.Context, in *mainflux.Token, opts ..
if id, ok := svc.users[in.Value]; ok {
return &mainflux.UserIdentity{Email: id, Id: id}, nil
}
return nil, users.ErrUnauthorizedAccess
return nil, errors.ErrAuthentication
}
func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ...grpc.CallOption) (*mainflux.Token, error) {
@@ -37,13 +37,25 @@ func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ..
return &mainflux.Token{Value: id}, nil
}
}
return nil, users.ErrUnauthorizedAccess
return nil, errors.ErrAuthentication
}
func (svc serviceMock) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
panic("not implemented")
}
func (svc serviceMock) AddPolicy(ctx context.Context, in *mainflux.AddPolicyReq, opts ...grpc.CallOption) (*mainflux.AddPolicyRes, error) {
panic("not implemented")
}
func (svc serviceMock) DeletePolicy(ctx context.Context, in *mainflux.DeletePolicyReq, opts ...grpc.CallOption) (*mainflux.DeletePolicyRes, error) {
panic("not implemented")
}
func (svc serviceMock) ListPolicies(ctx context.Context, in *mainflux.ListPoliciesReq, opts ...grpc.CallOption) (*mainflux.ListPoliciesRes, error) {
panic("not implemented")
}
func (svc serviceMock) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
panic("not implemented")
}
+50 -61
View File
@@ -19,8 +19,8 @@ import (
const (
duplicateErr = "unique_violation"
uuidErr = "invalid input syntax for type uuid"
connConstraintErr = "connections_config_id_fkey"
fkViolation = "foreign_key_violation"
connConstraintErr = "connections_config_id_config_owner_fkey"
configFieldsNum = 8
chanFieldsNum = 3
connFieldsNum = 2
@@ -29,17 +29,11 @@ const (
)
var (
errSaveDB = errors.New("failed to save bootstrap configuration to database")
errMarshalChannel = errors.New("failed to marshal channel into json")
errUnmarshalChannel = errors.New("failed to unmarshal json to channel")
errSaveChannels = errors.New("failed to insert channels to database")
errSaveConnections = errors.New("failed to insert connections to database")
errRetrieve = errors.New("failed to retreive bootstrap configuration from database")
errUpdate = errors.New("failed to update bootstrap configuration in database")
errRemove = errors.New("failed to remove bootstrap configuration from database")
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
errSaveChannels = errors.New("failed to insert channels to database")
errSaveConnections = errors.New("failed to insert connections to database")
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
)
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
@@ -61,7 +55,7 @@ func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (stri
tx, err := cr.db.Beginx()
if err != nil {
return "", errors.Wrap(errSaveDB, err)
return "", errors.Wrap(errors.ErrCreateEntity, err)
}
dbcfg := toDBConfig(cfg)
@@ -69,28 +63,26 @@ func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (stri
if _, err := tx.NamedExec(q, dbcfg); err != nil {
e := err
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
e = bootstrap.ErrConflict
e = errors.ErrConflict
}
cr.rollback("Failed to insert a Config", tx, err)
return "", errors.Wrap(errSaveDB, e)
cr.rollback("Failed to insert a Config", tx)
return "", errors.Wrap(errors.ErrCreateEntity, e)
}
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
cr.rollback("Failed to insert Channels", tx, err)
cr.rollback("Failed to insert Channels", tx)
return "", errors.Wrap(errSaveChannels, err)
}
if err := insertConnections(cfg, chsConnIDs, tx); err != nil {
cr.rollback("Failed to insert connections", tx, err)
cr.rollback("Failed to insert connections", tx)
return "", errors.Wrap(errSaveConnections, err)
}
if err := tx.Commit(); err != nil {
cr.rollback("Failed to commit Config save", tx, err)
cr.rollback("Failed to commit Config save", tx)
return "", err
}
return cfg.MFThing, nil
@@ -109,10 +101,10 @@ func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, err
if err := cr.db.QueryRowx(q, id, owner).StructScan(&dbcfg); err != nil {
empty := bootstrap.Config{}
if err == sql.ErrNoRows {
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
return empty, errors.Wrap(errors.ErrNotFound, err)
}
return empty, errors.Wrap(errRetrieve, err)
return empty, errors.Wrap(errors.ErrViewEntity, err)
}
q = `SELECT mainflux_channel, name, metadata FROM channels ch
@@ -123,7 +115,7 @@ func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, err
rows, err := cr.db.NamedQuery(q, dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
}
defer rows.Close()
@@ -132,13 +124,13 @@ func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, err
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
}
dbch.Owner = nullString(dbcfg.Owner)
ch, err := toChannel(dbch)
if err != nil {
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
}
chans = append(chans, ch)
}
@@ -206,9 +198,9 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
if err := cr.db.QueryRowx(q, externalID).StructScan(&dbcfg); err != nil {
empty := bootstrap.Config{}
if err == sql.ErrNoRows {
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
return empty, errors.Wrap(errors.ErrNotFound, err)
}
return empty, errors.Wrap(errRetrieve, err)
return empty, errors.Wrap(errors.ErrViewEntity, err)
}
q = `SELECT mainflux_channel, name, metadata FROM channels ch
@@ -219,7 +211,7 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
rows, err := cr.db.NamedQuery(q, dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
}
defer rows.Close()
@@ -228,13 +220,13 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
}
channels = append(channels, ch)
@@ -254,16 +246,16 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
if err != nil {
return errors.Wrap(errUpdate, err)
return errors.Wrap(errors.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(errUpdate, err)
return errors.Wrap(errors.ErrUpdateEntity, err)
}
if cnt == 0 {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
return nil
@@ -274,16 +266,16 @@ func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caC
res, err := cr.db.Exec(q, clientCert, clientKey, caCert, thingID, owner)
if err != nil {
return err
return errors.Wrap(errors.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return err
return errors.Wrap(errors.ErrUpdateEntity, err)
}
if cnt == 0 {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
return nil
@@ -292,28 +284,27 @@ func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caC
func (cr configRepository) UpdateConnections(owner, id string, channels []bootstrap.Channel, connections []string) error {
tx, err := cr.db.Beginx()
if err != nil {
return err
return errors.Wrap(errors.ErrUpdateEntity, err)
}
if err := insertChannels(owner, channels, tx); err != nil {
cr.rollback("Failed to insert Channels during the update", tx, err)
return err
cr.rollback("Failed to insert Channels during the update", tx)
return errors.Wrap(errors.ErrUpdateEntity, err)
}
if err := updateConnections(owner, id, connections, tx); err != nil {
if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
}
cr.rollback("Failed to update connections during the update", tx, err)
return err
cr.rollback("Failed to update connections during the update", tx)
return errors.Wrap(errors.ErrUpdateEntity, err)
}
if err := tx.Commit(); err != nil {
cr.rollback("Failed to commit Config update", tx, err)
cr.rollback("Failed to commit Config update", tx)
return errors.Wrap(errors.ErrUpdateEntity, err)
}
return nil
@@ -322,7 +313,7 @@ func (cr configRepository) UpdateConnections(owner, id string, channels []bootst
func (cr configRepository) Remove(owner, id string) error {
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
if _, err := cr.db.Exec(q, id, owner); err != nil {
return errors.Wrap(errRemove, err)
return errors.Wrap(errors.ErrRemoveEntity, err)
}
if _, err := cr.db.Exec(cleanupQuery); err != nil {
@@ -337,16 +328,16 @@ func (cr configRepository) ChangeState(owner, id string, state bootstrap.State)
res, err := cr.db.Exec(q, state, id, owner)
if err != nil {
return err
return errors.Wrap(errors.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return err
return errors.Wrap(errors.ErrUpdateEntity, err)
}
if cnt == 0 {
return bootstrap.ErrNotFound
return errors.ErrNotFound
}
return nil
@@ -361,14 +352,14 @@ func (cr configRepository) ListExisting(owner string, ids []string) ([]bootstrap
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
rows, err := cr.db.Queryx(q, owner, pq.Array(ids))
if err != nil {
return []bootstrap.Channel{}, err
return []bootstrap.Channel{}, errors.Wrap(errors.ErrViewEntity, err)
}
for rows.Next() {
var dbch dbChannel
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
return []bootstrap.Channel{}, err
return []bootstrap.Channel{}, errors.Wrap(errors.ErrViewEntity, err)
}
ch, err := toChannel(dbch)
@@ -391,7 +382,7 @@ func (cr configRepository) RemoveThing(id string) error {
cr.log.Warn("Failed to clean dangling channels after removal")
}
if err != nil {
return errors.Wrap(errRemove, err)
return errors.Wrap(errors.ErrRemoveEntity, err)
}
return nil
}
@@ -399,7 +390,7 @@ func (cr configRepository) RemoveThing(id string) error {
func (cr configRepository) UpdateChannel(c bootstrap.Channel) error {
dbch, err := toDBChannel("", c)
if err != nil {
return err
return errors.Wrap(errors.ErrUpdateEntity, err)
}
q := `UPDATE channels SET name = :name, metadata = :metadata WHERE mainflux_channel = :mainflux_channel`
@@ -449,9 +440,7 @@ func (cr configRepository) retrieveAll(owner string, filter bootstrap.Filter) (s
return fmt.Sprintf(template, f), params
}
func (cr configRepository) rollback(content string, tx *sqlx.Tx, err error) {
cr.log.Error(fmt.Sprintf("%s %s", content, err))
func (cr configRepository) rollback(content string, tx *sqlx.Tx) {
if err := tx.Rollback(); err != nil {
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
}
@@ -476,7 +465,7 @@ func insertChannels(owner string, channels []bootstrap.Channel, tx *sqlx.Tx) err
if _, err := tx.NamedExec(q, chans); err != nil {
e := err
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
e = bootstrap.ErrConflict
e = errors.ErrConflict
}
return e
}
@@ -641,7 +630,7 @@ func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
metadata, err := json.Marshal(ch.Metadata)
if err != nil {
return dbChannel{}, errors.Wrap(errMarshalChannel, err)
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
dbch.Metadata = string(metadata)
@@ -658,7 +647,7 @@ func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
}
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
return bootstrap.Channel{}, errors.Wrap(errUnmarshalChannel, err)
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
return ch, nil
+15 -15
View File
@@ -26,8 +26,8 @@ var (
ExternalKey: "external-key",
Owner: "user@email.com",
MFChannels: []bootstrap.Channel{
bootstrap.Channel{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
bootstrap.Channel{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
},
Content: "content",
State: bootstrap.Inactive,
@@ -74,19 +74,19 @@ func TestSave(t *testing.T) {
desc: "save config with same Thing ID",
config: duplicateThing,
connections: nil,
err: bootstrap.ErrConflict,
err: errors.ErrConflict,
},
{
desc: "save config with same external ID",
config: duplicateExternal,
connections: nil,
err: bootstrap.ErrConflict,
err: errors.ErrConflict,
},
{
desc: "save config with same Channels",
config: duplicateChannels,
connections: channels,
err: bootstrap.ErrConflict,
err: errors.ErrConflict,
},
}
for _, tc := range cases {
@@ -130,19 +130,19 @@ func TestRetrieveByID(t *testing.T) {
desc: "retrieve config with wrong owner",
owner: "2",
id: id,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "retrieve a non-existing config",
owner: c.Owner,
id: nonexistentConfID.String(),
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "retrieve a config with invalid ID",
owner: c.Owner,
id: "invalid",
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
}
for _, tc := range cases {
@@ -256,7 +256,7 @@ func TestRetrieveByExternalID(t *testing.T) {
{
desc: "retrieve with invalid external ID",
externalID: strconv.Itoa(numConfigs + 1),
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "retrieve with external key",
@@ -301,7 +301,7 @@ func TestUpdate(t *testing.T) {
{
desc: "update with wrong owner",
config: wrongOwner,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "update a config",
@@ -353,7 +353,7 @@ func TestUpdateCert(t *testing.T) {
certKey: "certKey",
ca: "",
owner: "wrong",
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "update a config",
@@ -411,7 +411,7 @@ func TestUpdateConnections(t *testing.T) {
id: "unknown",
channels: nil,
connections: []string{channels[1]},
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "update connections",
@@ -467,7 +467,7 @@ func TestRemove(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
_, err = repo.RetrieveByID(c.Owner, id)
require.True(t, errors.Contains(err, bootstrap.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
require.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, errors.ErrNotFound, err))
}
}
@@ -498,13 +498,13 @@ func TestChangeState(t *testing.T) {
desc: "change state with wrong owner",
id: saved,
owner: "2",
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "change state with wrong id",
id: "wrong",
owner: c.Owner,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "change state to Active",
+6 -7
View File
@@ -5,7 +5,6 @@ package postgres_test
import (
"fmt"
"log"
"os"
"testing"
@@ -28,7 +27,7 @@ var (
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
}
cfg := []string{
@@ -36,9 +35,9 @@ func TestMain(m *testing.M) {
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.8-alpine", cfg)
container, err := pool.Run("postgres", "13.3-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
}
port := container.GetPort("5432/tcp")
@@ -51,7 +50,7 @@ func TestMain(m *testing.M) {
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
}
dbConfig := postgres.Config{
@@ -67,7 +66,7 @@ func TestMain(m *testing.M) {
}
if db, err = postgres.Connect(dbConfig); err != nil {
log.Fatalf("Could not setup test DB connection: %s", err)
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
}
code := m.Run()
@@ -75,7 +74,7 @@ func TestMain(m *testing.M) {
// Defers will not be run when using os.Exit
db.Close()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
}
os.Exit(code)
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -39,7 +40,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:]
+13 -24
View File
@@ -4,10 +4,11 @@
package consumer
import (
"context"
"encoding/json"
"fmt"
"github.com/go-redis/redis"
"github.com/go-redis/redis/v8"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/logger"
)
@@ -30,7 +31,7 @@ const (
// Subscriber represents event source for things and channels provisioning.
type Subscriber interface {
// Subscribes to given subject and receives events.
Subscribe(string) error
Subscribe(context.Context, string) error
}
type eventStore struct {
@@ -50,14 +51,14 @@ func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string,
}
}
func (es eventStore) Subscribe(subject string) error {
err := es.client.XGroupCreateMkStream(stream, group, "$").Err()
func (es eventStore) Subscribe(ctx context.Context, subject string) error {
err := es.client.XGroupCreateMkStream(ctx, stream, group, "$").Err()
if err != nil && err.Error() != exists {
return err
}
for {
streams, err := es.client.XReadGroup(&redis.XReadGroupArgs{
streams, err := es.client.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: group,
Consumer: es.consumer,
Streams: []string{stream, ">"},
@@ -74,22 +75,22 @@ func (es eventStore) Subscribe(subject string) error {
switch event["operation"] {
case thingRemove:
rte := decodeRemoveThing(event)
err = es.handleRemoveThing(rte)
err = es.svc.RemoveConfigHandler(ctx, rte.id)
case thingDisconnect:
dte := decodeDisconnectThing(event)
err = es.handleDisconnectThing(dte)
err = es.svc.DisconnectThingHandler(ctx, dte.channelID, dte.thingID)
case channelUpdate:
uce := decodeUpdateChannel(event)
err = es.handleUpdateChannel(uce)
err = es.handleUpdateChannel(ctx, uce)
case channelRemove:
rce := decodeRemoveChannel(event)
err = es.handleRemoveChannel(rce)
err = es.svc.RemoveChannelHandler(ctx, rce.id)
}
if err != nil {
es.logger.Warn(fmt.Sprintf("Failed to handle event sourcing: %s", err.Error()))
break
}
es.client.XAck(stream, group, msg.ID)
es.client.XAck(ctx, stream, group, msg.ID)
}
}
}
@@ -127,25 +128,13 @@ func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
}
}
func (es eventStore) handleRemoveThing(rte removeEvent) error {
return es.svc.RemoveConfigHandler(rte.id)
}
func (es eventStore) handleUpdateChannel(uce updateChannelEvent) error {
func (es eventStore) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
channel := bootstrap.Channel{
ID: uce.id,
Name: uce.name,
Metadata: uce.metadata,
}
return es.svc.UpdateChannelHandler(channel)
}
func (es eventStore) handleRemoveChannel(rce removeEvent) error {
return es.svc.RemoveChannelHandler(rce.id)
}
func (es eventStore) handleDisconnectThing(dte disconnectEvent) error {
return es.svc.DisconnectThingHandler(dte.channelID, dte.thingID)
return es.svc.UpdateChannelHandler(ctx, channel)
}
func read(event map[string]interface{}, key, def string) string {
+3 -2
View File
@@ -4,12 +4,13 @@
package producer_test
import (
"context"
"fmt"
"log"
"os"
"testing"
"github.com/go-redis/redis"
"github.com/go-redis/redis/v8"
dockertest "github.com/ory/dockertest/v3"
)
@@ -38,7 +39,7 @@ func TestMain(m *testing.M) {
DB: 0,
})
return redisClient.Ping().Err()
return redisClient.Ping(context.Background()).Err()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
+36 -35
View File
@@ -4,9 +4,10 @@
package producer
import (
"context"
"time"
"github.com/go-redis/redis"
"github.com/go-redis/redis/v8"
"github.com/mainflux/mainflux/bootstrap"
)
@@ -31,8 +32,8 @@ func NewEventStoreMiddleware(svc bootstrap.Service, client *redis.Client) bootst
}
}
func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config, error) {
saved, err := es.svc.Add(token, cfg)
func (es eventStore) Add(ctx context.Context, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
saved, err := es.svc.Add(ctx, token, cfg)
if err != nil {
return saved, err
}
@@ -52,17 +53,17 @@ func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config,
timestamp: time.Now(),
}
es.add(ev)
es.add(ctx, ev)
return saved, err
}
func (es eventStore) View(token, id string) (bootstrap.Config, error) {
return es.svc.View(token, id)
func (es eventStore) View(ctx context.Context, token, id string) (bootstrap.Config, error) {
return es.svc.View(ctx, token, id)
}
func (es eventStore) Update(token string, cfg bootstrap.Config) error {
if err := es.svc.Update(token, cfg); err != nil {
func (es eventStore) Update(ctx context.Context, token string, cfg bootstrap.Config) error {
if err := es.svc.Update(ctx, token, cfg); err != nil {
return err
}
@@ -73,17 +74,17 @@ func (es eventStore) Update(token string, cfg bootstrap.Config) error {
timestamp: time.Now(),
}
es.add(ev)
es.add(ctx, ev)
return nil
}
func (es eventStore) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) error {
return es.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
func (es eventStore) UpdateCert(ctx context.Context, token, thingKey, clientCert, clientKey, caCert string) error {
return es.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert)
}
func (es eventStore) UpdateConnections(token, id string, connections []string) error {
if err := es.svc.UpdateConnections(token, id, connections); err != nil {
func (es eventStore) UpdateConnections(ctx context.Context, token, id string, connections []string) error {
if err := es.svc.UpdateConnections(ctx, token, id, connections); err != nil {
return err
}
@@ -93,17 +94,17 @@ func (es eventStore) UpdateConnections(token, id string, connections []string) e
timestamp: time.Now(),
}
es.add(ev)
es.add(ctx, ev)
return nil
}
func (es eventStore) List(token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
return es.svc.List(token, filter, offset, limit)
func (es eventStore) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
return es.svc.List(ctx, token, filter, offset, limit)
}
func (es eventStore) Remove(token, id string) error {
if err := es.svc.Remove(token, id); err != nil {
func (es eventStore) Remove(ctx context.Context, token, id string) error {
if err := es.svc.Remove(ctx, token, id); err != nil {
return err
}
@@ -112,13 +113,13 @@ func (es eventStore) Remove(token, id string) error {
timestamp: time.Now(),
}
es.add(ev)
es.add(ctx, ev)
return nil
}
func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (bootstrap.Config, error) {
cfg, err := es.svc.Bootstrap(externalKey, externalID, secure)
func (es eventStore) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
ev := bootstrapEvent{
externalID: externalID,
@@ -130,13 +131,13 @@ func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (boo
ev.success = false
}
es.add(ev)
es.add(ctx, ev)
return cfg, err
}
func (es eventStore) ChangeState(token, id string, state bootstrap.State) error {
if err := es.svc.ChangeState(token, id, state); err != nil {
func (es eventStore) ChangeState(ctx context.Context, token, id string, state bootstrap.State) error {
if err := es.svc.ChangeState(ctx, token, id, state); err != nil {
return err
}
@@ -146,33 +147,33 @@ func (es eventStore) ChangeState(token, id string, state bootstrap.State) error
timestamp: time.Now(),
}
es.add(ev)
es.add(ctx, ev)
return nil
}
func (es eventStore) RemoveConfigHandler(id string) error {
return es.svc.RemoveConfigHandler(id)
func (es eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
return es.svc.RemoveConfigHandler(ctx, id)
}
func (es eventStore) RemoveChannelHandler(id string) error {
return es.svc.RemoveChannelHandler(id)
func (es eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
return es.svc.RemoveChannelHandler(ctx, id)
}
func (es eventStore) UpdateChannelHandler(channel bootstrap.Channel) error {
return es.UpdateChannelHandler(channel)
func (es eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
return es.svc.UpdateChannelHandler(ctx, channel)
}
func (es eventStore) DisconnectThingHandler(channelID, thingID string) error {
return es.svc.DisconnectThingHandler(channelID, thingID)
func (es eventStore) DisconnectThingHandler(ctx context.Context, channelID, thingID string) error {
return es.svc.DisconnectThingHandler(ctx, channelID, thingID)
}
func (es eventStore) add(ev event) error {
func (es eventStore) add(ctx context.Context, ev event) error {
record := &redis.XAddArgs{
Stream: streamID,
MaxLenApprox: streamLen,
Values: ev.encode(),
}
return es.client.XAdd(record).Err()
return es.client.XAdd(ctx, record).Err()
}
+54 -51
View File
@@ -4,6 +4,7 @@
package producer_test
import (
"context"
"fmt"
"net/http/httptest"
"strconv"
@@ -11,8 +12,9 @@ import (
"testing"
"time"
"github.com/go-redis/redis"
"github.com/go-redis/redis/v8"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/opentracing/opentracing-go/mocktracer"
@@ -64,7 +66,7 @@ var (
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
configs := mocks.NewConfigsRepository()
config := mfsdk.Config{
BaseURL: url,
ThingsURL: url,
}
sdk := mfsdk.NewSDK(config)
@@ -86,12 +88,13 @@ func newThingsService(auth mainflux.AuthServiceClient) things.Service {
}
func newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(mocktracer.New(), svc)
logger := logger.NewMock()
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
return httptest.NewServer(mux)
}
func TestAdd(t *testing.T) {
redisClient.FlushAll().Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
redisClient.FlushAll(context.Background()).Err()
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
@@ -132,17 +135,17 @@ func TestAdd(t *testing.T) {
desc: "create invalid config",
config: invalidConfig,
token: validToken,
err: bootstrap.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
_, err := svc.Add(tc.token, tc.config)
_, err := svc.Add(context.Background(), tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
@@ -160,26 +163,26 @@ func TestAdd(t *testing.T) {
}
func TestView(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
svcConfig, svcErr := svc.View(validToken, saved.MFThing)
svcConfig, svcErr := svc.View(context.Background(), validToken, saved.MFThing)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
esConfig, esErr := svc.View(validToken, saved.MFThing)
esConfig, esErr := svc.View(context.Background(), validToken, saved.MFThing)
assert.Equal(t, svcConfig, esConfig, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfig, esConfig))
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
}
func TestUpdate(t *testing.T) {
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
@@ -189,9 +192,9 @@ func TestUpdate(t *testing.T) {
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
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))
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
modified := saved
modified.Content = "new-config"
@@ -224,17 +227,17 @@ func TestUpdate(t *testing.T) {
desc: "update non-existing config",
config: nonExisting,
token: validToken,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.Update(tc.token, tc.config)
err := svc.Update(context.Background(), tc.token, tc.config)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
@@ -252,16 +255,16 @@ func TestUpdate(t *testing.T) {
}
func TestUpdateConnections(t *testing.T) {
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
cases := []struct {
desc string
@@ -289,17 +292,17 @@ func TestUpdateConnections(t *testing.T) {
id: saved.MFThing,
token: validToken,
connections: []string{"256"},
err: bootstrap.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
err := svc.UpdateConnections(context.Background(), tc.token, tc.id, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
@@ -316,37 +319,37 @@ func TestUpdateConnections(t *testing.T) {
}
}
func TestList(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
_, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
offset := uint64(0)
limit := uint64(10)
svcConfigs, svcErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
svcConfigs, svcErr := svc.List(context.Background(), validToken, bootstrap.Filter{}, offset, limit)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
esConfigs, esErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
esConfigs, esErr := svc.List(context.Background(), validToken, bootstrap.Filter{}, offset, limit)
assert.Equal(t, svcConfigs, esConfigs, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfigs, esConfigs))
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
}
func TestRemove(t *testing.T) {
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
saved, err := svc.Add(validToken, c)
saved, err := svc.Add(context.Background(), validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
cases := []struct {
desc string
@@ -370,17 +373,17 @@ func TestRemove(t *testing.T) {
desc: "remove config with invalid credentials",
id: saved.MFThing,
token: "",
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.Remove(tc.token, tc.id)
err := svc.Remove(context.Background(), tc.token, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
@@ -398,18 +401,18 @@ func TestRemove(t *testing.T) {
}
func TestBootstrap(t *testing.T) {
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
saved, err := svc.Add(validToken, c)
saved, err := svc.Add(context.Background(), validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
cases := []struct {
desc string
@@ -434,7 +437,7 @@ func TestBootstrap(t *testing.T) {
desc: "bootstrap with an error",
externalID: saved.ExternalID,
externalKey: "external",
err: bootstrap.ErrNotFound,
err: bootstrap.ErrExternalKey,
event: map[string]interface{}{
"external_id": saved.ExternalID,
"success": "0",
@@ -446,10 +449,10 @@ func TestBootstrap(t *testing.T) {
lastID := "0"
for _, tc := range cases {
_, err := svc.Bootstrap(tc.externalKey, tc.externalID, false)
_, err := svc.Bootstrap(context.Background(), tc.externalKey, tc.externalID, false)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
@@ -467,18 +470,18 @@ func TestBootstrap(t *testing.T) {
}
func TestChangeState(t *testing.T) {
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
svc = producer.NewEventStoreMiddleware(svc, redisClient)
c := config
saved, err := svc.Add(validToken, c)
saved, err := svc.Add(context.Background(), validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
redisClient.FlushAll().Err()
redisClient.FlushAll(context.Background()).Err()
cases := []struct {
desc string
@@ -506,17 +509,17 @@ func TestChangeState(t *testing.T) {
id: saved.MFThing,
token: "",
state: bootstrap.Inactive,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
event: nil,
},
}
lastID := "0"
for _, tc := range cases {
err := svc.ChangeState(tc.token, tc.id, tc.state)
err := svc.ChangeState(context.Background(), tc.token, tc.id, tc.state)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
streams := redisClient.XRead(&redis.XReadArgs{
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
Streams: []string{streamID, lastID},
Count: 1,
Block: time.Second,
+39 -52
View File
@@ -16,28 +16,15 @@ import (
)
var (
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
// ErrMalformedEntity indicates malformed entity specification.
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
// ErrConflict indicates that entity with the same ID or external ID already exists.
ErrConflict = errors.New("entity already exists")
// ErrThings indicates failure to communicate with Mainflux Things service.
// It can be due to networking error or invalid/unauthorized request.
// It can be due to networking error or invalid/unauthenticated request.
ErrThings = errors.New("failed to receive response from Things service")
// ErrExternalKeyNotFound indicates a non-existent bootstrap configuration for given external key
ErrExternalKeyNotFound = errors.New("failed to get bootstrap configuration for given external key")
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key
ErrExternalKey = errors.New("failed to get bootstrap configuration for given external key")
// ErrSecureBootstrap indicates error in getting bootstrap configuration for given encrypted external key
ErrSecureBootstrap = errors.New("failed to get bootstrap configuration for given encrypted external key")
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key
ErrExternalKeySecure = errors.New("failed to get bootstrap configuration for given encrypted external key")
// ErrBootstrap indicates error in getting bootstrap configuration.
ErrBootstrap = errors.New("failed to read bootstrap configuration")
@@ -63,48 +50,48 @@ var _ Service = (*bootstrapService)(nil)
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// Add adds new Thing Config to the user identified by the provided token.
Add(token string, cfg Config) (Config, error)
Add(ctx context.Context, token string, cfg Config) (Config, error)
// View returns Thing Config with given ID belonging to the user identified by the given token.
View(token, id string) (Config, error)
View(ctx context.Context, token, id string) (Config, error)
// Update updates editable fields of the provided Config.
Update(token string, cfg Config) error
Update(ctx context.Context, token string, cfg Config) error
// UpdateCert updates an existing Config certificate and token.
// A non-nil error is returned to indicate operation failure.
UpdateCert(token, thingID, clientCert, clientKey, caCert string) error
UpdateCert(ctx context.Context, token, thingID, clientCert, clientKey, caCert string) error
// UpdateConnections updates list of Channels related to given Config.
UpdateConnections(token, id string, connections []string) error
UpdateConnections(ctx context.Context, token, id string, connections []string) error
// List returns subset of Configs with given search params that belong to the
// user identified by the given token.
List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
List(ctx context.Context, token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
// Remove removes Config with specified token that belongs to the user identified by the given token.
Remove(token, id string) error
Remove(ctx context.Context, token, id string) error
// Bootstrap returns Config to the Thing with provided external ID using external key.
Bootstrap(externalKey, externalID string, secure bool) (Config, error)
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
// ChangeState changes state of the Thing with given ID and owner.
ChangeState(token, id string, state State) error
ChangeState(ctx context.Context, token, id string, state State) error
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
// handlers for events. That's why these methods surpass ownership check.
// UpdateChannelHandler updates Channel with data received from an event.
UpdateChannelHandler(channel Channel) error
UpdateChannelHandler(ctx context.Context, channel Channel) error
// RemoveConfigHandler removes Configuration with id received from an event.
RemoveConfigHandler(id string) error
RemoveConfigHandler(ctx context.Context, id string) error
// RemoveChannelHandler removes Channel with id received from an event.
RemoveChannelHandler(id string) error
RemoveChannelHandler(ctx context.Context, id string) error
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
DisconnectThingHandler(channelID, thingID string) error
DisconnectThingHandler(ctx context.Context, channelID, thingID string) error
}
// ConfigReader is used to parse Config into format which will be encoded
@@ -133,7 +120,7 @@ func New(auth mainflux.AuthServiceClient, configs ConfigRepository, sdk mfsdk.SD
}
}
func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
func (bs bootstrapService) Add(ctx context.Context, token string, cfg Config) (Config, error) {
owner, err := bs.identify(token)
if err != nil {
return Config{}, err
@@ -180,7 +167,7 @@ func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
return cfg, nil
}
func (bs bootstrapService) View(token, id string) (Config, error) {
func (bs bootstrapService) View(ctx context.Context, token, id string) (Config, error) {
owner, err := bs.identify(token)
if err != nil {
return Config{}, err
@@ -189,7 +176,7 @@ func (bs bootstrapService) View(token, id string) (Config, error) {
return bs.configs.RetrieveByID(owner, id)
}
func (bs bootstrapService) Update(token string, cfg Config) error {
func (bs bootstrapService) Update(ctx context.Context, token string, cfg Config) error {
owner, err := bs.identify(token)
if err != nil {
return err
@@ -200,7 +187,7 @@ func (bs bootstrapService) Update(token string, cfg Config) error {
return bs.configs.Update(cfg)
}
func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caCert string) error {
func (bs bootstrapService) UpdateCert(ctx context.Context, token, thingID, clientCert, clientKey, caCert string) error {
owner, err := bs.identify(token)
if err != nil {
return err
@@ -211,7 +198,7 @@ func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caC
return nil
}
func (bs bootstrapService) UpdateConnections(token, id string, connections []string) error {
func (bs bootstrapService) UpdateConnections(ctx context.Context, token, id string, connections []string) error {
owner, err := bs.identify(token)
if err != nil {
return err
@@ -259,7 +246,7 @@ func (bs bootstrapService) UpdateConnections(token, id string, connections []str
}
if err := bs.sdk.Connect(conIDs, token); err != nil {
if errors.Contains(err, mfsdk.ErrFailedConnect) {
return ErrMalformedEntity
return errors.ErrMalformedEntity
}
return ErrThings
}
@@ -268,7 +255,7 @@ func (bs bootstrapService) UpdateConnections(token, id string, connections []str
return bs.configs.UpdateConnections(owner, id, channels, connections)
}
func (bs bootstrapService) List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
func (bs bootstrapService) List(ctx context.Context, token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
owner, err := bs.identify(token)
if err != nil {
return ConfigsPage{}, err
@@ -277,7 +264,7 @@ func (bs bootstrapService) List(token string, filter Filter, offset, limit uint6
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
}
func (bs bootstrapService) Remove(token, id string) error {
func (bs bootstrapService) Remove(ctx context.Context, token, id string) error {
owner, err := bs.identify(token)
if err != nil {
return err
@@ -288,7 +275,7 @@ func (bs bootstrapService) Remove(token, id string) error {
return nil
}
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
cfg, err := bs.configs.RetrieveByExternalID(externalID)
if err != nil {
return cfg, errors.Wrap(ErrBootstrap, err)
@@ -297,19 +284,19 @@ func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool
if secure {
dec, err := bs.dec(externalKey)
if err != nil {
return Config{}, errors.Wrap(ErrSecureBootstrap, err)
return Config{}, errors.Wrap(ErrExternalKeySecure, err)
}
externalKey = dec
}
if cfg.ExternalKey != externalKey {
return Config{}, errors.Wrap(ErrExternalKeyNotFound, ErrNotFound)
return Config{}, ErrExternalKey
}
return cfg, nil
}
func (bs bootstrapService) ChangeState(token, id string, state State) error {
func (bs bootstrapService) ChangeState(ctx context.Context, token, id string, state State) error {
owner, err := bs.identify(token)
if err != nil {
return err
@@ -351,28 +338,28 @@ func (bs bootstrapService) ChangeState(token, id string, state State) error {
return nil
}
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
if err := bs.configs.UpdateChannel(channel); err != nil {
return errors.Wrap(errUpdateChannel, err)
}
return nil
}
func (bs bootstrapService) RemoveConfigHandler(id string) error {
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
if err := bs.configs.RemoveThing(id); err != nil {
return errors.Wrap(errRemoveConfig, err)
}
return nil
}
func (bs bootstrapService) RemoveChannelHandler(id string) error {
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
if err := bs.configs.RemoveChannel(id); err != nil {
return errors.Wrap(errRemoveChannel, err)
}
return nil
}
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
func (bs bootstrapService) DisconnectThingHandler(ctx context.Context, channelID, thingID string) error {
if err := bs.configs.DisconnectThing(channelID, thingID); err != nil {
return errors.Wrap(errDisconnectThing, err)
}
@@ -385,7 +372,7 @@ func (bs bootstrapService) identify(token string) (string, error) {
res, err := bs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return "", ErrUnauthorizedAccess
return "", errors.ErrAuthentication
}
return res.GetEmail(), nil
@@ -406,7 +393,7 @@ func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
thing, err := bs.sdk.Thing(thingID, token)
if err != nil {
if errors.Contains(err, mfsdk.ErrFailedFetch) {
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, ErrNotFound)
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, errors.ErrNotFound)
}
if id != "" {
@@ -437,7 +424,7 @@ func (bs bootstrapService) connectionChannels(channels, existing []string, token
for id := range add {
ch, err := bs.sdk.Channel(id, token)
if err != nil {
return nil, errors.Wrap(ErrMalformedEntity, err)
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
ret = append(ret, Channel{
@@ -490,14 +477,14 @@ func (bs bootstrapService) toIDList(channels []Channel) []string {
func (bs bootstrapService) dec(in string) (string, error) {
ciphertext, err := hex.DecodeString(in)
if err != nil {
return "", ErrNotFound
return "", err
}
block, err := aes.NewCipher(bs.encKey)
if err != nil {
return "", err
}
if len(ciphertext) < aes.BlockSize {
return "", ErrMalformedEntity
return "", err
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
+65 -62
View File
@@ -4,6 +4,7 @@
package bootstrap_test
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
@@ -20,6 +21,7 @@ import (
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/bootstrap/mocks"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/errors"
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
"github.com/mainflux/mainflux/things"
@@ -56,7 +58,7 @@ var (
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
things := mocks.NewConfigsRepository()
config := mfsdk.Config{
BaseURL: url,
ThingsURL: url,
}
sdk := mfsdk.NewSDK(config)
@@ -78,7 +80,8 @@ func newThingsService(auth mainflux.AuthServiceClient) things.Service {
}
func newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(mocktracer.New(), svc)
logger := logger.NewMock()
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
return httptest.NewServer(mux)
}
@@ -98,7 +101,7 @@ func enc(in []byte) ([]byte, error) {
}
func TestAdd(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
@@ -127,35 +130,35 @@ func TestAdd(t *testing.T) {
desc: "add a config with an invalid ID",
config: neID,
token: validToken,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "add a config with wrong credentials",
config: config,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "add a config with invalid list of channels",
config: wrongChannels,
token: validToken,
err: bootstrap.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
},
}
for _, tc := range cases {
_, err := svc.Add(tc.token, tc.config)
_, err := svc.Add(context.Background(), tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestView(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -174,24 +177,24 @@ func TestView(t *testing.T) {
desc: "view a non-existing config",
id: unknown,
token: validToken,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "view a config with wrong credentials",
id: config.MFThing,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
for _, tc := range cases {
_, err := svc.View(tc.token, tc.id)
_, err := svc.View(context.Background(), tc.token, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdate(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
@@ -200,7 +203,7 @@ func TestUpdate(t *testing.T) {
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
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))
modifiedCreated := saved
@@ -226,24 +229,24 @@ func TestUpdate(t *testing.T) {
desc: "update a non-existing config",
config: nonExisting,
token: validToken,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "update a config with wrong credentials",
config: saved,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
for _, tc := range cases {
err := svc.Update(tc.token, tc.config)
err := svc.Update(context.Background(), tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateCert(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
@@ -252,7 +255,7 @@ func TestUpdateCert(t *testing.T) {
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
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 {
@@ -281,7 +284,7 @@ func TestUpdateCert(t *testing.T) {
caCert: "newCert",
token: validToken,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "update config cert with wrong credentials",
@@ -290,18 +293,18 @@ func TestUpdateCert(t *testing.T) {
clientKey: "newKey",
caCert: "newCert",
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
for _, tc := range cases {
err := svc.UpdateCert(tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
err := svc.UpdateCert(context.Background(), tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnections(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
@@ -310,15 +313,15 @@ func TestUpdateConnections(t *testing.T) {
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
created, err := svc.Add(validToken, c)
created, err := svc.Add(context.Background(), validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
externalID, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ExternalID = externalID.String()
active, err := svc.Add(validToken, c)
active, err := svc.Add(context.Background(), validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = svc.ChangeState(validToken, active.MFThing, bootstrap.Active)
err = svc.ChangeState(context.Background(), validToken, active.MFThing, bootstrap.Active)
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
nonExisting := config
@@ -350,32 +353,32 @@ func TestUpdateConnections(t *testing.T) {
token: validToken,
id: "",
connections: []string{"3"},
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "update connections with invalid channels",
token: validToken,
id: created.MFThing,
connections: []string{"wrong"},
err: bootstrap.ErrMalformedEntity,
err: errors.ErrMalformedEntity,
},
{
desc: "update connections a config with wrong credentials",
token: invalidToken,
id: created.MFKey,
connections: []string{"2", "3"},
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
}
for _, tc := range cases {
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
err := svc.UpdateConnections(context.Background(), tc.token, tc.id, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestList(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
@@ -389,12 +392,12 @@ func TestList(t *testing.T) {
c.ExternalID = id.String()
c.ExternalKey = id.String()
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
s, err := svc.Add(validToken, c)
s, err := svc.Add(context.Background(), validToken, c)
saved = append(saved, s)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
// Set one Thing to the different state
err := svc.ChangeState(validToken, "42", bootstrap.Active)
err := svc.ChangeState(context.Background(), validToken, "42", bootstrap.Active)
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
saved[41].State = bootstrap.Active
@@ -436,13 +439,13 @@ func TestList(t *testing.T) {
err: nil,
},
{
desc: "list configs unauthorized",
desc: "list configs with invalid token",
config: bootstrap.ConfigsPage{},
filter: bootstrap.Filter{},
token: invalidToken,
offset: 0,
limit: 10,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "list last page",
@@ -475,7 +478,7 @@ func TestList(t *testing.T) {
}
for _, tc := range cases {
result, err := svc.List(tc.token, tc.filter, tc.offset, tc.limit)
result, err := svc.List(context.Background(), tc.token, tc.filter, tc.offset, tc.limit)
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
@@ -483,12 +486,12 @@ func TestList(t *testing.T) {
}
func TestRemove(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -501,7 +504,7 @@ func TestRemove(t *testing.T) {
desc: "view a config with wrong credentials",
id: saved.MFThing,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "remove an existing config",
@@ -524,18 +527,18 @@ func TestRemove(t *testing.T) {
}
for _, tc := range cases {
err := svc.Remove(tc.token, tc.id)
err := svc.Remove(context.Background(), tc.token, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestBootstrap(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
e, err := enc([]byte(saved.ExternalKey))
@@ -554,7 +557,7 @@ func TestBootstrap(t *testing.T) {
config: bootstrap.Config{},
externalID: "invalid",
externalKey: saved.ExternalKey,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
encrypted: false,
},
{
@@ -562,7 +565,7 @@ func TestBootstrap(t *testing.T) {
config: bootstrap.Config{},
externalID: saved.ExternalID,
externalKey: "invalid",
err: bootstrap.ErrNotFound,
err: bootstrap.ErrExternalKey,
encrypted: false,
},
{
@@ -584,19 +587,19 @@ func TestBootstrap(t *testing.T) {
}
for _, tc := range cases {
config, err := svc.Bootstrap(tc.externalKey, tc.externalID, tc.encrypted)
config, err := svc.Bootstrap(context.Background(), tc.externalKey, tc.externalID, tc.encrypted)
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChangeState(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -611,14 +614,14 @@ func TestChangeState(t *testing.T) {
state: bootstrap.Active,
id: saved.MFThing,
token: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "change state of non-existing config",
state: bootstrap.Active,
id: unknown,
token: validToken,
err: bootstrap.ErrNotFound,
err: errors.ErrNotFound,
},
{
desc: "change state to Active",
@@ -644,18 +647,18 @@ func TestChangeState(t *testing.T) {
}
for _, tc := range cases {
err := svc.ChangeState(tc.token, tc.id, tc.state)
err := svc.ChangeState(context.Background(), tc.token, tc.id, tc.state)
assert.True(t, errors.Contains(err, tc.err), err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateChannelHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
_, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
ch := bootstrap.Channel{
ID: channel.ID,
@@ -681,18 +684,18 @@ func TestUpdateChannelHandler(t *testing.T) {
}
for _, tc := range cases {
err := svc.UpdateChannelHandler(tc.channel)
err := svc.UpdateChannelHandler(context.Background(), tc.channel)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemoveChannelHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
_, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -713,18 +716,18 @@ func TestRemoveChannelHandler(t *testing.T) {
}
for _, tc := range cases {
err := svc.RemoveChannelHandler(tc.id)
err := svc.RemoveChannelHandler(context.Background(), tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemoveCoinfigHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -745,18 +748,18 @@ func TestRemoveCoinfigHandler(t *testing.T) {
}
for _, tc := range cases {
err := svc.RemoveConfigHandler(tc.id)
err := svc.RemoveConfigHandler(context.Background(), tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestDisconnectThingsHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
users := mocks.NewAuthClient(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
saved, err := svc.Add(context.Background(), validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -780,7 +783,7 @@ func TestDisconnectThingsHandler(t *testing.T) {
}
for _, tc := range cases {
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
err := svc.DisconnectThingHandler(context.Background(), tc.channelID, tc.thingID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
+5 -5
View File
@@ -2,8 +2,8 @@
Issues certificates for things. `Certs` service can create certificates to be used when `Mainflux` is deployed to support mTLS.
Certificate service can create certificates in two modes:
1. Development mode - to be used when no PKI is deployed, this works similar to the [make thing_cert](../docker/ssl/Makefile)
2. PKI mode - certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
2. PKI mode - certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
## Development mode
If `MF_CERTS_VAULT_HOST` is empty than Development mode is on.
@@ -12,7 +12,7 @@ To issue a certificate:
TOK=`curl -s --insecure -S -X POST http://localhost/tokens -H 'Content-Type: application/json' -d '{"email":"edge@email.com","password":"12345678"}' | jq -r '.token'`
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "key_bits":2048, "key_type":"rsa"}'
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "key_bits":2048, "key_type":"rsa"}'
```
```json
@@ -31,7 +31,7 @@ curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Co
## PKI mode
When `MF_CERTS_VAULT_HOST` is set it is presumed that `Vault` is installed and `certs` service will issue certificates using `Vault` API.
First you'll need to set up `Vault`.
First you'll need to set up `Vault`.
To setup `Vault` follow steps in [Build Your Own Certificate Authority (CA)](https://learn.hashicorp.com/tutorials/vault/pki-engine).
To setup certs service with `Vault` following environment variables must be set:
@@ -49,5 +49,5 @@ Issuing certificate is same as in **Development** mode.
In this mode certificates can also be revoked:
```bash
curl -s -S -X DELETE http://localhost:8204/certs/revoke -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":"c30b8842-507c-4bcd-973c-74008cef3be5"}'
curl -s -S -X DELETE http://localhost:8204/certs/revoke -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"thing_id":"c30b8842-507c-4bcd-973c-74008cef3be5"}'
```
+58 -5
View File
@@ -16,16 +16,17 @@ func issueCert(svc certs.Service) endpoint.Endpoint {
if err := req.validate(); err != nil {
return nil, err
}
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.Valid, req.KeyBits, req.KeyType)
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.TTL, req.KeyBits, req.KeyType)
if err != nil {
return certsRes{}, err
}
return certsRes{
CertSerial: res.Serial,
ThingID: res.ThingID,
CertKey: res.ClientKey,
Cert: res.ClientCert,
CACert: res.IssuingCA,
Expiration: res.Expire,
created: true,
}, nil
}
}
@@ -54,9 +55,8 @@ func listCerts(svc certs.Service) endpoint.Endpoint {
view := certsRes{
CertSerial: cert.Serial,
ThingID: cert.ThingID,
CertKey: cert.ClientKey,
Cert: cert.ClientCert,
CACert: cert.IssuingCA,
Expiration: cert.Expire,
}
res.Certs = append(res.Certs, view)
}
@@ -64,6 +64,59 @@ func listCerts(svc certs.Service) endpoint.Endpoint {
}
}
func listSerials(svc certs.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listReq)
if err := req.validate(); err != nil {
return nil, err
}
page, err := svc.ListSerials(ctx, req.token, req.thingID, req.offset, req.limit)
if err != nil {
return certsPageRes{}, err
}
res := certsPageRes{
pageRes: pageRes{
Total: page.Total,
Offset: page.Offset,
Limit: page.Limit,
},
Certs: []certsRes{},
}
for _, cert := range page.Certs {
cr := certsRes{
CertSerial: cert.Serial,
}
res.Certs = append(res.Certs, cr)
}
return res, nil
}
}
func viewCert(svc certs.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(viewReq)
if err := req.validate(); err != nil {
return nil, err
}
cert, err := svc.ViewCert(ctx, req.token, req.serialID)
if err != nil {
return certsPageRes{}, err
}
certRes := certsRes{
CertSerial: cert.Serial,
ThingID: cert.ThingID,
Cert: cert.ClientCert,
Expiration: cert.Expire,
}
return certRes, nil
}
}
func revokeCert(svc certs.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(revokeReq)
+30 -2
View File
@@ -1,6 +1,8 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
//go:build !test
package api
import (
@@ -24,7 +26,7 @@ func NewLoggingMiddleware(svc certs.Service, logger log.Logger) certs.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (c certs.Cert, err error) {
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, ttl string, keyBits int, keyType string) (c certs.Cert, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method issue_cert for token: %s and thing: %s took %s to complete", token, thingID, time.Since(begin))
if err != nil {
@@ -34,7 +36,7 @@ func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, days
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
return lm.svc.IssueCert(ctx, token, thingID, ttl, keyBits, keyType)
}
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (cp certs.Page, err error) {
@@ -50,6 +52,32 @@ func (lm *loggingMiddleware) ListCerts(ctx context.Context, token, thingID strin
return lm.svc.ListCerts(ctx, token, thingID, offset, limit)
}
func (lm *loggingMiddleware) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (cp certs.Page, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_serials for token: %s and thing id: %s took %s to complete", token, thingID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListSerials(ctx, token, thingID, offset, limit)
}
func (lm *loggingMiddleware) ViewCert(ctx context.Context, token, serialID string) (c certs.Cert, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method view_cert for token: %s and serial id %s took %s to complete", token, serialID, 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.ViewCert(ctx, token, serialID)
}
func (lm *loggingMiddleware) RevokeCert(ctx context.Context, token, thingID string) (c certs.Revoke, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method revoke_cert for token: %s and thing: %s took %s to complete", token, thingID, time.Since(begin))
+22 -2
View File
@@ -1,6 +1,8 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
//go:build !test
package api
import (
@@ -28,13 +30,13 @@ func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metri
}
}
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (certs.Cert, error) {
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, ttl string, keyBits int, keyType string) (certs.Cert, error) {
defer func(begin time.Time) {
ms.counter.With("method", "issue_cert").Add(1)
ms.latency.With("method", "issue_cert").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
return ms.svc.IssueCert(ctx, token, thingID, ttl, keyBits, keyType)
}
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (certs.Page, error) {
@@ -46,6 +48,24 @@ func (ms *metricsMiddleware) ListCerts(ctx context.Context, token, thingID strin
return ms.svc.ListCerts(ctx, token, thingID, offset, limit)
}
func (ms *metricsMiddleware) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (certs.Page, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_serials").Add(1)
ms.latency.With("method", "list_serials").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListSerials(ctx, token, thingID, offset, limit)
}
func (ms *metricsMiddleware) ViewCert(ctx context.Context, token, serialID string) (certs.Cert, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_cert").Add(1)
ms.latency.With("method", "view_cert").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewCert(ctx, token, serialID)
}
func (ms *metricsMiddleware) RevokeCert(ctx context.Context, token, thingID string) (certs.Revoke, error) {
defer func(begin time.Time) {
ms.counter.With("method", "revoke_cert").Add(1)
+38 -9
View File
@@ -3,7 +3,7 @@
package api
import "github.com/mainflux/mainflux/certs"
import "github.com/mainflux/mainflux/internal/apiutil"
const maxLimitSize = 100
@@ -12,13 +12,22 @@ type addCertsReq struct {
ThingID string `json:"thing_id"`
KeyBits int `json:"key_bits"`
KeyType string `json:"key_type"`
Valid string `json:"valid"`
TTL string `json:"ttl"`
}
func (req addCertsReq) validate() error {
if req.ThingID == "" && req.token == "" {
return errUnauthorized
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.ThingID == "" {
return apiutil.ErrMissingID
}
if req.TTL == "" || req.KeyType == "" || req.KeyBits == 0 {
return apiutil.ErrMissingCertData
}
return nil
}
@@ -31,22 +40,42 @@ type listReq struct {
func (req *listReq) validate() error {
if req.token == "" {
return certs.ErrUnauthorizedAccess
return apiutil.ErrBearerToken
}
if req.limit == 0 || req.limit > maxLimitSize {
return certs.ErrMalformedEntity
if req.limit > maxLimitSize {
return apiutil.ErrLimitSize
}
return nil
}
type viewReq struct {
serialID string
token string
}
func (req *viewReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.serialID == "" {
return apiutil.ErrMissingID
}
return nil
}
type revokeReq struct {
token string
certID string
}
func (req *revokeReq) validate() error {
if req.token == "" || req.certID == "" {
return certs.ErrUnauthorizedAccess
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.certID == "" {
return apiutil.ErrMissingID
}
return nil
+12 -7
View File
@@ -5,6 +5,7 @@ package api
import (
"net/http"
"time"
)
type pageRes struct {
@@ -19,15 +20,15 @@ type certsPageRes struct {
}
type certsRes struct {
ThingID string `json:"thing_id"`
Cert string `json:"cert"`
CertKey string `json:"cert_key"`
CertSerial string `json:"cert_serial"`
CACert string `json:"ca_cert"`
ThingID string `json:"thing_id"`
Cert string `json:"cert"`
CertSerial string `json:"cert_serial"`
Expiration time.Time `json:"expiration"`
created bool
}
func (res certsPageRes) Code() int {
return http.StatusCreated
return http.StatusOK
}
func (res certsPageRes) Headers() map[string]string {
@@ -39,7 +40,11 @@ func (res certsPageRes) Empty() bool {
}
func (res certsRes) Code() int {
return http.StatusCreated
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res certsRes) Headers() map[string]string {
+52 -31
View File
@@ -6,14 +6,14 @@ package api
import (
"context"
"encoding/json"
"io"
"net/http"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/internal/httputil"
"github.com/mainflux/mainflux/internal/apiutil"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
@@ -26,15 +26,10 @@ const (
defLimit = 10
)
var (
errUnauthorized = errors.New("missing or invalid credentials provided")
errConflict = errors.New("entity already exists")
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc certs.Service) http.Handler {
func MakeHandler(svc certs.Service, logger logger.Logger) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
}
r := bone.New()
@@ -46,9 +41,9 @@ func MakeHandler(svc certs.Service) http.Handler {
opts...,
))
r.Get("/certs/:thingId", kithttp.NewServer(
listCerts(svc),
decodeListCerts,
r.Get("/certs/:certId", kithttp.NewServer(
viewCert(svc),
decodeViewCert,
encodeResponse,
opts...,
))
@@ -60,8 +55,15 @@ func MakeHandler(svc certs.Service) http.Handler {
opts...,
))
r.Get("/serials/:thingId", kithttp.NewServer(
listSerials(svc),
decodeListCerts,
encodeResponse,
opts...,
))
r.Handle("/metrics", promhttp.Handler())
r.GetFunc("/version", mainflux.Version("certs"))
r.GetFunc("/health", mainflux.Health("certs"))
return r
}
@@ -85,16 +87,17 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
}
func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
req := listReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
thingID: bone.GetValue(r, "thingId"),
limit: l,
offset: o,
@@ -102,12 +105,21 @@ func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
return req, nil
}
func decodeViewCert(_ context.Context, r *http.Request) (interface{}, error) {
req := viewReq{
token: apiutil.ExtractBearerToken(r),
serialID: bone.GetValue(r, "certId"),
}
return req, nil
}
func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errors.ErrUnsupportedContentType
}
req := addCertsReq{token: r.Header.Get("Authorization")}
req := addCertsReq{token: apiutil.ExtractBearerToken(r)}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
@@ -117,7 +129,7 @@ func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
req := revokeReq{
token: r.Header.Get("Authorization"),
token: apiutil.ExtractBearerToken(r),
certID: bone.GetValue(r, "certId"),
}
@@ -125,23 +137,32 @@ func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error)
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", contentType)
switch err {
case errors.ErrUnsupportedContentType:
switch {
case errors.Contains(err, errors.ErrAuthentication),
err == apiutil.ErrBearerToken:
w.WriteHeader(http.StatusUnauthorized)
case errors.Contains(err, errors.ErrUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case io.EOF, errors.ErrMalformedEntity,
errors.ErrInvalidQueryParams:
case errors.Contains(err, errors.ErrMalformedEntity),
err == apiutil.ErrMissingID,
err == apiutil.ErrMissingCertData,
err == apiutil.ErrLimitSize:
w.WriteHeader(http.StatusBadRequest)
case errConflict:
case errors.Contains(err, errors.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, errors.ErrCreateEntity),
errors.Contains(err, errors.ErrViewEntity),
errors.Contains(err, errors.ErrRemoveEntity):
w.WriteHeader(http.StatusInternalServerError)
default:
switch err.(type) {
case *json.SyntaxError:
w.WriteHeader(http.StatusBadRequest)
case *json.UnmarshalTypeError:
w.WriteHeader(http.StatusBadRequest)
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)
}
}
+9 -6
View File
@@ -18,12 +18,15 @@ type Repository interface {
// Save saves cert for thing into database
Save(ctx context.Context, cert Cert) (string, error)
// RetrieveAll retrieve all issued certificates for given owner and thing id
RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (Page, error)
// RetrieveAll retrieve issued certificates for given owner ID
RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (Page, error)
// Remove certificate from DB for given thing
Remove(ctx context.Context, thingID string) error
// Remove removes certificate from DB for a given thing ID
Remove(ctx context.Context, ownerID, thingID string) error
// RetrieveByThing certificate by given thing
RetrieveByThing(ctx context.Context, thingID string) (Cert, error)
// RetrieveByThing retrieves issued certificates for a given thing ID
RetrieveByThing(ctx context.Context, ownerID, thingID string, offset, limit uint64) (Page, error)
// RetrieveBySerial retrieves a certificate for a given serial ID
RetrieveBySerial(ctx context.Context, ownerID, serialID string) (Cert, error)
}
+72 -22
View File
@@ -8,6 +8,7 @@ import (
"sync"
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/pkg/errors"
)
var _ certs.Repository = (*certsRepoMock)(nil)
@@ -15,44 +16,63 @@ var _ certs.Repository = (*certsRepoMock)(nil)
type certsRepoMock struct {
mu sync.Mutex
counter uint64
certs map[string]certs.Cert
certsByThingID map[string]certs.Cert
certsBySerial map[string]certs.Cert
certsByThingID map[string]map[string][]certs.Cert
}
// NewCertsRepository creates in-memory certs repository.
func NewCertsRepository() certs.Repository {
return &certsRepoMock{
certs: make(map[string]certs.Cert),
certsByThingID: make(map[string]certs.Cert),
certsBySerial: make(map[string]certs.Cert),
certsByThingID: make(map[string]map[string][]certs.Cert),
}
}
func (c *certsRepoMock) Save(ctx context.Context, cert certs.Cert) (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
c.certs[cert.Serial] = cert
c.certsByThingID[cert.ThingID] = cert
crt := certs.Cert{
OwnerID: cert.OwnerID,
ThingID: cert.ThingID,
Serial: cert.Serial,
Expire: cert.Expire,
}
_, ok := c.certsByThingID[cert.OwnerID][cert.ThingID]
switch ok {
case false:
c.certsByThingID[cert.OwnerID] = map[string][]certs.Cert{
cert.ThingID: []certs.Cert{crt},
}
default:
c.certsByThingID[cert.OwnerID][cert.ThingID] = append(c.certsByThingID[cert.OwnerID][cert.ThingID], crt)
}
c.certsBySerial[cert.Serial] = crt
c.counter++
return cert.Serial, nil
}
func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
c.mu.Lock()
defer c.mu.Unlock()
if limit <= 0 {
return certs.Page{}, nil
}
first := offset + 1
last := first + limit
oc, ok := c.certsByThingID[ownerID]
if !ok {
return certs.Page{}, errors.ErrNotFound
}
var crts []certs.Cert
i := uint64(1)
for _, v := range c.certs {
if i >= first && i < last {
crts = append(crts, v)
for _, tc := range oc {
for i, v := range tc {
if uint64(i) >= offset && uint64(i) < offset+limit {
crts = append(crts, v)
}
}
i++
}
page := certs.Page{
@@ -64,24 +84,54 @@ func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID, thingID string
return page, nil
}
func (c *certsRepoMock) Remove(ctx context.Context, serial string) error {
func (c *certsRepoMock) Remove(ctx context.Context, ownerID, serial string) error {
c.mu.Lock()
defer c.mu.Unlock()
crt, ok := c.certs[serial]
crt, ok := c.certsBySerial[serial]
if !ok {
return certs.ErrNotFound
return errors.ErrNotFound
}
delete(c.certs, crt.Serial)
delete(c.certsBySerial, crt.Serial)
delete(c.certsByThingID, crt.ThingID)
return nil
}
func (c *certsRepoMock) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
func (c *certsRepoMock) RetrieveByThing(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
c.mu.Lock()
defer c.mu.Unlock()
crt, ok := c.certsByThingID[thingID]
if !ok {
return certs.Cert{}, certs.ErrNotFound
if limit <= 0 {
return certs.Page{}, nil
}
cs, ok := c.certsByThingID[ownerID][thingID]
if !ok {
return certs.Page{}, errors.ErrNotFound
}
var crts []certs.Cert
for i, v := range cs {
if uint64(i) >= offset && uint64(i) < offset+limit {
crts = append(crts, v)
}
}
page := certs.Page{
Certs: crts,
Total: c.counter,
Offset: offset,
Limit: limit,
}
return page, nil
}
func (c *certsRepoMock) RetrieveBySerial(ctx context.Context, ownerID, serialID string) (certs.Cert, error) {
c.mu.Lock()
defer c.mu.Unlock()
crt, ok := c.certsBySerial[serialID]
if !ok {
return certs.Cert{}, errors.ErrNotFound
}
return crt, nil
}
+35 -13
View File
@@ -14,6 +14,7 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"sync"
"time"
"github.com/mainflux/mainflux/certs/pki"
@@ -32,28 +33,27 @@ type agent struct {
TLSCert tls.Certificate
X509Cert *x509.Certificate
RSABits int
HoursValid string
TTL string
mu sync.Mutex
counter uint64
certs map[string]pki.Cert
}
func NewPkiAgent(tlsCert tls.Certificate, caCert *x509.Certificate, keyBits int, hoursValid string, timeout time.Duration) pki.Agent {
func NewPkiAgent(tlsCert tls.Certificate, caCert *x509.Certificate, keyBits int, ttl string, timeout time.Duration) pki.Agent {
return &agent{
AuthTimeout: timeout,
TLSCert: tlsCert,
X509Cert: caCert,
RSABits: keyBits,
HoursValid: hoursValid,
TTL: ttl,
certs: make(map[string]pki.Cert),
}
}
func (a *agent) IssueCert(cn string, ttl, keyType string, keyBits int) (pki.Cert, error) {
return a.certs(cn, ttl, keyBits)
}
a.mu.Lock()
defer a.mu.Unlock()
func (a *agent) Revoke(serial string) (time.Time, error) {
return time.Now(), nil
}
func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
if a.X509Cert == nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, pki.ErrMissingCACertificate)
}
@@ -64,12 +64,12 @@ func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
if daysValid == "" {
daysValid = a.HoursValid
if ttl == "" {
ttl = a.TTL
}
notBefore := time.Now()
validFor, err := time.ParseDuration(daysValid)
validFor, err := time.ParseDuration(ttl)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
@@ -129,6 +129,12 @@ func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
}
buffKeyOut.Flush()
key := keyOut.String()
a.certs[x509cert.SerialNumber.String()] = pki.Cert{
ClientCert: cert,
}
a.counter++
return pki.Cert{
ClientCert: cert,
ClientKey: key,
@@ -138,6 +144,22 @@ func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
}, nil
}
func (a *agent) Read(serial string) (pki.Cert, error) {
a.mu.Lock()
defer a.mu.Unlock()
crt, ok := a.certs[serial]
if !ok {
return pki.Cert{}, errors.ErrNotFound
}
return crt, nil
}
func (a *agent) Revoke(serial string) (time.Time, error) {
return time.Now(), nil
}
func publicKey(priv interface{}) (interface{}, error) {
if priv == nil {
return nil, errPrivateKeyEmpty
-155
View File
@@ -1,155 +0,0 @@
openapi: 3.0.1
info:
title: Mainflux Certs service
description: HTTP API for Certs service
version: "1.0.0"
paths:
/certs:
post:
summary: Creates a certificate for thing
description: Creates a certificate for thing
tags:
- Thing to proxy
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/CertReq"
responses:
'201':
description: Created
'400':
description: Failed due to malformed JSON.
'500':
description: Unexpected server-side error ocurred.
/certs/{thingId}:
get:
summary: Retrieves certificates
description: |
Retrieves a certificates for given thing ID .
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingID"
responses:
'200':
$ref: "#/components/responses/CertsRes"
'404':
description: |
Failed to retrieve corresponding certificate.
'500':
$ref: "#/components/responses/ServiceError"
/certs/{certId}:
delete:
summary: Revokes certificate
description: |
Revokes a certificates for given cert ID .
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/CertID"
responses:
'200':
$ref: "#/components/responses/RevokeRes"
'404':
description: |
Failed to revoke corresponding certificate.
'500':
$ref: "#/components/responses/ServiceError"
components:
parameters:
Authorization:
name: Authorization
description: User's access token. Used instead of credentials in env or config.toml.
in: header
schema:
type: string
required: false
ThingID:
name: thingID
description: Thing ID
in: path
schema:
type: string
format: uuid
required: true
CertID:
name: certID
description: Serial of certificate
in: path
schema:
type: string
format: uuid
required: true
schemas:
Certs:
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
Revoke:
type: object
properties:
revocation_time:
type: string
description: Certificate revocation time
requestBodies:
CertReq:
description: |
Issues a certificate that is required for mTLS. To create a certificate for a thing
provide a thing id, data identifying particular thing will be embedded into the Certificate.
x509 and ECC certificates are supported when using when Vault is used as PKI.
content:
application/json:
schema:
type: object
required:
- thing_id
- days_valid
- key_bits
properties:
thing_id:
type: string
format: uuid
days_valid:
type: string
key_bits:
type: integer
responses:
ServiceError:
description: Unexpected server-side error occurred.
CertsRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Certs"
RevokeRes:
description: Certificate revoked.
content:
application/json:
schema:
$ref: "#/components/schemas/Revoke"
+45 -17
View File
@@ -17,14 +17,12 @@ import (
const (
issue = "issue"
cert = "cert"
revoke = "revoke"
apiVer = "v1"
)
var (
// ErrNotImplemented indicate that method called is not implemented
ErrNotImplemented = errors.New("method not implemented for certs")
// ErrMissingCACertificate indicates missing CA certificate
ErrMissingCACertificate = errors.New("missing CA certificate for certificate signing")
@@ -35,6 +33,7 @@ var (
ErrFailedCertRevocation = errors.New("failed to revoke certificate")
errFailedVaultCertIssue = errors.New("failed to issue vault certificate")
errFailedVaultRead = errors.New("failed to read vault certificate")
errFailedCertDecoding = errors.New("failed to decode response from vault service")
)
@@ -48,10 +47,14 @@ type Cert struct {
Expire time.Time `json:"expire" mapstructure:"-"`
}
// Agent represents the Vault PKI interface.
type Agent interface {
// IssueCert issues certificate on PKI
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
// Read retrieves certificate from PKI
Read(serial string) (Cert, error)
// Revoke revokes certificate from PKI
Revoke(serial string) (time.Time, error)
}
@@ -62,6 +65,7 @@ type pkiAgent struct {
role string
host string
issueURL string
readURL string
revokeURL string
client *api.Client
}
@@ -77,6 +81,7 @@ type certRevokeReq struct {
SerialNumber string `json:"serial_number"`
}
// NewVaultClient instantiates a Vault client.
func NewVaultClient(token, host, path, role string) (Agent, error) {
conf := &api.Config{
Address: host,
@@ -94,6 +99,7 @@ func NewVaultClient(token, host, path, role string) (Agent, error) {
path: path,
client: client,
issueURL: "/" + apiVer + "/" + path + "/" + issue + "/" + role,
readURL: "/" + apiVer + "/" + path + "/" + cert + "/",
revokeURL: "/" + apiVer + "/" + path + "/" + revoke,
}
return &p, nil
@@ -129,22 +135,47 @@ func (p *pkiAgent) IssueCert(cn string, ttl, keyType string, keyBits int) (Cert,
return Cert{}, errors.Wrap(errFailedVaultCertIssue, err)
}
s, _ := api.ParseSecret(resp.Body)
cert := Cert{}
s, err := api.ParseSecret(resp.Body)
if err != nil {
return Cert{}, err
}
cert := Cert{}
if err = mapstructure.Decode(s.Data, &cert); err != nil {
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
}
// Expire time calc must be revised value doesnt look correct
exp, err := s.Data["expiration"].(json.Number).Float64()
if err != nil {
return cert, err
}
expTime := time.Unix(0, int64(exp)*int64(time.Millisecond))
cert.Expire = expTime
return cert, nil
}
func (p *pkiAgent) Read(serial string) (Cert, error) {
r := p.client.NewRequest("GET", p.readURL+"/"+serial)
resp, err := p.client.RawRequest(r)
if err != nil {
return Cert{}, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
_, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Cert{}, err
}
return Cert{}, errors.Wrap(errFailedVaultRead, err)
}
s, err := api.ParseSecret(resp.Body)
if err != nil {
return Cert{}, err
}
cert := Cert{}
if err = mapstructure.Decode(s.Data, &cert); err != nil {
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
}
return cert, nil
}
func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
@@ -158,13 +189,10 @@ func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
}
resp, err := p.client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return time.Time{}, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
_, err := ioutil.ReadAll(resp.Body)
@@ -183,6 +211,6 @@ func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
if err != nil {
return time.Time{}, err
}
return time.Unix(0, int64(rev)*int64(time.Millisecond)), nil
return time.Unix(0, int64(rev)*int64(time.Millisecond)), nil
}
+44 -35
View File
@@ -14,17 +14,11 @@ import (
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/things"
)
const duplicateErr = "unique_violation"
var (
errSaveDB = errors.New("failed to save certificate to database")
errRetrieveDB = errors.New("failed to retrieve certificate from db")
errRemove = errors.New("failed to remove certificate from database")
errInvalid = "invalid_text_representation"
)
var errInvalid = "invalid_text_representation"
var _ certs.Repository = (*certsRepository)(nil)
@@ -45,7 +39,7 @@ func NewRepository(db *sqlx.DB, log logger.Logger) certs.Repository {
return &certsRepository{db: db, log: log}
}
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 ORDER BY expire LIMIT $2 OFFSET $3;`
rows, err := cr.db.Query(q, ownerID, limit, offset)
if err != nil {
@@ -55,7 +49,6 @@ func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID, thingID stri
defer rows.Close()
certificates := []certs.Cert{}
for rows.Next() {
c := certs.Cert{}
if err := rows.Scan(&c.ThingID, &c.OwnerID, &c.Serial, &c.Expire); err != nil {
@@ -86,7 +79,7 @@ func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, er
tx, err := cr.db.Beginx()
if err != nil {
return "", errors.Wrap(errSaveDB, err)
return "", errors.Wrap(errors.ErrCreateEntity, err)
}
dbcrt := toDBCert(cert)
@@ -99,7 +92,7 @@ func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, er
cr.rollback("Failed to insert a Cert", tx, err)
return "", errors.Wrap(errSaveDB, e)
return "", errors.Wrap(errors.ErrCreateEntity, e)
}
if err := tx.Commit(); err != nil {
@@ -109,52 +102,68 @@ func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, er
return cert.Serial, nil
}
func (cr certsRepository) Remove(ctx context.Context, serial string) error {
if _, err := cr.retrieveBySerial(ctx, serial); err != nil {
return errors.Wrap(errRemove, err)
func (cr certsRepository) Remove(ctx context.Context, ownerID, serial string) error {
if _, err := cr.RetrieveBySerial(ctx, ownerID, serial); err != nil {
return errors.Wrap(errors.ErrRemoveEntity, err)
}
q := `DELETE FROM certs WHERE serial = :serial`
var c certs.Cert
c.Serial = serial
dbcrt := toDBCert(c)
if _, err := cr.db.NamedExecContext(ctx, q, dbcrt); err != nil {
return errors.Wrap(errRemove, err)
return errors.Wrap(errors.ErrRemoveEntity, err)
}
return nil
}
func (cr certsRepository) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE thing_id = $1`
var dbcrt dbCert
var c certs.Cert
if err := cr.db.QueryRowxContext(ctx, q, thingID).StructScan(&dbcrt); err != nil {
pqErr, ok := err.(*pq.Error)
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
return c, errors.Wrap(things.ErrNotFound, err)
}
return c, errors.Wrap(errRetrieveDB, err)
func (cr certsRepository) RetrieveByThing(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 AND thing_id = $2 ORDER BY expire LIMIT $3 OFFSET $4;`
rows, err := cr.db.Query(q, ownerID, thingID, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
return certs.Page{}, err
}
c = toCert(dbcrt)
defer rows.Close()
return c, nil
certificates := []certs.Cert{}
for rows.Next() {
c := certs.Cert{}
if err := rows.Scan(&c.ThingID, &c.OwnerID, &c.Serial, &c.Expire); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return certs.Page{}, err
}
certificates = append(certificates, c)
}
q = `SELECT COUNT(*) FROM certs WHERE owner_id = $1 AND thing_id = $2`
var total uint64
if err := cr.db.QueryRow(q, ownerID, thingID).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count certs due to %s", err))
return certs.Page{}, err
}
return certs.Page{
Total: total,
Limit: limit,
Offset: offset,
Certs: certificates,
}, nil
}
func (cr certsRepository) retrieveBySerial(ctx context.Context, serial string) (certs.Cert, error) {
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE serial = $1`
func (cr certsRepository) RetrieveBySerial(ctx context.Context, ownerID, serialID string) (certs.Cert, error) {
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 AND serial = $2`
var dbcrt dbCert
var c certs.Cert
if err := cr.db.QueryRowxContext(ctx, q, serial).StructScan(&dbcrt); err != nil {
if err := cr.db.QueryRowxContext(ctx, q, ownerID, serialID).StructScan(&dbcrt); err != nil {
pqErr, ok := err.(*pq.Error)
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
return c, errors.Wrap(things.ErrNotFound, err)
return c, errors.Wrap(errors.ErrNotFound, err)
}
return c, errors.Wrap(errRetrieveDB, err)
return c, errors.Wrap(errors.ErrViewEntity, err)
}
c = toCert(dbcrt)
+1 -1
View File
@@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.8-alpine", cfg)
container, err := pool.Run("postgres", "13.3-alpine", cfg)
if err != nil {
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
}
+81 -32
View File
@@ -16,16 +16,6 @@ import (
)
var (
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
// ErrMalformedEntity indicates malformed entity specification
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
// ErrFailedCertCreation failed to create certificate
ErrFailedCertCreation = errors.New("failed to create client certificate")
@@ -41,13 +31,19 @@ var _ Service = (*certsService)(nil)
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// IssueCert issues certificate for given thing id if access is granted with token
IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (Cert, error)
IssueCert(ctx context.Context, token, thingID, ttl string, keyBits int, keyType string) (Cert, error)
// ListCerts lists all certificates issued for given owner
// ListCerts lists certificates issued for a given thing ID
ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error)
// RevokeCert revokes certificate for given thing
RevokeCert(ctx context.Context, token, thingID string) (Revoke, error)
// ListSerials lists certificate serial IDs issued for a given thing ID
ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error)
// ViewCert retrieves the certificate issued for a given serial ID
ViewCert(ctx context.Context, token, serialID string) (Cert, error)
// RevokeCert revokes a certificate for a given serial ID
RevokeCert(ctx context.Context, token, serialID string) (Revoke, error)
}
// Config defines the service parameters
@@ -58,8 +54,7 @@ type Config struct {
HTTPPort string
ServerCert string
ServerKey string
BaseURL string
ThingsPrefix string
CertsURL string
JaegerURL string
AuthURL string
AuthTimeout time.Duration
@@ -110,10 +105,10 @@ type Cert struct {
Expire time.Time `json:"expire" mapstructure:"-"`
}
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, ttl string, keyBits int, keyType string) (Cert, error) {
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return Cert{}, errors.Wrap(ErrUnauthorizedAccess, err)
return Cert{}, err
}
thing, err := cs.sdk.Thing(thingID, token)
@@ -121,14 +116,14 @@ func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, da
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
}
cert, err := cs.pki.IssueCert(thing.Key, daysValid, keyType, keyBits)
cert, err := cs.pki.IssueCert(thing.Key, ttl, keyType, keyBits)
if err != nil {
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
}
c := Cert{
ThingID: thingID,
OwnerID: owner.GetEmail(),
OwnerID: owner.GetId(),
ClientCert: cert.ClientCert,
IssuingCA: cert.IssuingCA,
CAChain: cert.CAChain,
@@ -144,36 +139,90 @@ func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, da
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
var revoke Revoke
_, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return revoke, errors.Wrap(ErrUnauthorizedAccess, err)
return revoke, err
}
thing, err := cs.sdk.Thing(thingID, token)
if err != nil {
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
cert, err := cs.certsRepo.RetrieveByThing(ctx, thing.ID)
// TODO: Replace offset and limit
offset, limit := uint64(0), uint64(10000)
cp, err := cs.certsRepo.RetrieveByThing(ctx, u.GetId(), thing.ID, offset, limit)
if err != nil {
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
revTime, err := cs.pki.Revoke(cert.Serial)
if err != nil {
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
revoke.RevocationTime = revTime
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
for _, c := range cp.Certs {
revTime, err := cs.pki.Revoke(c.Serial)
if err != nil {
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
revoke.RevocationTime = revTime
if err = cs.certsRepo.Remove(context.Background(), u.GetId(), c.Serial); err != nil {
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
}
}
return revoke, nil
}
func (cs *certsService) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error) {
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
return Page{}, err
}
return cs.certsRepo.RetrieveAll(ctx, u.GetEmail(), thingID, offset, limit)
cp, err := cs.certsRepo.RetrieveByThing(ctx, u.GetId(), thingID, offset, limit)
if err != nil {
return Page{}, err
}
for i, cert := range cp.Certs {
vcert, err := cs.pki.Read(cert.Serial)
if err != nil {
return Page{}, err
}
cp.Certs[i].ClientCert = vcert.ClientCert
cp.Certs[i].ClientKey = vcert.ClientKey
}
return cp, nil
}
func (cs *certsService) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error) {
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return Page{}, err
}
return cs.certsRepo.RetrieveByThing(ctx, u.GetId(), thingID, offset, limit)
}
func (cs *certsService) ViewCert(ctx context.Context, token, serialID string) (Cert, error) {
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return Cert{}, err
}
cert, err := cs.certsRepo.RetrieveBySerial(ctx, u.GetId(), serialID)
if err != nil {
return Cert{}, err
}
vcert, err := cs.pki.Read(serialID)
if err != nil {
return Cert{}, err
}
c := Cert{
ThingID: cert.ThingID,
ClientCert: vcert.ClientCert,
Serial: cert.Serial,
Expire: cert.Expire,
}
return c, nil
}
+189 -66
View File
@@ -21,6 +21,7 @@ import (
bsmocks "github.com/mainflux/mainflux/bootstrap/mocks"
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/certs/mocks"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/errors"
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
"github.com/mainflux/mainflux/things"
@@ -38,20 +39,19 @@ const (
thingsNum = 1
thingKey = "thingKey"
thingID = "1"
daysValid = "1h"
ttl = "1h"
keyBits = 2048
key = "rsa"
certNum = 10
cfgLogLevel = "error"
cfgClientTLS = false
cfgServerCert = ""
cfgServerKey = ""
cfgBaseURL = "http://localhost"
cfgThingsPrefix = ""
cfgJaegerURL = ""
cfgAuthURL = "localhost:8181"
cfgAuthTimeout = "1s"
cfgLogLevel = "error"
cfgClientTLS = false
cfgServerCert = ""
cfgServerKey = ""
cfgCertsURL = "http://localhost"
cfgJaegerURL = ""
cfgAuthURL = "localhost:8181"
cfgAuthTimeout = "1s"
caPath = "../docker/ssl/certs/ca.crt"
caKeyPath = "../docker/ssl/certs/ca.key"
@@ -60,12 +60,13 @@ const (
)
func newService(tokens map[string]string) (certs.Service, error) {
users := bsmocks.NewUsersService(map[string]string{token: email})
server := newThingsServer(newThingsService(users))
ac := bsmocks.NewAuthClient(map[string]string{token: email})
server := newThingsServer(newThingsService(ac))
auth := thmocks.NewAuthService(tokens)
policies := []thmocks.MockSubjectSet{{Object: "users", Relation: "member"}}
auth := thmocks.NewAuthService(tokens, map[string][]thmocks.MockSubjectSet{email: policies})
config := mfsdk.Config{
BaseURL: server.URL,
ThingsURL: server.URL,
}
sdk := mfsdk.NewSDK(config)
@@ -86,8 +87,7 @@ func newService(tokens map[string]string) (certs.Service, error) {
ClientTLS: cfgClientTLS,
ServerCert: cfgServerCert,
ServerKey: cfgServerKey,
BaseURL: cfgBaseURL,
ThingsPrefix: cfgThingsPrefix,
CertsURL: cfgCertsURL,
JaegerURL: cfgJaegerURL,
AuthURL: cfgAuthURL,
SignTLSCert: tlsCert,
@@ -120,63 +120,63 @@ func TestIssueCert(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
cases := []struct {
token string
desc string
thingID string
daysValid string
key string
keyBits int
err error
token string
desc string
thingID string
ttl string
key string
keyBits int
err error
}{
{
desc: "issue new cert",
token: token,
thingID: thingID,
daysValid: daysValid,
key: key,
keyBits: 2048,
err: nil,
desc: "issue new cert",
token: token,
thingID: thingID,
ttl: ttl,
key: key,
keyBits: 2048,
err: nil,
},
{
desc: "issue new cert for non existing thing id",
token: token,
thingID: "2",
daysValid: daysValid,
key: key,
keyBits: 2048,
err: certs.ErrFailedCertCreation,
desc: "issue new cert for non existing thing id",
token: token,
thingID: "2",
ttl: ttl,
key: key,
keyBits: 2048,
err: certs.ErrFailedCertCreation,
},
{
desc: "issue new cert for non existing thing id",
token: wrongValue,
thingID: thingID,
daysValid: daysValid,
key: key,
keyBits: 2048,
err: certs.ErrUnauthorizedAccess,
desc: "issue new cert for non existing thing id",
token: wrongValue,
thingID: thingID,
ttl: ttl,
key: key,
keyBits: 2048,
err: errors.ErrAuthentication,
},
{
desc: "issue new cert for bad key bits",
token: token,
thingID: thingID,
daysValid: daysValid,
key: key,
keyBits: -2,
err: certs.ErrFailedCertCreation,
desc: "issue new cert for bad key bits",
token: token,
thingID: thingID,
ttl: ttl,
key: key,
keyBits: -2,
err: certs.ErrFailedCertCreation,
},
{
desc: "issue new cert for bad key bits",
token: token,
thingID: thingID,
daysValid: daysValid,
key: key,
keyBits: -2,
err: certs.ErrFailedCertCreation,
desc: "issue new cert for bad key bits",
token: token,
thingID: thingID,
ttl: ttl,
key: key,
keyBits: -2,
err: certs.ErrFailedCertCreation,
},
}
for _, tc := range cases {
c, err := svc.IssueCert(context.Background(), tc.token, tc.thingID, tc.daysValid, tc.keyBits, tc.key)
c, err := svc.IssueCert(context.Background(), tc.token, tc.thingID, tc.ttl, tc.keyBits, tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
cert, _ := readCert([]byte(c.ClientCert))
if cert != nil {
@@ -190,7 +190,7 @@ func TestRevokeCert(t *testing.T) {
svc, err := newService(map[string]string{token: email})
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
_, err = svc.IssueCert(context.Background(), token, thingID, daysValid, keyBits, key)
_, err = svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
cases := []struct {
@@ -209,7 +209,7 @@ func TestRevokeCert(t *testing.T) {
desc: "revoke cert for invalid token",
token: wrongValue,
thingID: thingID,
err: certs.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "revoke cert for invalid thing id",
@@ -231,7 +231,7 @@ func TestListCerts(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
for i := 0; i < certNum; i++ {
_, err = svc.IssueCert(context.Background(), token, thingID, daysValid, keyBits, key)
_, err = svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
}
@@ -260,10 +260,10 @@ func TestListCerts(t *testing.T) {
offset: 0,
limit: certNum,
size: 0,
err: certs.ErrUnauthorizedAccess,
err: errors.ErrAuthentication,
},
{
desc: "list half certs with invalid token",
desc: "list half certs with valid token",
token: token,
thingID: thingID,
offset: certNum / 2,
@@ -272,7 +272,7 @@ func TestListCerts(t *testing.T) {
err: nil,
},
{
desc: "list last certs with invalid token",
desc: "list last cert with valid token",
token: token,
thingID: thingID,
offset: certNum - 1,
@@ -288,11 +288,134 @@ func TestListCerts(t *testing.T) {
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListSerials(t *testing.T) {
svc, err := newService(map[string]string{token: email})
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
var issuedCerts []certs.Cert
for i := 0; i < certNum; i++ {
cert, err := svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
crt := certs.Cert{
OwnerID: cert.OwnerID,
ThingID: cert.ThingID,
Serial: cert.Serial,
Expire: cert.Expire,
}
issuedCerts = append(issuedCerts, crt)
}
cases := []struct {
token string
desc string
thingID string
offset uint64
limit uint64
certs []certs.Cert
err error
}{
{
desc: "list all certs with valid token",
token: token,
thingID: thingID,
offset: 0,
limit: certNum,
certs: issuedCerts,
err: nil,
},
{
desc: "list all certs with invalid token",
token: wrongValue,
thingID: thingID,
offset: 0,
limit: certNum,
certs: nil,
err: errors.ErrAuthentication,
},
{
desc: "list half certs with valid token",
token: token,
thingID: thingID,
offset: certNum / 2,
limit: certNum,
certs: issuedCerts[certNum/2:],
err: nil,
},
{
desc: "list last cert with valid token",
token: token,
thingID: thingID,
offset: certNum - 1,
limit: certNum,
certs: []certs.Cert{issuedCerts[certNum-1]},
err: nil,
},
}
for _, tc := range cases {
page, err := svc.ListSerials(context.Background(), tc.token, tc.thingID, tc.offset, tc.limit)
assert.Equal(t, tc.certs, page.Certs, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.certs, page.Certs))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestViewCert(t *testing.T) {
svc, err := newService(map[string]string{token: email})
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
ic, err := svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
cert := certs.Cert{
ThingID: thingID,
ClientCert: ic.ClientCert,
Serial: ic.Serial,
Expire: ic.Expire,
}
cases := []struct {
token string
desc string
serialID string
cert certs.Cert
err error
}{
{
desc: "list cert with valid token and serial",
token: token,
serialID: cert.Serial,
cert: cert,
err: nil,
},
{
desc: "list cert with invalid token",
token: wrongValue,
serialID: cert.Serial,
cert: certs.Cert{},
err: errors.ErrAuthentication,
},
{
desc: "list cert with invalid serial",
token: token,
serialID: wrongValue,
cert: certs.Cert{},
err: errors.ErrNotFound,
},
}
for _, tc := range cases {
cert, err := svc.ViewCert(context.Background(), tc.token, tc.serialID)
assert.Equal(t, tc.cert, cert, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.cert, cert))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(mocktracer.New(), svc)
logger := logger.NewMock()
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
return httptest.NewServer(mux)
}
+17 -3
View File
@@ -7,9 +7,9 @@ make cli
## Usage
### Service
#### Get the version of Mainflux services
#### Get Mainflux Things services Health Check
```bash
mainflux-cli version
mainflux-cli health
```
### Users management
@@ -239,4 +239,18 @@ mainflux-cli groups members <group_id> <user_auth_token>
#### List groups that user belongs to
```bash
mainflux-cli groups membership <user_id> <user_auth_token>
```
```
### Keys management
#### Issue a new Key
```bash
mainflux-cli keys issue <duration> <user_auth_token>
```
#### Remove API key from database
```bash
mainflux-cli keys revoke <key_id> <user_auth_token>
```
#### Retrieve API key with given id
```bash
mainflux-cli keys retrieve <key_id> <user_auth_token>
```

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