Compare commits

..

74 Commits

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

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

* fix groups sdk

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

* fix groups sdk

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

* fix groups sdk

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

* small fix, revert some changes

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

* fix test fail

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

* fix unassign request

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

* decouple util method form group repo

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

* resolve comments

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

* fix param parsing

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

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

* fix test for passwd related responses

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

* fix test for passwd related responses

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

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

* Fix comments

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

* Fix comments

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

* Fix reviews

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

* Use internal/http and internalhttp alias

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

* Mv errors types to pkg

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

* Use httputil/query.go and remove aliases

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

* Add blank lines after error definitions

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

* Add ReadBoolValueQuery

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

* Mv readBoolValueQuery

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

* User ErrNotFoundParam instead of pointer

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

* Revert ReadUintQuery to use default values

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

* Use default values for all query readers

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

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

* Update README

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

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

* add certs mock

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

* remove not wanted changes

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

* refactor certs

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

* addint tests

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

* addint tests

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

* adding tests

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

* add certs test

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

* add certs test

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

* add cert test, remove default implementation

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

* fix default value for vault host

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

* add cert test, remove default implementation

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

* linter cleaning

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

* fix comments, and logging

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

* use mocks from other services

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

* rename struct and url path params

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

* resolve minor comments

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

* resolve comments

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

* align url params naming

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

* resolve comments

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

* resolve comments

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

* fix typo

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

* resolve comments

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

* remove struct revoke

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

* refactor certRes

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

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

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

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

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

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

* move all parameters into body

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

* fix swagger

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

* fix error description

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

* Add tests

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

* remove dead code
fix tests

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

* remove unused var

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

* fix sdk tests

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

* add url endpoint for search test

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

* description in swagger
fix tracer string
change test offset

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

* rename in tests searchThReq to searchThingReq

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

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

* fix auth http port

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

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

* add users endpoint for retrieving users from group

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

* remove  groups from things and users

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

* move groups into auth

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

* separate endpoints for users and things

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

* fix problems with retrieving members

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

* add groups test

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

* remove groups from users

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

* remove groups from things

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

* rename constant

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

* add new errors

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

* remove unnecessary constants

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

* fix validation

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

* create groups db mock

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

* adding tests

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

* revert changes to docker related files

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

* remove groups endpoints from users openapi

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

* remove groups endpoints from users openapi

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

* move constant from postgres to groups

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

* move constant from postgres to groups

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

* move constant from postgres to groups

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

* remove testing group

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

* renam typ to groupType

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

* add error for max level

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

* remove print

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

* remove groups.Member interface

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

* fix query building and add test cases

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

* uncomment tests

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

* move groups package

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

* remove group type, add bulk assign and unassign

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

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

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

* update openapi

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

* update openapi for users and things

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

* fix groups test

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

* fix linter errors

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

* resolve comments

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

* rename assignReq structure

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

* refactor mocks, response, remove type from endpoint

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

* some refactor, renaming, errors

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

* simplify check

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

* remove package alias

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

* fix naming and comment

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

* additional comments

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

* add members grpc endpoint test

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

* fix retrieving members for different types

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

* fix retrieving members for different types

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

* remove unecessary structure

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

* fix api grpc

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

* rename const

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

* refactore retrieve parents and children with common function

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

* small changes for errors

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

* fix compile error

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

* fix sorting in mock

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

* remove regexp for groups

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

* revert as change is made by mistake

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

* revert as change is made by mistake

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

* refactor groups and keys package

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

* fix naming

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

* fix naming

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

* fix test for timestamp compare

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

* fix error handling

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

* remove errors not being used

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

* var renaming

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

* resolve comments

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

* minor changes

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

* fix test

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

* add endpoints for groups into nginx

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

* reorganize endpoints, remove some errors

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

* reorganize endpoints, remove some errors

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

* small fix

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

* fix linter errors

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

* minor changes

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

* resolve comments

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

* fix group save path problem

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

* description constant

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

* rename variables

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

* fix validation

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

* get back return

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

* fix compile

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

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

* fix certs, fix scripts, reorganize env vars

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

* set pki path vars

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

* add certs mock

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

* remove not wanted changes

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

* update readme

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

* correct rsa_bits to key_bits

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

* fix tabulation

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

* hardcode vault version

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

* add env desc

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

* renam variables

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

* use Mainflux Labs

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

* replace BG with Belgrade

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

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

* pass regexp to user/api

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

* resolve conflicts

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

* use exported regexp variable

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

* move password validation from users package

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

* remove dead code

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

* add password change request

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

* move regexp from api to users package

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

* fix tests

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

* remove commented code

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

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

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

* add passwd validation in service

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

* Add psswd validation for change password in service

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

* add password validation in password reset

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

* Remove password validation from user validation test

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

* Replace email and passwords in test with constants

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

* compile error not fail silently

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

* fix tempate path

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

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

* fix default port and certs settings

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

* small typo fix

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

* refactor env vars

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

* revert users

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

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

* Type fix

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

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

* Add Note

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

* Remarks resolved

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

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

* Rename grpc endpoint to IsChannelOwner

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

* Add tests for IsChannelOwner grpc

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

* Bump protoc version

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

* Make proto

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

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

* Fix reviews

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

* Remove test

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

* Fix reviews

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

* Add order and direction tests

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

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

* Check if comparison parameter is valid

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

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

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

* Use consts for comparison operators

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

* Use comparator naming instead of comparison

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

* Fix openapi.yml

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

* Fix typo

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

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

* Remove unused Driver option

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

* Add Email Agent README

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

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

* Use cassandra version 3.11.9

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

* Add MF_INFLUXDB_HTTP_AUTH_ENABLED envar

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

* Use influxdb 1.8.4-alpine

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

* Use influxdb 1.8.4

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

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

* update readme file for MF_RELEASE_TAG

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

* update readme file for MF_RELEASE_TAG

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

* fix explanation, space

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

* fix bad search/replace

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

* fix bad search/replace

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

* minor changes

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

* fix readme

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

* improve explanation for tag

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

* replace snippet with link to code section

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

* improve explanation for tag

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

* replace snippet with link to code section

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

* remove MF_RELEASE_TAG from service readme

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

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

* Remove notify

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

* Add initial repo implementation

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

* Add Subscriptions repo implementation

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

* Add working version of main method

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

* Add Notifier API

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

* Add API layer

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

* Add Notifier interface

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

* Fix SMTP notifier main

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

* Fix email sending

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

* Fix main log

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

* Add Readme and fix env vars

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

* Update README

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

* Remove commented code

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

* Rename notifiers to notify

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

* Move Consumer API to writers

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

* Fix contact naming

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

* Fix spacing in init.go

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

* Fix API

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

* Add mocks

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

* Add initial service test

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

* Use pagination

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

* Update List method

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

* Fix listing

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

* Rename SubscriptionPage to Page

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

* Finish service tests

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

* Add DB tests

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

* Add API tests

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

* Add more API tests

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

* Remove Things dependency

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

* Add API docs

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

* Add docs

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

* Fix Things references

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

* Fix AuthN typo

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

* Fix linter warnings

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

* Add license comments

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

* Fix test order

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

* Use service error for wrappers

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

* Fix repo setup test

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

* Use total const in tests

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

* Use constant contacts

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

* Fix code formatting

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

* Rename notifier implementation

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

* Rename Notify to Notifiers

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

* Resolve remarks

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

* Fix naming and docs

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

* Fix docker-compose

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

* Rename Subscriptions to Notifiers service

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

* Fix logging

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

* Fix formatting and typo

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

* Resolve remarks

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

* Fix topic request

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

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

* Upgrade grafana version

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

* Upgrade influxdb to 1.8.3-alpine

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

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

* Remove CLI binary

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

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

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

* Revert cli.ParseConfig()

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

* Check config values before to use them

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

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

* Fix jwt

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

* Fix acces-by-key

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

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

* Fix merge conflicts

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

* Fix typo

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

* Mv Total to MessagesPage

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

* Fix review

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

* Fix readers mock and add filters tests

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

* Add Total check and allow combinations of query parameters

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

* Use slices length as Total

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

* Simplify readers mock

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

* Add empty lines

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

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

* Fix warnings

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

* Fix typo

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

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

* Rm duplicated test parameters

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

* Use pubID2 naming

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

* Fix merge

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

* Fix typo

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

* Fix merge

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

* Rm unecessary *bson.D pointer

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

* Use from >= and to <= conditions and add tests

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

* Revert to half open interva

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-19 09:30:48 +01:00
Manuel Imperiale 0f856f5667 NOISSUE - Rename package aliases uuidProvider into uuid (#1323)
* NOISSUE - Rename pkg aliases uuidProvider into uuid and fix authn typo

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

* Add missing error checks

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

* Use global uuidProvider

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

* Use expTime globally

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

* Fix user uuid provider

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

* Fix review

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

* Use idProvider name

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

* Use idProvider instead of uuidProvider

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

* Use idProvider instead of uuidProvider

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-17 23:12:45 +01:00
Ivan Milošević 0516fe2fd7 MF-1034 - Wrapping MQTT client (#1318)
* use subscribe interface

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

* delete old implementation

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

* add subscribeToLoRaBroker method
change declaration

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

* remove alias for package

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-01-15 23:22:18 +01:00
Manuel Imperiale a8c652f96c MF-1061 - Fix cassandra-reader count for json format (#1327)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-13 10:23:05 +01:00
Manuel Imperiale 8e5a9cfc9a MF-1061 - Implement v, vb, vs, vd and from/to cassandra-reader filters (#1325)
* MF-1061 - Implement v, vb, vs, vd and from/to cassandra-reader filters

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

* Revert JSON implementation

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-12 20:39:31 +01:00
Dušan Borovčanin 6b7dc54c8b NOISSUE - Switch to Consumers interface (#1316)
* Replace Writer with Consumer

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

* Add Notifications package

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

* Update Consumer Start

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

* Fix Readers

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

* Fix Consumer naming

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

* Add repo to Notify

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

* Remove notify

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

* Rename consumer field in middlewares

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

* Fix remarks and add Readme

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2021-01-11 23:55:34 +01:00
Manuel Imperiale 973ca177ea MF-1061 - Implement protocol, name, v, vb, vs, vd and from/to Postgres reader… (#1322)
* MF-1061 - Implement protocol, name, v, vb, vs, vd and from/to postgres-reader filters and tests

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

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-11 13:17:12 +01:00
Manuel Imperiale a185855c06 MF-1061 - Add name, protocol and publisher tests to influxdb-reader (#1320)
* MF-1061 - Add name, protocol and publisher tests to influxdb-reader

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

* Fix typo

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

* Use short package aliases

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-11 10:53:38 +01:00
Manuel Imperiale ac09815457 NOISSUE - Fix Auth typo (#1319)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2021-01-11 10:00:13 +01:00
Ivan Milošević 3eca6920e1 NOISSUE - Add health check for MQTT broker (#1305)
* Add health check

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

* update readme
use constants

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

* close body after each HEAD request

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

* log and close health check response body

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

* use healthcheck endpoint instead ping for docker healthcheck

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

* remove for loop, only one check and exit if failed healthcheck

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

* changed cmd for healthcheck
new version of verne image

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

* use false instead exit 1

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

* return for loop

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

* review

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

* add continue after unsuccesful http.get

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

* Use backoff package

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

* extracting healtcheck function outside main()

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

* go mod vendor

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

* go mod tidy

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

* go mod vendor

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-01-05 19:45:09 +01:00
Dušan Borovčanin e326494166 MF-1264 - Add support for JSON readers (#1295)
* MF-1254 - Create universal JSON writer (#1260)

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

* Add JSON support to Readers

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

* Fix Influx Reader tests

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

* Fix messages format query

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

* Fix Postgres reader

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

* Fix Cassandra Readers and writers

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

* Fix Mongo reader

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

* Extract utility method to the JSON transformer

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

* Fix Influx and Postgres count

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

* Update JSON transformer

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

* Fix Influxdb Reader total count

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

* Refactor init.go for Cassandra writer

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

* Create a Payload type

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

* Add comments for defaults

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

* Fix variable declarations

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

* Replace interface{} with a new type

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

* Don't set channel just to overwrite it later

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

* Fix range search

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

* Rename Messages field

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

Co-authored-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-30 15:43:04 +01:00
Mirko Teodorovic 47217cb5b9 NOISSUE - Merge authz and authn into new service auth (#1313)
* remove owner id

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

* move authz into authn and merge into new service

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

* add groups

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

* add groups

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

* add groups endpoints

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

* add group type

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

* adding mocks, some renaming, refactor

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

* update proto

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

* adding mocks, some renaming, refactor

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

* fix linter err,and comments

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

* undo renaming, add interface for authn and authz

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

* renam some variables

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

* renaming

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

* remove extra slashes from comment

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

* resolving small remarks

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-29 23:02:35 +01:00
Manuel Imperiale b2ccbaec27 MF-1061 - Implement InfluxDB filters value, v, vb, vs, vd, from, to (#1312)
* MF-1061 - Implement InfluxDB filters value, v, vb, vs, vd, from, to

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

* Use time filters as float64 instead of int64

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

* Fix reviews

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

* Remove unnecessary cast

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

* Use a const for limit in tests

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

* Fix typo

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

* Revert float64 cast when dividing

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

* Remove value filter in favour to v

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

* Use v, vb, vs, vd

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

* Use v, vb, vs, vd

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

* Rm unecessary cast

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-25 19:23:54 +01:00
Manuel Imperiale cb9985d160 NOISSUE - Correct readers openapi.yml (#1310)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-21 11:45:37 +01:00
Dušan Borovčanin 1810cec82b NOISSUE - Fix MQTT Forwarder client id (#1309)
* Fix MQTT Forwarder client ID

Don't set client ID for MQTT client in MQTT forwarder. It results in
error in case of multiple instances of the MQTT adapter because they all
share MQTT client ID, which makes broker disconnect all the other
adapters.

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

* Inline broker address option

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

* Add MQTT client username

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

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2020-12-17 17:17:33 +01:00
Mirko Teodorovic d2af0602a0 NOISSUE - Fix dates not being init properly on save, change path construction, replace UUID with ULID for group ID (#1300)
* fix path, group saving with parent

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

* change path, enable name change

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

* revert changes for port

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

* replace UUID with ULID

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

* add ulid

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

* fix migrations

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

* revert user groups

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

* add ulid provider

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

* enable group name change

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

* use null string for parent id

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

* remove migrations, disable group delete if group not empty

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

* revert docker compose

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

* add ulid provider

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

* remove dash character replacment

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

* add ulid lib

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

* rename UUIDProvider

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

* rename package alias

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

* remove unused error

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-17 17:08:28 +01:00
Mirko Teodorovic 8aadc0ff40 NOISSUE - Remove authz from docker comp (#1307)
* remove owner id

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

* remove authz from docker

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

* revert init.go

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-16 18:15:27 +01:00
Darko Draskovic c7d2feb434 Shorten descriptions and add formats (#1306)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-12-16 16:00:56 +01:00
Mirko Teodorovic 18dd8967cc NOISSUE - remove owner id from user table and object (#1303)
* remove owner id

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

* remove owner id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-15 15:57:37 +01:00
Darko Draskovic 9864b27271 NOISSUE - Add missing fields to openapi specs and enclose http codes in single quotes (#1302)
* Add req body to token req

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

* Shorten summaries and add/remove keys

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

* Add single quotes to html response codes

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-12-15 00:20:42 +01:00
Manuel Imperiale 3653e6b5d2 MF-1290 - Sort Things and Channels by name (#1293)
* MF-1290 - Sort Things and Channels by name

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

* Add sort HTTP parameter and use PageMetadata to pass filters

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

* Fix channels conn list

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

* Fix logs

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

* Replace sort naming by order

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

* Add order in responses

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

* Improve tests

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

* Improve order tests

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

* Improve getNameQuery func

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

* Fix commits

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

* Add name-asc and name-desc

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

* Fix typo

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

* Add dir query parameter

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

* Fix openapi

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

* Fix typo

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

* Fix openapi format

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

* Fix test messages and openapi.yml

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

* Fix openapi uuid format

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

* Check query parameters in endpoint layer

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

* Add asc endpoint tests

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

* Rename pageMeta into pageMetadata

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-12-08 21:30:47 +01:00
Mirko Teodorovic fbba7aaa1a MF-1248 - Add access policies for users (#1246)
* authz service init

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

* authz service init

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

* add proto

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

* add proto

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

* authorize method

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

* add casbib

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

* add casbib

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

* add casbib

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

* add policy grpc

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

* add policy grpc

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

* fix db connection, add environment variables, docker-compose

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

* add model.conf policy

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

* add model.conf policy

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

* fix test

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

* adding tests, and token for auth

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

* revert changes

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

* fix auth service

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

* small changes, add model conf env var

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

* remove users grpc for now

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

* fix error

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

* some cosmetics

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

* resolvin minor comments

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

* change endpoint, add file to fix test

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

* fix docker env

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

* fix alignment and path for model.conf

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-08 17:04:09 +01:00
Ivan Savčić 20f5290d7a Fixes, without spaces. (#1296)
Signed-off-by: Ivan Savcic <isavcic@gmail.com>
2020-12-04 18:59:16 +01:00
Darko Draskovic 23bc822433 Add different CNs for CA and certs (#1292)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-11-26 15:32:52 +01:00
Mirko Teodorovic 7195cad0f6 MF-397 - Introduce Thing Groups (#1259)
* add things group

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

* add things group repository

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

* add things group repository

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

* add things group repository

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

* add parents and children methods

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

* add parents and children methods

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

* fix logging message and temporary test fix

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

* fix bootstrap test fail

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

* move groups to pkg

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

* move groups to pkg

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

* move groups to pkg

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

* move groups to internal

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

* move groups to internal

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

* fix import

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

* fix linter errors

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

* fix comments

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

* resolve comments, add hierarchy info when retrieving groups

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

* code refactor, separate http into multiple files

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

* code refactor, separate http into multiple files

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

* fix group update

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

* use user id when saving

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

* use user id when saving

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

* rename methods

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

* move code

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

* move code

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

* remove temporary test

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

* add groups test

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

* fix comments, fix responses in api for groups

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

* revert changes

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

add checks for name length

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

fix validation

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

small change to response

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

add comment for Level

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

add level to limit hierarchy retrieval

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

tidy vendor

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

add level to limit hierarchy retrieval

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

mod tidy

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

revert

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

resolve comments

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

small naming and code organize refactor

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

revert Member type

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

fix typo

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

use ltree

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

use ltree

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

add level for retrieving

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

add level

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

add ltre

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

upgrade postgres version in test

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

add ltre

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

remove test for now

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

minor fixes

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

fix id setting

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

fix tree endpoint

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

minor style changes

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

* small changes, adding new lines

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

* minor changes

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

* change function signature

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

* change primary key

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

* change function signature

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

* simplufy code

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

* remove groups test for now

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

* fix tabulation

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

* fix whitespace

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

* remove white space, fix grammar

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

* fix sqls so that not additional retrieve is needed

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

* fix sqls so that not additional retrieve is needed

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

fix primary key

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

remove retrive by name

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

* remove name and parent update

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

* fix cound sql

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

* add line

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

* simplify sql, fix table name

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

* add date

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

* add date

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

* add timestamp to api

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

* fix var name

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

* fix var name

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-11-23 11:34:29 +01:00
Dušan Borovčanin 663bd4e18d Add Enhancement section to the issue template (#1284)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-11-17 22:52:56 +01:00
Darko Draskovic c03644524e Fix hardcoded env var values (#1283)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2020-11-16 20:44:25 +01:00
Dušan Borovčanin f1aa32d89c NOISSUE - Improve AuthN service docs (#1282)
* Update AuthN service README

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

* Update Authn service docs

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-11-13 21:46:04 +01:00
João Matos 3273c30d8b MF-1268 - CLI improvements (#1274)
* Prefix error messages in CLI with a bold "error: ".

Signed-off-by: Joao Matos <joao@tritao.eu>

* Remove duplicated "Usage: " from groups command help.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Add a raw output mode for CLI and use it on logCreated.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Add CLI global flag for user auth token.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Add CLI config flag and parsing logic.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Refactor CLI users commands outside array structure.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Refactor CLI certificates commands using flags.

Signed-off-by: Joao Matos <joao@tritao.eu>

* Refactor CLI things create command using flags.

Signed-off-by: Joao Matos <joao@tritao.eu>

Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2020-11-01 00:29:06 +01:00
João Matos 46c675cd5f NOISSSUE - Vault integration as an addon. (#1266)
Integrates Vault PKI service as a service addon.

Also adds some helper scripts to help setup the CA in Vault, as well as
some docs to explain how to use them.

Originally based from https://github.com/mteodor/vault.

Signed-off-by: Joao Matos <joao@tritao.eu>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2020-10-31 21:44:25 +01:00
Dušan Borovčanin 02db4066b1 Fix naming in Authn API tests (#1275)
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-10-27 20:08:16 +01:00
Dušan Borovčanin f6b1ae735c MF-1244 - Return UserID alongside with user Email in Identify response (#1245)
* Add both an ID and an Email to API key requests

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

* Use return UserIdentity response

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

* Replace GetValue with GetEmail

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

* Refactor Mainflux Key

Add `Subject` field and reorganize Key manipulation.
**Remove backward compatibility**

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

* Fix service test

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

* Fix DB tests

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

* Fix API tests

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

* Fix JWT tests

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

* Uncomment and fix API tests

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

* Fix SQL statements alignment

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

* Fix Issue method docs

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

* Fix Retrieve API and API docs

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

* Update tests

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
2020-10-27 19:42:53 +01:00
Manuel Imperiale 926e9799d6 NOISSUE - Fix ViewGroup and UpdateGroup (#1269)
* NOISSUE - Fix ViewGroup and UpdateGroup

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

* Add ID check and fix naming

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

* Fix id in groupUpdateReq

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

* Fix review

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

* Add tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-27 11:26:32 +01:00
Manuel Imperiale 1c298d8f27 NOISSUE - Add ListUsers, ViewUser and ViewProfile methods (#1262)
* NOISSUE - Add admin method in users service to return users list

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

* Fix loggings and metrics

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

* Add email and metadata filters

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

* Fix typo

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

* Add comment

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

* Retrieve User infos by ID if Admin

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

* Remove admin checks and fix comments

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

* Fix missing query

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

* Use generic funccs to create email and metadata queries

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

* Add /users/profile endpoint

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

* Simplify db helpers

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

* Fix View, List, Retrieve prefix methods naming

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

* Fix tracer endpoints naming

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

* Fix comment

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

* Fix typo

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

* Fix typo

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

* Add tests and remove TODO comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-26 10:17:08 +01:00
Manuel Imperiale 420b598ac7 NOISSUE - Rm users http package (#1256)
* NOISSUE - Mv users http gokit layers to http package

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

* Remove users/api/http dir

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-16 18:00:24 +02:00
Manuel Imperiale 86fe3f7cdb NOISSUE - Remove content-type check from decodeListUserGroupsRequest (#1255)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2020-10-16 12:54:27 +02:00
420 changed files with 26768 additions and 10785 deletions
+10
View File
@@ -28,3 +28,13 @@ the following locations:
5. In what environment did you encounter the issue?
6. Additional information you deem important:
**ENHANCEMENT**
1. Describe the enhancement you are requesting. Enhancements include:
- tests
- code refactor
- documentation
- research
- tooling
2. Indicate the importance of this enhancement to you (must-have, should-have, nice-to-have).
+36
View File
@@ -0,0 +1,36 @@
# Adopters
As Mainflux Community grows, we'd like to keep track of Mainflux adopters to grow the community, contact other users, share experiences and best practices.
To accomplish this, we created a public ledger. The list of organizations and users who consider themselves as Mainflux adopters and that **publicly/officially** shared information and/or details of their adoption journey(optional).
Where users themselves directly maintain the list.
## Adding yourself as an adopter
If you are using Mainflux, please consider adding yourself as an adopter with a brief description of your use case by opening a pull request to this file and adding a section describing your adoption of Mainflux technology.
**Please send PRs to add or remove organizations/users**
### Format
```
N: Name of user (company or individual)
D: Short Use Case Description (optional)
L: Link with further information (optional)
T: Type of adaptation: Evaluation, Core Technology, Production Usage (optional)
```
## Requirements
* You must represent the user or organization listed. Do NOT add entries on behalf of other organizations or individuals.
Pull request commit must be [signed](https://docs.github.com/en/github/authenticating-to-github/signing-commits) and auto-checked with [ Developer Certificate of Origin (DCO)](https://probot.github.io/apps/dco/)
* There is no minimum requirement or adaptation size, but we request to list permanent deployments only, i.e., no demo or trial deployments. Commercial or production use is not required. A well-done home lab setup can be equally impressive as a large-scale commercial deployment.
**The list of organizations/users that have publicly shared the usage of Mainflux:**
**Note**: Several other organizations/users couldn't publicly share their usage details but are active project contributors and Mainflux Community members.
## Adopters list (alphabetical)
**Note:** The list is maintained by the users themselves. If you find yourself on this list, and you think it's inappropriate. Please contact [project maintainers](https://github.com/mainflux/mainflux/blob/master/MAINTAINERS) and you will be permanently removed from the list.
+114
View File
@@ -11,6 +11,120 @@ Otherwise, whole log in a similar format can be observed via:
git log --pretty=oneline --abbrev-commit
```
## 0.12.0 - 29. MAR 2021.
### Features and Bugfixes
- MF-1394 - SDK groups (#1396)
- NOISSUE - fix response for passwd endpoints (#1393)
- NOISSUE - dont retrieve groups (#1392)
- MF-1368 - Add internal http api package for query params reading (#1384)
- MF-1390 - Fix docker-compose env_file (#1391)
- NOISSUE - put order direction in response body (#1387)
- NOISSUE - Certs service refactor (#1369)
- MF-1357 - Add new endpoint for searching things (#1383)
- NOISSUE - Add missing auth port in nginx enrypoint.sh (#1380)
- MF-1346 - Create Groups API - add grouping of entities (#1334)
- NOISSUE - Fix certs and vault deployment, reorganize and remove unnecessary vars (#1368)
- MF-1317 - Configurable regexp rule for password (#1355)
- Fix CoAP Adapter README (#1376)
- NOISSUE - Fix default values for port and x509 provision (#1367)
- NOISSUE - Added missing endpoints for users service (#1372)
- MF-1365 - Add ADOPTERS.md file (#1371)
- Fix grpc endpoint parameter permutation (#1370)
- NOISSUE - Add IsChannelOwner grpc endpoint (#1366)
- MF-1362 - Sort Things and Channels connections by name (#1363)
- MF-1314 - Add value comparison filters for readers (#1353)
- Fix env configuration and documentation (#1360)
- NOISSUE - Support disabling Email Agent authentication (#1356)
- NOISSUE - Upgrade Mongo, Cassandra and Influx docker images (#1354)
- NOISSUE - Add READMEs to pkg packages (#1352)
- NOISSUE - Correct README (#1349)
- MF-1342 - Use environment variables in docker-compose to use tagged version of image (#1343)
- MF-1311 - Add Notifications service (#1324)
- MF-1344 - Fix links to API documentations #1345
- NOISSUE - Upgrade influxdb and postgres docker images (#1341)
- NOISSUE - Revert cli to use user token from command args (#1339)
- MF-1276 - Fix openapi IDs and Keys format (#1338)
- MF-1061 - Add PageMetadata to readers (#1333)
- NOISSUE - Fix run script and compiler warnings (#1336)
- Fix Postgres writer transaction handling (#1335)
- Make Transformer type configurable (#1331)
- MF-1061 - Implement v, vb, vs, vd and from/to mongodb-reader filters (#1326)
- NOISSUE - Rename package aliases uuidProvider into uuid (#1323)
- MF-1034 - Wrapping MQTT client (#1318)
- MF-1061 - Fix cassandra-reader count for json format (#1327)
- MF-1061 - Implement v, vb, vs, vd and from/to cassandra-reader filters (#1325)
- NOISSUE - Switch to Consumers interface (#1316)
- MF-1061 - Implement protocol, name, v, vb, vs, vd and from/to Postgres reader… (#1322)
- MF-1061 - Add name, protocol and publisher tests to influxdb-reader (#1320)
- NOISSUE - Fix Auth typo (#1319)
- NOISSUE - Add health check for MQTT broker (#1305)
- MF-1264 - Add support for JSON readers (#1295)
- NOISSUE - Merge authz and authn into new service auth (#1313)
- MF-1061 - Implement InfluxDB filters value, v, vb, vs, vd, from, to (#1312)
- NOISSUE - Correct readers openapi.yml (#1310)
- NOISSUE - Fix MQTT Forwarder client id (#1309)
- NOISSUE - Fix dates not being init properly on save, change path construction, replace UUID with ULID for group ID (#1300)
- NOISSUE - Remove authz from docker comp (#1307)
- Shorten descriptions and add formats (#1306)
- NOISSUE - remove owner id from user table and object (#1303)
- NOISSUE - Add missing fields to openapi specs and enclose http codes in single quotes (#1302)
- MF-1290 - Sort Things and Channels by name (#1293)
- MF-1248 - Add access policies for users (#1246)
- Fixes, without spaces. (#1296)
- Add different CNs for CA and certs (#1292)
- MF-397 - Introduce Thing Groups (#1259)
- Add Enhancement section to the issue template (#1284)
- Fix hardcoded env var values (#1283)
- NOISSUE - Improve AuthN service docs (#1282)
- MF-1268 - CLI improvements (#1274)
- NOISSSUE - Vault integration as an addon. (#1266)
- Fix naming in Authn API tests (#1275)
- MF-1244 - Return UserID alongside with user Email in Identify response (#1245)
- NOISSUE - Fix ViewGroup and UpdateGroup (#1269)
- NOISSUE - Add ListUsers, ViewUser and ViewProfile methods (#1262)
- NOISSUE - Rm users http package (#1256)
- NOISSUE - Remove content-type check from decodeListUserGroupsRequest (#1255)
- NOISSUE - Migrate swaggers to openapi 3 spec (#1250)
- Update MQTT Broker Docker scripts (#1253)
- update mproxy version (#1251)
- NOISSUE - Fix group retrieval when parent id is not specified (#1247)
- NOISSUE - Add new endpoint to retrieve configuration to be used as a template. (#1242)
- NOISSUE - Add user groups (#1228)
- MF-1237 - Return to transport only things service errors (#1236)
- MF-928 - Change CoAP lib (#1233)
- NOISSUE - Simplify make cleandocker (#1230)
- NOISSUE - Fix malformed Swagger API specs (#1229)
- MF-435 - Add support for env file loading (#1223)
- update certs docs (#1227)
- NOISSUE - Fix certs update in bootstrap config and make content handling in config.toml user friendly (#1221)
- NOISSUE - Fix typo in authorization.js (#1226)
- MF-983 - Add HTTP query param to connections list endpoints to fetch disconnected Things or Channels (#1217)
- MF-1179 - Add a certificate service and certs endpoint to SDK (#1188)
- NOISUE - Fix cache error when key is not in Redis (#1220)
- MF-1199 - Add NATS messaging tests (#1209)
- NOISSUE: Fix emailer (#1219)
- NOISSUE - Update dependencies (#1218)
- NOISSUE - Add subtopic wildcard for twin attribute's definition (#1214)
- fix envs for nginx (#1215)
- Remove twin mqtt related obsolete var and fix es-redis address (#1213)
- NOISSUE - Remove unused `MF_THINGS_SECRET` env var (#1211)
- NOISSUE - Fix some typos (#1212)
- NOISSUE - Remove unknown Bootstrap requests (#1210)
- NOISSUE - Use `pgcrypto` instead `uuid-ossp` for UUIDs generation (version 4) (#1208)
- MF-1198 - Add errors package tests (#1207)
- MF-1025 - timeout env in sec, use parseduration (#1206)
- MF-1201 - Fix MF_THINGS_AUTH_GRPC_URL mongo reader ENVAR (#1203)
- NOISSUE - Fix CI (#1204)
- MF-1180 - Add redis based twins and states cache (#1184)
- MF-739 - Add ID to the User entity (#1152)
- NOISSUE - Fix default db name for storage databases (#1194)
- NOISSUE - Add `MF_DOCKER_IMAGE_NAME_PREFIX` to Makefile (#1173)
- MF-1154 - Move UUID provider to project root (#1172)
- Fix typo in error messages (#1193)
- MF-1190 - Add pkg for library packages (#1191)
- MF-1177 - Implement caching in MQTT adapter (#1187)
- NOISSUE - Refactor provision tool (#1189)
## 0.11.0 - 29. MAY 2020.
### Features and Bugfixes
- Add VerneMQ docker image build from source (#1178)
+1 -1
View File
@@ -5,7 +5,7 @@ MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
BUILD_DIR = build
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
bootstrap opcua authn twins mqtt provision certs
bootstrap opcua auth twins mqtt provision certs smtp-notifier
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
+26 -5
View File
@@ -10,7 +10,7 @@
Mainflux is modern, scalable, secure, open-source, and patent-free IoT cloud platform written in Go.
It accepts user and thing connections over various network protocols (i.e. HTTP,
It accepts user and thing (sensor, actuator, application) connections over various network protocols (i.e. HTTP,
MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware
for building complex IoT solutions.
@@ -25,12 +25,14 @@ which maintains the project and offers professional services around it.
- Multi-protocol connectivity and bridging (HTTP, MQTT, WebSocket and CoAP)
- Device management and provisioning (Zero Touch provisioning)
- Mutual TLS Authentication (mTLS) using X.509 Certificates
- Fine-grained access control
- Fine-grained access control (policies, ABAC/RBAC)
- Message persistence (Cassandra, InfluxDB, MongoDB and PostgresSQL)
- Platform logging and instrumentation support (Grafana, Prometheus and OpenTracing)
- Event sourcing
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
- [LoRaWAN][lora] network integration
- [OPC UA](opcua) integration
- Edge [Agent](agent) and [Export](export) services for remote IoT gateway management and edge computing
- SDK
- CLI
- Small memory footprint and fast execution
@@ -40,8 +42,8 @@ which maintains the project and offers professional services around it.
The following are needed to run Mainflux:
- [Docker](https://docs.docker.com/install/) (version 18.09)
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.24.1)
- [Docker](https://docs.docker.com/install/) (version 20.10)
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.28)
Developing Mainflux will also require:
@@ -62,9 +64,25 @@ This will bring up the Mainflux docker services and interconnect them. This comm
make run
```
If you want to run services from specific release checkout code from github and make sure that
`MF_RELEASE_TAG` in [.env](.env) is being set to match the release version
```bash
git checkout tags/<release_number> -b <release_number>
# e.g. `git checkout tags/0.12.0 -b 0.12.0`
```
Check that `.env` file contains:
```bash
MF_RELEASE_TAG=<release_number>
```
>`docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://mainflux.readthedocs.io/en/latest/kubernetes/).
## Usage
The quickest way to start using Mainflux is via the CLI. The latest version can be downloaded from the [official releases page][rel].
The quickest way to start using Mainflux is via the CLI. The latest version can be downloaded from the [official releases page][rel].
It can also be built and used from the project's root directory:
@@ -143,6 +161,9 @@ please head to company's [careers page][careers] or shoot us an e-mail at <caree
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
[twitter]: https://twitter.com/mainflux
[lora]: https://lora-alliance.org/
[opcua]: https://opcfoundation.org/about/opc-technologies/opc-ua/
[agent]: https://github.com/mainflux/agent
[export]: https://github.com/mainflux/export
[kubernetes]: https://kubernetes.io/
[rel]: https://github.com/mainflux/mainflux/releases
[careers]: https://www.mainflux.com/careers.html
+3794
View File
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package mainflux;
import "google/protobuf/empty.proto";
service ThingsService {
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
rpc IsChannelOwner(ChannelOwnerReq) returns (google.protobuf.Empty) {}
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
rpc Identify(Token) returns (ThingID) {}
}
service AuthService {
rpc Issue(IssueReq) returns (Token) {}
rpc Identify(Token) returns (UserIdentity) {}
rpc Authorize(AuthorizeReq) returns (AuthorizeRes) {}
rpc Assign(Assignment) returns(google.protobuf.Empty) {}
rpc Members(MembersReq) returns (MembersRes) {}
}
message AccessByKeyReq {
string token = 1;
string chanID = 2;
}
message ChannelOwnerReq {
string owner = 1;
string chanID = 2;
}
message ThingID {
string value = 1;
}
message ChannelID {
string value = 1;
}
message AccessByIDReq {
string thingID = 1;
string chanID = 2;
}
// If a token is not carrying any information itself, the type
// field can be used to determine how to validate the token.
// Also, different tokens can be encoded in different ways.
message Token {
string value = 1;
}
message UserIdentity {
string id = 1;
string email = 2;
}
message IssueReq {
string id = 1;
string email = 2;
uint32 type = 3;
}
message AuthorizeReq {
string sub = 1;
string obj = 2;
string act = 3;
}
message AuthorizeRes {
bool authorized = 1;
}
message Assignment {
string token = 1;
string groupID = 2;
string memberID = 3;
}
message MembersReq {
string token = 1;
string groupID = 2;
uint64 offset = 3;
uint64 limit = 4;
string type = 5;
}
message MembersRes {
uint64 total = 1;
uint64 offset = 2;
uint64 limit = 3;
string type = 4;
repeated string members = 5;
}
+108
View File
@@ -0,0 +1,108 @@
# Auth - Authentication and Authorization service
Auth service provides authentication features as an API for managing authentication keys as well as administering groups of entities - `things` and `users`.
# Authentication
User service is using Auth service gRPC API to obtain login token or password reset token. Authentication key consists of the following fields:
- ID - key ID
- Type - one of the three types described below
- IssuerID - an ID of the Mainflux User who issued the key
- Subject - user email
- IssuedAt - the timestamp when the key is issued
- ExpiresAt - the timestamp after which the key is invalid
There are *three types of authentication keys*:
- User key - keys issued to the user upon login request
- API key - keys issued upon the user request
- Recovery key - password recovery key
Authentication keys are represented and distributed by the corresponding [JWT](jwt.io).
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user.
API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. This also means that, despite being used as a JWT, it requires a query to the database to validate the API key. The user with API key can perform all the same actions as the user with login key (can act on behalf of the user for Thing, Channel, or user profile management), *except issuing new API keys*.
Recovery key is the password recovery key. It's short-lived token used for password recovery process.
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
The following actions are supported:
- create (all key types)
- verify (all key types)
- obtain (API keys only)
- revoke (API keys only)
# Groups
User and Things service are using Auth gRPC API to get the list of ids that are part of a group. Groups can be organized as tree structure.
Group consists of the following fields:
- ID - ULID id uniquely representing group
- Name - name of the group, name of the group is unique at the same level of tree hierarchy for a given tree.
- ParentID - id of the parent group
- OwnerID - id of the user that created a group
- Description - free form text, up to 1024 characters
- Metadata - Arbitrary, object-encoded group's data
- Path - tree path consisting of group ids
- CreatedAt - timestamp at which the group is created
- UpdatedAt - timestamp at which the group is updated
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|---------------------------|--------------------------------------------------------------------------|---------------|
| MF_AUTH_LOG_LEVEL | Service level (debug, info, warn, error) | error |
| MF_AUTH_DB_HOST | Database host address | localhost |
| MF_AUTH_DB_PORT | Database host port | 5432 |
| MF_AUTH_DB_USER | Database user | mainflux |
| MF_AUTH_DB_PASSWORD | Database password | mainflux |
| MF_AUTH_DB | Name of the database used by the service | auth |
| MF_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| MF_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | |
| MF_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | |
| MF_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
| MF_AUTH_HTTP_PORT | Auth service HTTP port | 8180 |
| MF_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
| MF_AUTH_SERVER_CERT | Path to server certificate in pem format | |
| MF_AUTH_SERVER_KEY | Path to server key in pem format | |
| MF_AUTH_SECRET | String used for signing tokens | auth |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831|
## Deployment
The service itself is distributed as Docker container. Check the [`auth`](https://github.com/mainflux/mainflux/blob/master/docker/docker-compose.yml#L71-L94) service section in
docker-compose to see how service is deployed.
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the service
make auth
# copy binary to bin
make install
# set the environment variables and run the service
MF_AUTH_LOG_LEVEL=[Service log level] MF_AUTH_DB_HOST=[Database host address] MF_AUTH_DB_PORT=[Database host port] MF_AUTH_DB_USER=[Database user] MF_AUTH_DB_PASS=[Database password] MF_AUTH_DB=[Name of the database used by the service] MF_AUTH_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTH_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTH_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTH_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTH_HTTP_PORT=[Service HTTP port] MF_AUTH_GRPC_PORT=[Service gRPC port] MF_AUTH_SECRET=[String used for signing tokens] MF_AUTH_SERVER_CERT=[Path to server certificate] MF_AUTH_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-auth
```
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](openapi.yml).
[doc]: http://mainflux.readthedocs.io
+1 -1
View File
@@ -1,5 +1,5 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package api contains implementation of AuthN service HTTP API.
// Package api contains implementation of Auth service HTTP API.
package api
+226
View File
@@ -0,0 +1,226 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
kitot "github.com/go-kit/kit/tracing/opentracing"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/golang/protobuf/ptypes/empty"
"github.com/mainflux/mainflux"
opentracing "github.com/opentracing/opentracing-go"
"google.golang.org/grpc"
)
const (
svcName = "mainflux.AuthService"
)
var _ mainflux.AuthServiceClient = (*grpcClient)(nil)
type grpcClient struct {
issue endpoint.Endpoint
identify endpoint.Endpoint
authorize endpoint.Endpoint
assign endpoint.Endpoint
members endpoint.Endpoint
timeout time.Duration
}
// NewClient returns new gRPC client instance.
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthServiceClient {
return &grpcClient{
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
conn,
svcName,
"Issue",
encodeIssueRequest,
decodeIssueResponse,
mainflux.UserIdentity{},
).Endpoint()),
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
conn,
svcName,
"Identify",
encodeIdentifyRequest,
decodeIdentifyResponse,
mainflux.UserIdentity{},
).Endpoint()),
authorize: kitot.TraceClient(tracer, "authorize")(kitgrpc.NewClient(
conn,
svcName,
"Authorize",
encodeAuthorizeRequest,
decodeAuthorizeResponse,
mainflux.AuthorizeRes{},
).Endpoint()),
assign: kitot.TraceClient(tracer, "assign")(kitgrpc.NewClient(
conn,
svcName,
"Assign",
encodeAssignRequest,
decodeAssignResponse,
mainflux.AuthorizeRes{},
).Endpoint()),
members: kitot.TraceClient(tracer, "members")(kitgrpc.NewClient(
conn,
svcName,
"Members",
encodeMembersRequest,
decodeMembersResponse,
mainflux.MembersRes{},
).Endpoint()),
timeout: timeout,
}
}
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.issue(ctx, issueReq{id: req.GetId(), email: req.GetEmail(), keyType: req.Type})
if err != nil {
return nil, err
}
ir := res.(identityRes)
return &mainflux.Token{Value: ir.id}, nil
}
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(issueReq)
return &mainflux.IssueReq{Id: req.id, Email: req.email, Type: req.keyType}, nil
}
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.UserIdentity)
return identityRes{id: res.GetId(), email: res.GetEmail()}, nil
}
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserIdentity, error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
if err != nil {
return nil, err
}
ir := res.(identityRes)
return &mainflux.UserIdentity{Id: ir.id, Email: ir.email}, nil
}
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(identityReq)
return &mainflux.Token{Value: req.token}, nil
}
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.UserIdentity)
return identityRes{id: res.GetId(), email: res.GetEmail()}, nil
}
func (client grpcClient) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.authorize(ctx, authReq{Act: req.Act, Obj: req.Obj, Sub: req.Sub})
if err != nil {
return &mainflux.AuthorizeRes{Authorized: false}, err
}
ar := res.(authorizeRes)
return &mainflux.AuthorizeRes{Authorized: ar.authorized}, err
}
func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.AuthorizeRes)
return authorizeRes{authorized: res.Authorized}, nil
}
func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(authReq)
return &mainflux.AuthorizeReq{
Sub: req.Sub,
Obj: req.Obj,
Act: req.Act,
}, nil
}
func (client grpcClient) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.members(ctx, membersReq{
token: req.GetToken(),
groupID: req.GetGroupID(),
memberType: req.GetType(),
offset: req.GetOffset(),
limit: req.GetLimit(),
})
if err != nil {
return &mainflux.MembersRes{}, err
}
mr := res.(membersRes)
return &mainflux.MembersRes{
Offset: mr.offset,
Limit: mr.limit,
Total: mr.total,
Type: mr.groupType,
Members: mr.members,
}, err
}
func encodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(membersReq)
return &mainflux.MembersReq{
Token: req.token,
Offset: req.offset,
Limit: req.limit,
GroupID: req.groupID,
Type: req.memberType,
}, nil
}
func decodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.MembersRes)
return membersRes{
offset: res.Offset,
limit: res.Limit,
total: res.Total,
members: res.Members,
}, nil
}
func (client grpcClient) Assign(ctx context.Context, req *mainflux.Assignment, _ ...grpc.CallOption) (r *empty.Empty, err error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
_, err = client.assign(ctx, assignReq{token: req.GetToken(), groupID: req.GetGroupID(), memberID: req.GetMemberID()})
if err != nil {
return &empty.Empty{}, err
}
return &empty.Empty{}, err
}
func encodeAssignRequest(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.AuthorizeRes)
return authorizeRes{authorized: res.Authorized}, nil
}
func decodeAssignResponse(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(authReq)
return &mainflux.AuthorizeReq{
Sub: req.Sub,
Obj: req.Obj,
Act: req.Act,
}, nil
}
@@ -1,9 +1,5 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package grpc contains implementation of Auth service gRPC API.
package grpc
type identityRes struct {
id string
err error
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/auth"
)
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(issueReq)
if err := req.validate(); err != nil {
return issueRes{}, err
}
key := auth.Key{
Type: req.keyType,
Subject: req.email,
IssuerID: req.id,
IssuedAt: time.Now().UTC(),
}
_, secret, err := svc.Issue(ctx, "", key)
if err != nil {
return issueRes{}, err
}
return issueRes{secret}, nil
}
}
func identifyEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(identityReq)
if err := req.validate(); err != nil {
return identityRes{}, err
}
id, err := svc.Identify(ctx, req.token)
if err != nil {
return identityRes{}, err
}
ret := identityRes{
id: id.ID,
email: id.Email,
}
return ret, nil
}
}
func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(authReq)
if err := req.validate(); err != nil {
return authorizeRes{}, err
}
_, err := svc.Identify(ctx, req.token)
if err != nil {
return authorizeRes{}, err
}
authorized, err := svc.Authorize(ctx, req.token, req.Sub, req.Obj, req.Obj)
if err != nil {
return authorizeRes{}, err
}
return authorizeRes{authorized: authorized}, err
}
}
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(assignReq)
if err := req.validate(); err != nil {
return emptyRes{}, err
}
_, err := svc.Identify(ctx, req.token)
if err != nil {
return emptyRes{}, err
}
err = svc.Assign(ctx, req.token, req.memberID, req.groupID, req.groupType)
if err != nil {
return emptyRes{}, err
}
return emptyRes{}, nil
}
}
func membersEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(membersReq)
if err := req.validate(); err != nil {
return membersRes{}, err
}
pm := auth.PageMetadata{
Offset: req.offset,
Limit: req.limit,
}
mp, err := svc.ListMembers(ctx, req.token, req.groupID, req.memberType, pm)
if err != nil {
return membersRes{}, err
}
var members []string
for _, m := range mp.Members {
members = append(members, m.ID)
}
return membersRes{
offset: req.offset,
limit: req.limit,
total: mp.PageMetadata.Total,
members: members,
}, nil
}
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc_test
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/auth"
grpcapi "github.com/mainflux/mainflux/auth/api/grpc"
"github.com/mainflux/mainflux/auth/jwt"
"github.com/mainflux/mainflux/auth/mocks"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = 8081
secret = "secret"
email = "test@example.com"
id = "testID"
thingsType = "things"
usersType = "users"
description = "Description"
numOfThings = 5
numOfUsers = 5
)
var svc auth.Service
func newService() auth.Service {
repo := mocks.NewKeyRepository()
groupRepo := mocks.NewGroupRepository()
idProvider := uuid.NewMock()
t := jwt.New(secret)
return auth.New(repo, groupRepo, idProvider, t)
}
func startGRPCServer(svc auth.Service, port int) {
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
server := grpc.NewServer()
mainflux.RegisterAuthServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
go server.Serve(listener)
}
func TestIssue(t *testing.T) {
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
cases := []struct {
desc string
id string
email string
kind uint32
err error
code codes.Code
}{
{
desc: "issue for user with valid token",
id: id,
email: email,
kind: auth.UserKey,
err: nil,
code: codes.OK,
},
{
desc: "issue recovery key",
id: id,
email: email,
kind: auth.RecoveryKey,
err: nil,
code: codes.OK,
},
{
desc: "issue API key unauthenticated",
id: id,
email: email,
kind: auth.APIKey,
err: nil,
code: codes.Unauthenticated,
},
{
desc: "issue for invalid key type",
id: id,
email: email,
kind: 32,
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
},
{
desc: "issue for user that exist",
id: "",
kind: auth.APIKey,
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
},
}
for _, tc := range cases {
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Id: tc.id, Email: tc.email, Type: tc.kind})
e, ok := status.FromError(err)
assert.True(t, ok, "gRPC status can't be extracted from the error")
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
}
}
func TestIdentify(t *testing.T) {
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
_, recoverySecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
_, apiSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute), IssuerID: id, Subject: email})
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
cases := []struct {
desc string
token string
idt mainflux.UserIdentity
err error
code codes.Code
}{
{
desc: "identify user with user token",
token: loginSecret,
idt: mainflux.UserIdentity{Email: email, Id: id},
err: nil,
code: codes.OK,
},
{
desc: "identify user with recovery token",
token: recoverySecret,
idt: mainflux.UserIdentity{Email: email, Id: id},
err: nil,
code: codes.OK,
},
{
desc: "identify user with API token",
token: apiSecret,
idt: mainflux.UserIdentity{Email: email, Id: id},
err: nil,
code: codes.OK,
},
{
desc: "identify user with invalid user token",
token: "invalid",
idt: mainflux.UserIdentity{},
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
},
{
desc: "identify user that doesn't exist",
token: "",
idt: mainflux.UserIdentity{},
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
},
}
for _, tc := range cases {
idt, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
if idt != nil {
assert.Equal(t, tc.idt, *idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, *idt))
}
e, ok := status.FromError(err)
assert.True(t, ok, "gRPC status can't be extracted from the error")
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
}
}
func TestMembers(t *testing.T) {
_, token, 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))
group := auth.Group{
Name: "Mainflux",
Description: description,
}
var things []string
for i := 0; i < numOfThings; i++ {
id, err := uuid.New().ID()
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
things = append(things, id)
}
var users []string
for i := 0; i < numOfUsers; i++ {
id, err := uuid.New().ID()
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
users = append(users, id)
}
group, err = svc.CreateGroup(context.Background(), token, group)
assert.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
err = svc.Assign(context.Background(), token, group.ID, thingsType, things...)
assert.Nil(t, err, fmt.Sprintf("Assign members to expected to succeed: %s", err))
err = svc.Assign(context.Background(), token, group.ID, usersType, users...)
assert.Nil(t, err, fmt.Sprintf("Assign members to group expected to succeed: %s", err))
cases := []struct {
desc string
token string
groupID string
groupType string
size int
err error
code codes.Code
}{
{
desc: "get all things with user token",
groupID: group.ID,
token: token,
groupType: thingsType,
size: numOfThings,
err: nil,
code: codes.OK,
},
{
desc: "get all users with user token",
groupID: group.ID,
token: token,
groupType: usersType,
size: numOfUsers,
err: nil,
code: codes.OK,
},
}
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
for _, tc := range cases {
m, err := client.Members(context.Background(), &mainflux.MembersReq{Token: tc.token, GroupID: tc.groupID, Type: tc.groupType, Offset: 0, Limit: 10})
e, ok := status.FromError(err)
assert.Equal(t, tc.size, len(m.Members), fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.size, len(m.Members)))
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
assert.True(t, ok, "OK expected to be true")
}
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"github.com/mainflux/mainflux/auth"
)
type identityReq struct {
token string
kind uint32
}
func (req identityReq) validate() error {
if req.token == "" {
return auth.ErrMalformedEntity
}
if req.kind != auth.UserKey &&
req.kind != auth.APIKey &&
req.kind != auth.RecoveryKey {
return auth.ErrMalformedEntity
}
return nil
}
type issueReq struct {
id string
email string
keyType uint32
}
func (req issueReq) validate() error {
if req.email == "" {
return auth.ErrUnauthorizedAccess
}
if req.keyType != auth.UserKey &&
req.keyType != auth.APIKey &&
req.keyType != auth.RecoveryKey {
return auth.ErrMalformedEntity
}
return nil
}
type assignReq struct {
token string
groupID string
memberID string
groupType string
}
func (req assignReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.groupID == "" || req.memberID == "" {
return auth.ErrMalformedEntity
}
return nil
}
type membersReq struct {
token string
groupID string
offset uint64
limit uint64
memberType string
}
func (req membersReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.groupID == "" {
return auth.ErrMalformedEntity
}
if req.memberType == "" {
return auth.ErrMalformedEntity
}
return nil
}
// authReq represents authorization request. It contains:
// 1. subject - an action invoker
// 2. object - an entity over which action will be executed
// 3. action - type of action that will be executed (read/write)
type authReq struct {
token string
Sub string
Obj string
Act string
}
func (req authReq) validate() error {
if req.token == "" {
return auth.ErrMalformedEntity
}
if req.Sub == "" {
return auth.ErrMalformedEntity
}
if req.Obj == "" {
return auth.ErrMalformedEntity
}
if req.Act == "" {
return auth.ErrMalformedEntity
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
type identityRes struct {
id string
email string
}
type issueRes struct {
value string
}
type authorizeRes struct {
authorized bool
}
type membersRes struct {
total uint64
offset uint64
limit uint64
groupType string
members []string
}
type emptyRes struct {
err error
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
kitot "github.com/go-kit/kit/tracing/opentracing"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/golang/protobuf/ptypes/empty"
mainflux "github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
opentracing "github.com/opentracing/opentracing-go"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ mainflux.AuthServiceServer = (*grpcServer)(nil)
type grpcServer struct {
issue kitgrpc.Handler
identify kitgrpc.Handler
authorize kitgrpc.Handler
assign kitgrpc.Handler
members kitgrpc.Handler
}
// NewServer returns new AuthServiceServer instance.
func NewServer(tracer opentracing.Tracer, svc auth.Service) mainflux.AuthServiceServer {
return &grpcServer{
issue: kitgrpc.NewServer(
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
decodeIssueRequest,
encodeIssueResponse,
),
identify: kitgrpc.NewServer(
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
decodeIdentifyRequest,
encodeIdentifyResponse,
),
authorize: kitgrpc.NewServer(
kitot.TraceServer(tracer, "authorize")(authorizeEndpoint(svc)),
decodeAuthorizeRequest,
encodeAuthorizeResponse,
),
assign: kitgrpc.NewServer(
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
decodeAssignRequest,
encodeEmptyResponse,
),
members: kitgrpc.NewServer(
kitot.TraceServer(tracer, "members")(membersEndpoint(svc)),
decodeMembersRequest,
encodeMembersResponse,
),
}
}
func (s *grpcServer) Issue(ctx context.Context, req *mainflux.IssueReq) (*mainflux.Token, error) {
_, res, err := s.issue.ServeGRPC(ctx, req)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.Token), nil
}
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.UserIdentity, error) {
_, res, err := s.identify.ServeGRPC(ctx, token)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.UserIdentity), nil
}
func (s *grpcServer) Authorize(ctx context.Context, token *mainflux.AuthorizeReq) (*mainflux.AuthorizeRes, error) {
_, res, err := s.authorize.ServeGRPC(ctx, token)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.AuthorizeRes), nil
}
func (s *grpcServer) Assign(ctx context.Context, token *mainflux.Assignment) (*empty.Empty, error) {
_, res, err := s.assign.ServeGRPC(ctx, token)
if err != nil {
return nil, encodeError(err)
}
return res.(*empty.Empty), nil
}
func (s *grpcServer) Members(ctx context.Context, req *mainflux.MembersReq) (*mainflux.MembersRes, error) {
_, res, err := s.members.ServeGRPC(ctx, req)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.MembersRes), nil
}
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.IssueReq)
return issueReq{id: req.GetId(), email: req.GetEmail(), keyType: req.GetType()}, nil
}
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(issueRes)
return &mainflux.Token{Value: res.value}, nil
}
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.Token)
return identityReq{token: req.GetValue()}, nil
}
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(identityRes)
return &mainflux.UserIdentity{Id: res.id, Email: res.email}, nil
}
func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.AuthorizeReq)
return authReq{Act: req.Act, Obj: req.Obj, Sub: req.Sub}, nil
}
func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.AuthorizeRes)
return res, nil
}
func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.Token)
return assignReq{token: req.GetValue()}, nil
}
func decodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.MembersReq)
return membersReq{
token: req.GetToken(),
groupID: req.GetGroupID(),
memberType: req.GetType(),
offset: req.Offset,
limit: req.Limit,
}, nil
}
func encodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(membersRes)
return &mainflux.MembersRes{
Total: res.total,
Offset: res.offset,
Limit: res.limit,
Type: res.groupType,
Members: res.members,
}, nil
}
func encodeEmptyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(emptyRes)
return &empty.Empty{}, encodeError(res.err)
}
func encodeError(err error) error {
switch {
case errors.Contains(err, nil):
return nil
case errors.Contains(err, 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):
return status.Error(codes.Unauthenticated, err.Error())
default:
return status.Error(codes.Internal, "internal server error")
}
}
@@ -1,5 +1,3 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package http contains implementation of users service HTTP API.
package http
+359
View File
@@ -0,0 +1,359 @@
package groups
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/auth"
)
func createGroupEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(createGroupReq)
if err := req.validate(); err != nil {
return groupRes{}, err
}
group := auth.Group{
Name: req.Name,
Description: req.Description,
ParentID: req.ParentID,
Metadata: req.Metadata,
}
group, err := svc.CreateGroup(ctx, req.token, group)
if err != nil {
return groupRes{}, err
}
return groupRes{created: true, id: group.ID}, nil
}
}
func viewGroupEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(groupReq)
if err := req.validate(); err != nil {
return viewGroupRes{}, err
}
group, err := svc.ViewGroup(ctx, req.token, req.id)
if err != nil {
return viewGroupRes{}, err
}
res := viewGroupRes{
ID: group.ID,
Name: group.Name,
Description: group.Description,
Metadata: group.Metadata,
ParentID: group.ParentID,
OwnerID: group.OwnerID,
CreatedAt: group.CreatedAt,
UpdatedAt: group.UpdatedAt,
}
return res, nil
}
}
func updateGroupEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateGroupReq)
if err := req.validate(); err != nil {
return groupRes{}, err
}
group := auth.Group{
ID: req.id,
Name: req.Name,
Description: req.Description,
Metadata: req.Metadata,
}
_, err := svc.UpdateGroup(ctx, req.token, group)
if err != nil {
return groupRes{}, err
}
res := groupRes{created: false}
return res, nil
}
}
func deleteGroupEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(groupReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.RemoveGroup(ctx, req.token, req.id); err != nil {
return nil, err
}
return deleteRes{}, nil
}
}
func listGroupsEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listGroupsReq)
if err := req.validate(); err != nil {
return groupPageRes{}, err
}
pm := auth.PageMetadata{
Level: req.level,
Metadata: req.metadata,
}
page, err := svc.ListGroups(ctx, req.token, pm)
if err != nil {
return groupPageRes{}, err
}
if req.tree {
return buildGroupsResponseTree(page), nil
}
return buildGroupsResponse(page), nil
}
}
func listMemberships(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listMembershipsReq)
if err := req.validate(); err != nil {
return memberPageRes{}, err
}
pm := auth.PageMetadata{
Offset: req.offset,
Limit: req.limit,
Metadata: req.metadata,
}
page, err := svc.ListMemberships(ctx, req.token, req.id, pm)
if err != nil {
return memberPageRes{}, err
}
if req.tree {
return buildGroupsResponseTree(page), nil
}
return buildGroupsResponse(page), nil
}
}
func listChildrenEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listGroupsReq)
if err := req.validate(); err != nil {
return groupPageRes{}, err
}
pm := auth.PageMetadata{
Level: req.level,
Metadata: req.metadata,
}
page, err := svc.ListChildren(ctx, req.token, req.id, pm)
if err != nil {
return groupPageRes{}, err
}
if req.tree {
return buildGroupsResponseTree(page), nil
}
return buildGroupsResponse(page), nil
}
}
func listParentsEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listGroupsReq)
if err := req.validate(); err != nil {
return groupPageRes{}, err
}
pm := auth.PageMetadata{
Level: req.level,
Metadata: req.metadata,
}
page, err := svc.ListParents(ctx, req.token, req.id, pm)
if err != nil {
return groupPageRes{}, err
}
if req.tree {
return buildGroupsResponseTree(page), nil
}
return buildGroupsResponse(page), nil
}
}
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(assignReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.Assign(ctx, req.token, req.groupID, req.Type, req.Members...); err != nil {
return nil, err
}
return assignRes{}, nil
}
}
func unassignEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(unassignReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.Unassign(ctx, req.token, req.groupID, req.Members...); err != nil {
return nil, err
}
return unassignRes{}, nil
}
}
func listMembersEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listMembersReq)
if err := req.validate(); err != nil {
return memberPageRes{}, err
}
pm := auth.PageMetadata{
Offset: req.offset,
Limit: req.limit,
Metadata: req.metadata,
}
page, err := svc.ListMembers(ctx, req.token, req.id, req.groupType, pm)
if err != nil {
return memberPageRes{}, err
}
return buildUsersResponse(page), nil
}
}
func buildGroupsResponseTree(page auth.GroupPage) groupPageRes {
groupsMap := map[string]*auth.Group{}
// Parents' map keeps its array of children.
parentsMap := map[string][]*auth.Group{}
for i := range page.Groups {
if _, ok := groupsMap[page.Groups[i].ID]; !ok {
groupsMap[page.Groups[i].ID] = &page.Groups[i]
parentsMap[page.Groups[i].ID] = make([]*auth.Group, 0)
}
}
for _, group := range groupsMap {
if children, ok := parentsMap[group.ParentID]; ok {
children = append(children, group)
parentsMap[group.ParentID] = children
}
}
res := groupPageRes{
pageRes: pageRes{
Limit: page.Limit,
Offset: page.Offset,
Total: page.Total,
Level: page.Level,
},
Groups: []viewGroupRes{},
}
for _, group := range groupsMap {
if children, ok := parentsMap[group.ID]; ok {
group.Children = children
}
}
for _, group := range groupsMap {
view := toViewGroupRes(*group)
if children, ok := parentsMap[group.ParentID]; len(children) == 0 || !ok {
res.Groups = append(res.Groups, view)
}
}
return res
}
func toViewGroupRes(group auth.Group) viewGroupRes {
view := viewGroupRes{
ID: group.ID,
ParentID: group.ParentID,
OwnerID: group.OwnerID,
Name: group.Name,
Description: group.Description,
Metadata: group.Metadata,
Level: group.Level,
Path: group.Path,
Children: make([]*viewGroupRes, 0),
CreatedAt: group.CreatedAt,
UpdatedAt: group.UpdatedAt,
}
for _, ch := range group.Children {
child := toViewGroupRes(*ch)
view.Children = append(view.Children, &child)
}
return view
}
func buildGroupsResponse(gp auth.GroupPage) groupPageRes {
res := groupPageRes{
pageRes: pageRes{
Total: gp.Total,
Level: gp.Level,
},
Groups: []viewGroupRes{},
}
for _, group := range gp.Groups {
view := viewGroupRes{
ID: group.ID,
ParentID: group.ParentID,
OwnerID: group.OwnerID,
Name: group.Name,
Description: group.Description,
Metadata: group.Metadata,
Level: group.Level,
Path: group.Path,
CreatedAt: group.CreatedAt,
UpdatedAt: group.UpdatedAt,
}
res.Groups = append(res.Groups, view)
}
return res
}
func buildUsersResponse(mp auth.MemberPage) memberPageRes {
res := memberPageRes{
pageRes: pageRes{
Total: mp.Total,
Offset: mp.Offset,
Limit: mp.Limit,
Name: mp.Name,
},
Members: []interface{}{},
}
for _, m := range mp.Members {
res.Members = append(res.Members, m)
}
return res
}
+162
View File
@@ -0,0 +1,162 @@
package groups
import (
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
type createGroupReq struct {
token string
Name string `json:"name,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Description string `json:"description,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (req createGroupReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if len(req.Name) > maxNameSize || req.Name == "" {
return errors.Wrap(auth.ErrMalformedEntity, auth.ErrBadGroupName)
}
return nil
}
type updateGroupReq struct {
token string
id string
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (req updateGroupReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.id == "" {
return auth.ErrMalformedEntity
}
return nil
}
type listGroupsReq struct {
token string
id string
level uint64
// - `true` - result is JSON tree representing groups hierarchy,
// - `false` - result is JSON array of groups.
tree bool
metadata auth.GroupMetadata
}
func (req listGroupsReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.level > auth.MaxLevel || req.level < auth.MinLevel {
return auth.ErrMaxLevelExceeded
}
return nil
}
type listMembersReq struct {
token string
id string
groupType string
offset uint64
limit uint64
tree bool
metadata auth.GroupMetadata
}
func (req listMembersReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.id == "" {
return auth.ErrMalformedEntity
}
return nil
}
type listMembershipsReq struct {
token string
id string
offset uint64
limit uint64
tree bool
metadata auth.GroupMetadata
}
func (req listMembershipsReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.id == "" {
return auth.ErrMalformedEntity
}
return nil
}
type assignReq struct {
token string
groupID string
Type string `json:"type,omitempty"`
Members []string `json:"members"`
}
func (req assignReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.Type == "" || req.groupID == "" || len(req.Members) == 0 {
return auth.ErrMalformedEntity
}
return nil
}
type unassignReq struct {
assignReq
}
func (req unassignReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.groupID == "" || len(req.Members) == 0 {
return auth.ErrMalformedEntity
}
return nil
}
type groupReq struct {
token string
id string
}
func (req groupReq) validate() error {
if req.token == "" {
return auth.ErrUnauthorizedAccess
}
if req.id == "" {
return auth.ErrMalformedEntity
}
return nil
}
+162
View File
@@ -0,0 +1,162 @@
package groups
import (
"fmt"
"net/http"
"time"
"github.com/mainflux/mainflux"
)
var (
_ mainflux.Response = (*memberPageRes)(nil)
_ mainflux.Response = (*groupRes)(nil)
_ mainflux.Response = (*deleteRes)(nil)
_ mainflux.Response = (*assignRes)(nil)
_ mainflux.Response = (*unassignRes)(nil)
)
type memberPageRes struct {
pageRes
Members []interface{}
}
func (res memberPageRes) Code() int {
return http.StatusOK
}
func (res memberPageRes) Headers() map[string]string {
return map[string]string{}
}
func (res memberPageRes) Empty() bool {
return false
}
type viewGroupRes struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerID string `json:"owner_id"`
ParentID string `json:"parent_id,omitempty"`
Description string `json:"description,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
// Indicates a level in tree hierarchy from first group node - root.
Level int `json:"level"`
// Path in a tree consisting of group ids
// parentID1.parentID2.childID1
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
Path string `json:"path"`
Children []*viewGroupRes `json:"children,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (res viewGroupRes) Code() int {
return http.StatusOK
}
func (res viewGroupRes) Headers() map[string]string {
return map[string]string{}
}
func (res viewGroupRes) Empty() bool {
return false
}
type groupRes struct {
id string
created bool
}
func (res groupRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res groupRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/groups/%s", res.id),
}
}
return map[string]string{}
}
func (res groupRes) Empty() bool {
return true
}
type groupPageRes struct {
pageRes
Groups []viewGroupRes `json:"groups"`
}
type pageRes struct {
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
Total uint64 `json:"total"`
Level uint64 `json:"level"`
Name string `json:"name"`
}
func (res groupPageRes) Code() int {
return http.StatusOK
}
func (res groupPageRes) Headers() map[string]string {
return map[string]string{}
}
func (res groupPageRes) Empty() bool {
return false
}
type deleteRes struct{}
func (res deleteRes) Code() int {
return http.StatusNoContent
}
func (res deleteRes) Headers() map[string]string {
return map[string]string{}
}
func (res deleteRes) Empty() bool {
return true
}
type assignRes struct{}
func (res assignRes) Code() int {
return http.StatusOK
}
func (res assignRes) Headers() map[string]string {
return map[string]string{}
}
func (res assignRes) Empty() bool {
return true
}
type unassignRes struct{}
func (res unassignRes) Code() int {
return http.StatusNoContent
}
func (res unassignRes) Headers() map[string]string {
return map[string]string{}
}
func (res unassignRes) Empty() bool {
return true
}
type errorRes struct {
Err string `json:"error"`
}
+346
View File
@@ -0,0 +1,346 @@
package groups
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
kitot "github.com/go-kit/kit/tracing/opentracing"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/internal/httputil"
"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
offsetKey = "offset"
limitKey = "limit"
levelKey = "level"
metadataKey = "metadata"
treeKey = "tree"
groupType = "type"
defOffset = 0
defLimit = 10
defLevel = 1
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
mux.Post("/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "create_group")(createGroupEndpoint(svc)),
decodeGroupCreate,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID", kithttp.NewServer(
kitot.TraceServer(tracer, "view_group")(viewGroupEndpoint(svc)),
decodeGroupRequest,
encodeResponse,
opts...,
))
mux.Put("/groups/:groupID", kithttp.NewServer(
kitot.TraceServer(tracer, "update_group")(updateGroupEndpoint(svc)),
decodeGroupUpdate,
encodeResponse,
opts...,
))
mux.Delete("/groups/:groupID", kithttp.NewServer(
kitot.TraceServer(tracer, "delete_group")(deleteGroupEndpoint(svc)),
decodeGroupRequest,
encodeResponse,
opts...,
))
mux.Get("/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "list_groups")(listGroupsEndpoint(svc)),
decodeListGroupsRequest,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID/children", kithttp.NewServer(
kitot.TraceServer(tracer, "list_children")(listChildrenEndpoint(svc)),
decodeListGroupsRequest,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID/parents", kithttp.NewServer(
kitot.TraceServer(tracer, "list_parents_groups")(listParentsEndpoint(svc)),
decodeListGroupsRequest,
encodeResponse,
opts...,
))
mux.Post("/groups/:groupID/members", kithttp.NewServer(
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
decodeAssignRequest,
encodeResponse,
opts...,
))
mux.Delete("/groups/:groupID/members", kithttp.NewServer(
kitot.TraceServer(tracer, "unassign")(unassignEndpoint(svc)),
decodeUnassignRequest,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID/members", kithttp.NewServer(
kitot.TraceServer(tracer, "list_members")(listMembersEndpoint(svc)),
decodeListMembersRequest,
encodeResponse,
opts...,
))
mux.Get("/members/:memberID/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "list_memberships")(listMemberships(svc)),
decodeListMembershipsRequest,
encodeResponse,
opts...,
))
return mux
}
func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, auth.ErrUnsupportedContentType
}
l, err := httputil.ReadUintQuery(r, levelKey, defLevel)
if err != nil {
return nil, err
}
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
if err != nil {
return nil, err
}
t, err := httputil.ReadBoolQuery(r, treeKey, false)
if err != nil {
return nil, err
}
req := listGroupsReq{
token: r.Header.Get("Authorization"),
level: l,
metadata: m,
tree: t,
id: bone.GetValue(r, "groupID"),
}
return req, nil
}
func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, auth.ErrUnsupportedContentType
}
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
if err != nil {
return nil, err
}
tree, err := httputil.ReadBoolQuery(r, treeKey, false)
if err != nil {
return nil, err
}
t, err := httputil.ReadStringQuery(r, groupType, "")
if err != nil {
return nil, err
}
req := listMembersReq{
token: r.Header.Get("Authorization"),
id: bone.GetValue(r, "groupID"),
groupType: t,
offset: o,
limit: l,
metadata: m,
tree: tree,
}
return req, nil
}
func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, auth.ErrUnsupportedContentType
}
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
if err != nil {
return nil, err
}
tree, err := httputil.ReadBoolQuery(r, treeKey, false)
if err != nil {
return nil, err
}
req := listMembershipsReq{
token: r.Header.Get("Authorization"),
id: bone.GetValue(r, "memberID"),
offset: o,
limit: l,
metadata: m,
tree: tree,
}
return req, nil
}
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, auth.ErrUnsupportedContentType
}
var req createGroupReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.ErrFailedDecode, 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
}
var req updateGroupReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.ErrFailedDecode, 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"),
id: bone.GetValue(r, "groupID"),
}
return req, nil
}
func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := assignReq{
token: r.Header.Get("Authorization"),
groupID: bone.GetValue(r, "groupID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
}
return req, nil
}
func decodeUnassignRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := unassignReq{
assignReq{
token: r.Header.Get("Authorization"),
groupID: bone.GetValue(r, "groupID"),
},
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(auth.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, auth.ErrMalformedEntity):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, auth.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, auth.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, auth.ErrConflict):
w.WriteHeader(http.StatusConflict)
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):
w.WriteHeader(http.StatusUnsupportedMediaType)
default:
w.WriteHeader(http.StatusInternalServerError)
}
errorVal, ok := err.(errors.Error)
if ok {
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
w.Header().Set("Content-Type", contentType)
w.WriteHeader(http.StatusInternalServerError)
}
}
}
@@ -1,17 +1,17 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
package keys
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
)
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(issueKeyReq)
if err := req.validate(); err != nil {
@@ -19,8 +19,7 @@ func issueEndpoint(svc authn.Service) endpoint.Endpoint {
}
now := time.Now().UTC()
newKey := authn.Key{
Issuer: req.issuer,
newKey := auth.Key{
IssuedAt: now,
Type: req.Type,
}
@@ -31,14 +30,14 @@ func issueEndpoint(svc authn.Service) endpoint.Endpoint {
newKey.ExpiresAt = exp
}
key, err := svc.Issue(ctx, req.issuer, newKey)
key, secret, err := svc.Issue(ctx, req.token, newKey)
if err != nil {
return nil, err
}
res := issueKeyRes{
ID: key.ID,
Value: key.Secret,
Value: secret,
IssuedAt: key.IssuedAt,
}
if !key.ExpiresAt.IsZero() {
@@ -48,7 +47,7 @@ func issueEndpoint(svc authn.Service) endpoint.Endpoint {
}
}
func revokeEndpoint(svc authn.Service) endpoint.Endpoint {
func retrieveEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(keyReq)
@@ -56,28 +55,38 @@ func revokeEndpoint(svc authn.Service) endpoint.Endpoint {
return nil, err
}
if err := svc.Revoke(ctx, req.issuer, req.id); err != nil {
key, err := svc.RetrieveKey(ctx, req.token, req.id)
if err != nil {
return nil, err
}
ret := retrieveKeyRes{
ID: key.ID,
IssuerID: key.IssuerID,
Subject: key.Subject,
Type: key.Type,
IssuedAt: key.IssuedAt,
}
if !key.ExpiresAt.IsZero() {
ret.ExpiresAt = &key.ExpiresAt
}
return ret, nil
}
}
func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(keyReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.Revoke(ctx, req.token, req.id); err != nil {
return nil, err
}
return revokeKeyRes{}, nil
}
}
func retrieveEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(keyReq)
if err := req.validate(); err != nil {
return nil, err
}
key, err := svc.Retrieve(ctx, req.issuer, req.id)
if err != nil {
return nil, err
}
return key, nil
}
}
@@ -1,7 +1,7 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http_test
package keys_test
import (
"context"
@@ -14,22 +14,20 @@ import (
"testing"
"time"
authn "github.com/mainflux/mainflux/authn"
httpapi "github.com/mainflux/mainflux/authn/api/http"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/authn/mocks"
"github.com/mainflux/mainflux/auth"
httpapi "github.com/mainflux/mainflux/auth/api/http"
"github.com/mainflux/mainflux/auth/jwt"
"github.com/mainflux/mainflux/auth/mocks"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
)
const (
secret = "secret"
contentType = "application/json"
invalidEmail = "userexample.com"
wrongID = "123e4567-e89b-12d3-a456-000000000042"
id = "123e4567-e89b-12d3-a456-000000000001"
email = "user@example.com"
secret = "secret"
contentType = "application/json"
id = "123e4567-e89b-12d3-a456-000000000001"
email = "user@example.com"
)
type issueRequest struct {
@@ -62,14 +60,15 @@ func (tr testRequest) make() (*http.Response, error) {
return tr.client.Do(req)
}
func newService() authn.Service {
func newService() auth.Service {
repo := mocks.NewKeyRepository()
uuidProvider := uuid.NewMock()
groupRepo := mocks.NewGroupRepository()
idProvider := uuid.NewMock()
t := jwt.New(secret)
return authn.New(repo, uuidProvider, t)
return auth.New(repo, groupRepo, idProvider, t)
}
func newServer(svc authn.Service) *httptest.Server {
func newServer(svc auth.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc, mocktracer.New())
return httptest.NewServer(mux)
}
@@ -81,16 +80,16 @@ func toJSON(data interface{}) string {
func TestIssue(t *testing.T) {
svc := newService()
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
_, 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))
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
uk := issueRequest{Type: authn.UserKey}
ak := issueRequest{Type: authn.APIKey, Duration: time.Hour}
rk := issueRequest{Type: authn.RecoveryKey}
uk := issueRequest{Type: auth.UserKey}
ak := issueRequest{Type: auth.APIKey, Duration: time.Hour}
rk := issueRequest{Type: auth.RecoveryKey}
cases := []struct {
desc string
@@ -110,27 +109,28 @@ func TestIssue(t *testing.T) {
desc: "issue API key",
req: toJSON(ak),
ct: contentType,
token: userKey.Secret,
token: loginSecret,
status: http.StatusCreated,
},
{
desc: "issue recovery key",
req: toJSON(rk),
ct: contentType,
token: userKey.Secret,
token: loginSecret,
status: http.StatusBadRequest,
},
{
desc: "issue user key wrong content type",
req: toJSON(uk),
ct: "", token: userKey.Secret,
desc: "issue user key wrong content type",
req: toJSON(uk),
ct: "",
token: loginSecret,
status: http.StatusUnsupportedMediaType,
},
{
desc: "issue key wrong content type",
desc: "issue recovery key wrong content type",
req: toJSON(rk),
ct: "",
token: userKey.Secret,
token: loginSecret,
status: http.StatusUnsupportedMediaType,
},
{
@@ -187,11 +187,11 @@ func TestIssue(t *testing.T) {
func TestRetrieve(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
k, err := svc.Issue(context.Background(), loginKey.Secret, key)
k, _, err := svc.Issue(context.Background(), loginSecret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
ts := newServer(svc)
@@ -207,13 +207,13 @@ func TestRetrieve(t *testing.T) {
{
desc: "retrieve an existing key",
id: k.ID,
token: loginKey.Secret,
token: loginSecret,
status: http.StatusOK,
},
{
desc: "retrieve a non-existing key",
id: "non-existing",
token: loginKey.Secret,
token: loginSecret,
status: http.StatusNotFound,
},
{
@@ -239,11 +239,11 @@ func TestRetrieve(t *testing.T) {
func TestRevoke(t *testing.T) {
svc := newService()
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
_, 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))
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
k, err := svc.Issue(context.Background(), userKey.Secret, key)
k, _, err := svc.Issue(context.Background(), loginSecret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
ts := newServer(svc)
@@ -259,13 +259,13 @@ func TestRevoke(t *testing.T) {
{
desc: "revoke an existing key",
id: k.ID,
token: userKey.Secret,
token: loginSecret,
status: http.StatusNoContent,
},
{
desc: "revoke a non-existing key",
id: "non-existing",
token: userKey.Secret,
token: loginSecret,
status: http.StatusNoContent,
},
{
@@ -1,39 +1,39 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
package keys
import (
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
)
type issueKeyReq struct {
issuer string
token string
Type uint32 `json:"type,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
}
// It is not possible to issue Reset key using HTTP API.
func (req issueKeyReq) validate() error {
if req.Type == authn.UserKey {
if req.Type == auth.UserKey {
return nil
}
if req.issuer == "" || (req.Type != authn.APIKey) {
return authn.ErrMalformedEntity
if req.token == "" || (req.Type != auth.APIKey) {
return auth.ErrMalformedEntity
}
return nil
}
type keyReq struct {
issuer string
id string
token string
id string
}
func (req keyReq) validate() error {
if req.issuer == "" || req.id == "" {
return authn.ErrMalformedEntity
if req.token == "" || req.id == "" {
return auth.ErrMalformedEntity
}
return nil
}
@@ -1,7 +1,7 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
package keys
import (
"net/http"
@@ -34,6 +34,27 @@ func (res issueKeyRes) Empty() bool {
return res.Value == ""
}
type retrieveKeyRes struct {
ID string `json:"id,omitempty"`
IssuerID string `json:"issuer_id,omitempty"`
Subject string `json:"subject,omitempty"`
Type uint32 `json:"type,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
func (res retrieveKeyRes) Code() int {
return http.StatusOK
}
func (res retrieveKeyRes) Headers() map[string]string {
return map[string]string{}
}
func (res retrieveKeyRes) Empty() bool {
return false
}
type revokeKeyRes struct {
}
@@ -1,7 +1,7 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
package keys
import (
"context"
@@ -14,24 +14,19 @@ import (
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/opentracing/opentracing-go"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const contentType = "application/json"
var errUnsupportedContentType = errors.New("unsupported content type")
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc authn.Service, tracer opentracing.Tracer) http.Handler {
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
mux := bone.New()
mux.Post("/keys", kithttp.NewServer(
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
decodeIssue,
@@ -53,9 +48,6 @@ func MakeHandler(svc authn.Service, tracer opentracing.Tracer) http.Handler {
opts...,
))
mux.GetFunc("/version", mainflux.Version("auth"))
mux.Handle("/metrics", promhttp.Handler())
return mux
}
@@ -64,10 +56,10 @@ func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
return nil, errUnsupportedContentType
}
req := issueKeyReq{
issuer: r.Header.Get("Authorization"),
token: r.Header.Get("Authorization"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(authn.ErrMalformedEntity, err)
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
}
return req, nil
@@ -75,8 +67,8 @@ func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
req := keyReq{
issuer: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
token: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
}
return req, nil
}
@@ -101,13 +93,13 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
switch {
case errors.Contains(err, authn.ErrMalformedEntity):
case errors.Contains(err, auth.ErrMalformedEntity):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, authn.ErrUnauthorizedAccess):
case errors.Contains(err, auth.ErrUnauthorizedAccess):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, authn.ErrNotFound):
case errors.Contains(err, auth.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, authn.ErrConflict):
case errors.Contains(err, auth.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, io.EOF):
w.WriteHeader(http.StatusBadRequest)
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package http
import (
"net/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/auth/api/http/groups"
"github.com/mainflux/mainflux/auth/api/http/keys"
"github.com/opentracing/opentracing-go"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func MakeHandler(svc auth.Service, tracer opentracing.Tracer) http.Handler {
mux := bone.New()
mux = keys.MakeHandler(svc, mux, tracer)
mux = groups.MakeHandler(svc, mux, tracer)
mux.GetFunc("/version", mainflux.Version("auth"))
mux.Handle("/metrics", promhttp.Handler())
return mux
}
+239
View File
@@ -0,0 +1,239 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
package api
import (
"context"
"fmt"
"time"
"github.com/mainflux/mainflux/auth"
log "github.com/mainflux/mainflux/logger"
)
var _ auth.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc auth.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc auth.Service, logger log.Logger) auth.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Issue(ctx context.Context, token string, newKey auth.Key) (key auth.Key, secret string, err error) {
defer func(begin time.Time) {
d := "infinite duration"
if !key.ExpiresAt.IsZero() {
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
}
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Issue(ctx, token, newKey)
}
func (lm *loggingMiddleware) Revoke(ctx context.Context, token, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Revoke(ctx, token, id)
}
func (lm *loggingMiddleware) RetrieveKey(ctx context.Context, token, id string) (key auth.Key, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RetrieveKey(ctx, token, id)
}
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id auth.Identity, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Identify(ctx, key)
}
func (lm *loggingMiddleware) Authorize(ctx context.Context, token, sub, obj, act string) (auth bool, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method authorize took %s to complete", time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Authorize(ctx, token, sub, obj, act)
}
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (g auth.Group, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, group.Name, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.CreateGroup(ctx, token, group)
}
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, group.Name, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateGroup(ctx, token, group)
}
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RemoveGroup(ctx, token, id)
}
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method view_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ViewGroup(ctx, token, id)
}
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_groups for token %s took %s to complete", token, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListGroups(ctx, token, pm)
}
func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_children for token %s and parent %s took %s to complete", token, parentID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListChildren(ctx, token, parentID, pm)
}
func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_parents for token %s and child %s took for child %s to complete", token, childID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListParents(ctx, token, childID, pm)
}
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_members for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListMembers(ctx, token, groupID, groupType, pm)
}
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_memberships for token %s and member id %s took %s to complete", token, memberID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListMemberships(ctx, token, memberID, pm)
}
func (lm *loggingMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
}
func (lm *loggingMiddleware) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Unassign(ctx, token, groupID, memberIDs...)
}
+170
View File
@@ -0,0 +1,170 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/auth"
)
var _ auth.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc auth.Service
}
// MetricsMiddleware instruments core service by tracking request count and latency.
func MetricsMiddleware(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.Key) (auth.Key, string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "issue_key").Add(1)
ms.latency.With("method", "issue_key").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Issue(ctx, token, key)
}
func (ms *metricsMiddleware) Revoke(ctx context.Context, token, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "revoke_key").Add(1)
ms.latency.With("method", "revoke_key").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Revoke(ctx, token, id)
}
func (ms *metricsMiddleware) RetrieveKey(ctx context.Context, token, id string) (auth.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "retrieve_key").Add(1)
ms.latency.With("method", "retrieve_key").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RetrieveKey(ctx, token, id)
}
func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.Identity, error) {
defer func(begin time.Time) {
ms.counter.With("method", "identify").Add(1)
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Identify(ctx, token)
}
func (ms *metricsMiddleware) Authorize(ctx context.Context, token, sub, obj, act string) (auth bool, err 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, token, sub, obj, act)
}
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_group").Add(1)
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CreateGroup(ctx, token, group)
}
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "update_group").Add(1)
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateGroup(ctx, token, group)
}
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
defer func(begin time.Time) {
ms.counter.With("method", "remove_group").Add(1)
ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveGroup(ctx, token, id)
}
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_group").Add(1)
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewGroup(ctx, token, id)
}
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_groups").Add(1)
ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListGroups(ctx, token, pm)
}
func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "parents").Add(1)
ms.latency.With("method", "parents").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListParents(ctx, token, childID, pm)
}
func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_children").Add(1)
ms.latency.With("method", "list_children").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListChildren(ctx, token, parentID, pm)
}
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_members").Add(1)
ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListMembers(ctx, token, groupID, groupType, pm)
}
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_memberships").Add(1)
ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListMemberships(ctx, token, memberID, pm)
}
func (ms *metricsMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
defer func(begin time.Time) {
ms.counter.With("method", "assign").Add(1)
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
}
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID string, memberIDs ...string) (err error) {
defer func(begin time.Time) {
ms.counter.With("method", "unassign").Add(1)
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Unassign(ctx, token, groupID, memberIDs...)
}
+177
View File
@@ -0,0 +1,177 @@
package auth
import (
"context"
"errors"
"time"
)
const MaxLevel = uint64(5)
const 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")
// ErrGroupNotEmpty indicates group is not empty, can't be deleted.
ErrGroupNotEmpty = errors.New("group is not empty")
// ErrMemberAlreadyAssigned indicates that members is already assigned.
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
// ErrSelectEntity indicates error while reading entity from database
ErrSelectEntity = errors.New("select entity from db error")
)
type GroupMetadata map[string]interface{}
type Member struct {
ID string
Type string
}
type Group struct {
ID string
OwnerID string
ParentID string
Name string
Description string
Metadata GroupMetadata
// Indicates a level in tree hierarchy.
// Root node is level 1.
Level int
// Path in a tree consisting of group ids
// parentID1.parentID2.childID1
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
Path string
Children []*Group
CreatedAt time.Time
UpdatedAt time.Time
}
type PageMetadata struct {
Total uint64
Offset uint64
Limit uint64
Size uint64
Level uint64
Name string
Type string
Metadata GroupMetadata
}
type GroupPage struct {
PageMetadata
Groups []Group
}
type MemberPage struct {
PageMetadata
Members []Member
}
type GroupService interface {
// CreateGroup creates new group.
CreateGroup(ctx context.Context, token string, g Group) (Group, error)
// UpdateGroup updates the group identified by the provided ID.
UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
// ViewGroup retrieves data about the group identified by ID.
ViewGroup(ctx context.Context, token, id string) (Group, error)
// ListGroups retrieves groups.
ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error)
// ListChildren retrieves groups that are children to group identified by parentID
ListChildren(ctx context.Context, token, parentID string, pm PageMetadata) (GroupPage, error)
// ListParents retrieves groups that are parent to group identified by childID.
ListParents(ctx context.Context, token, childID string, pm PageMetadata) (GroupPage, error)
// ListMembers retrieves everything that is assigned to a group identified by groupID.
ListMembers(ctx context.Context, token, groupID, groupType string, pm PageMetadata) (MemberPage, error)
// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
ListMemberships(ctx context.Context, token, memberID string, pm PageMetadata) (GroupPage, error)
// RemoveGroup removes the group identified with the provided ID.
RemoveGroup(ctx context.Context, token, id string) error
// Assign adds a member with memberID into the group identified by groupID.
Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) error
// Unassign removes member with memberID from group identified by groupID.
Unassign(ctx context.Context, token, groupID string, memberIDs ...string) error
}
type GroupRepository interface {
// Save group
Save(ctx context.Context, g Group) (Group, error)
// Update a group
Update(ctx context.Context, g Group) (Group, error)
// Delete a group
Delete(ctx context.Context, id string) error
// RetrieveByID retrieves group by its id
RetrieveByID(ctx context.Context, id string) (Group, error)
// RetrieveAll retrieves all groups.
RetrieveAll(ctx context.Context, pm PageMetadata) (GroupPage, error)
// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
RetrieveAllParents(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
RetrieveAllChildren(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
// Retrieves list of groups that member belongs to
Memberships(ctx context.Context, memberID string, pm PageMetadata) (GroupPage, error)
// Members retrieves everything that is assigned to a group identified by groupID.
Members(ctx context.Context, groupID, groupType string, pm PageMetadata) (MemberPage, error)
// Assign adds a member to group.
Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error
// Unassign removes a member from a group
Unassign(ctx context.Context, groupID string, memberIDs ...string) error
}
@@ -8,8 +8,8 @@ import (
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/auth/jwt"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -17,13 +17,13 @@ import (
const secret = "test"
func key() authn.Key {
func key() auth.Key {
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
return authn.Key{
return auth.Key{
ID: "id",
Type: authn.UserKey,
Issuer: "user@email.com",
Secret: "",
Type: auth.UserKey,
Subject: "user@email.com",
IssuerID: "",
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
ExpiresAt: exp,
}
@@ -32,12 +32,9 @@ func key() authn.Key {
func TestIssue(t *testing.T) {
tokenizer := jwt.New(secret)
emptyIssuer := key()
emptyIssuer.Issuer = ""
cases := []struct {
desc string
key authn.Key
key auth.Key
err error
}{
{
@@ -59,10 +56,10 @@ func TestParse(t *testing.T) {
token, err := tokenizer.Issue(key())
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
userKey := key()
userKey.Type = authn.APIKey
userKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
userToken, err := tokenizer.Issue(userKey)
apiKey := key()
apiKey.Type = auth.APIKey
apiKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
apiToken, err := tokenizer.Issue(apiKey)
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
expKey := key()
@@ -72,7 +69,7 @@ func TestParse(t *testing.T) {
cases := []struct {
desc string
key authn.Key
key auth.Key
token string
err error
}{
@@ -84,22 +81,21 @@ func TestParse(t *testing.T) {
},
{
desc: "parse ivalid key",
key: authn.Key{},
key: auth.Key{},
token: "invalid",
err: authn.ErrUnauthorizedAccess,
err: auth.ErrUnauthorizedAccess,
},
{
desc: "parse expired key",
key: authn.Key{},
key: auth.Key{},
token: expToken,
err: authn.ErrKeyExpired,
err: auth.ErrKeyExpired,
},
{
desc: "parse expired user key",
key: userKey,
token: userToken,
err: nil,
desc: "parse expired API key",
key: apiKey,
token: apiToken,
err: auth.ErrAPIKeyExpired,
},
}
@@ -7,18 +7,21 @@ import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
const issuerName = "mainflux.auth"
type claims struct {
jwt.StandardClaims
Type *uint32 `json:"type,omitempty"`
IssuerID string `json:"issuer_id,omitempty"`
Type *uint32 `json:"type,omitempty"`
}
func (c claims) Valid() error {
if c.Type == nil || *c.Type > authn.APIKey {
return authn.ErrMalformedEntity
if c.Type == nil || *c.Type > auth.APIKey || c.Issuer != issuerName {
return auth.ErrMalformedEntity
}
return c.StandardClaims.Valid()
@@ -29,18 +32,19 @@ type tokenizer struct {
}
// New returns new JWT Tokenizer.
func New(secret string) authn.Tokenizer {
func New(secret string) auth.Tokenizer {
return tokenizer{secret: secret}
}
func (svc tokenizer) Issue(key authn.Key) (string, error) {
func (svc tokenizer) Issue(key auth.Key) (string, error) {
claims := claims{
StandardClaims: jwt.StandardClaims{
Issuer: key.Issuer,
Subject: key.Secret,
Issuer: issuerName,
Subject: key.Subject,
IssuedAt: key.IssuedAt.UTC().Unix(),
},
Type: &key.Type,
IssuerID: key.IssuerID,
Type: &key.Type,
}
if !key.ExpiresAt.IsZero() {
@@ -54,11 +58,11 @@ func (svc tokenizer) Issue(key authn.Key) (string, error) {
return token.SignedString([]byte(svc.secret))
}
func (svc tokenizer) Parse(token string) (authn.Key, error) {
func (svc tokenizer) Parse(token string) (auth.Key, error) {
c := claims{}
_, err := jwt.ParseWithClaims(token, &c, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, authn.ErrUnauthorizedAccess
return nil, auth.ErrUnauthorizedAccess
}
return []byte(svc.secret), nil
})
@@ -66,22 +70,22 @@ func (svc tokenizer) Parse(token string) (authn.Key, error) {
if err != nil {
if e, ok := err.(*jwt.ValidationError); ok && e.Errors == jwt.ValidationErrorExpired {
// Expired User key needs to be revoked.
if c.Type != nil && *c.Type == authn.APIKey {
return c.toKey(), nil
if c.Type != nil && *c.Type == auth.APIKey {
return c.toKey(), auth.ErrAPIKeyExpired
}
return authn.Key{}, errors.Wrap(authn.ErrKeyExpired, err)
return auth.Key{}, errors.Wrap(auth.ErrKeyExpired, err)
}
return authn.Key{}, errors.Wrap(authn.ErrUnauthorizedAccess, err)
return auth.Key{}, errors.Wrap(auth.ErrUnauthorizedAccess, err)
}
return c.toKey(), nil
}
func (c claims) toKey() authn.Key {
key := authn.Key{
func (c claims) toKey() auth.Key {
key := auth.Key{
ID: c.Id,
Issuer: c.Issuer,
Secret: c.Subject,
IssuerID: c.IssuerID,
Subject: c.Subject,
IssuedAt: time.Unix(c.IssuedAt, 0).UTC(),
}
if c.ExpiresAt != 0 {
+13 -3
View File
@@ -1,7 +1,7 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
package auth
import (
"context"
@@ -15,6 +15,10 @@ var (
// ErrKeyExpired indicates that the Key is expired.
ErrKeyExpired = errors.New("use of expired key")
// ErrAPIKeyExpired indicates that the Key is expired
// and that the key type is API key.
ErrAPIKeyExpired = errors.New("use of expired API key")
)
const (
@@ -30,12 +34,18 @@ const (
type Key struct {
ID string
Type uint32
Issuer string
Secret string
IssuerID string
Subject string
IssuedAt time.Time
ExpiresAt time.Time
}
// Identity contains ID and Email.
type Identity struct {
ID string
Email string
}
// Expired verifies if the key is expired.
func (k Key) Expired() bool {
if k.Type == APIKey && k.ExpiresAt.IsZero() {
+8 -8
View File
@@ -1,14 +1,14 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn_test
package auth_test
import (
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
"github.com/stretchr/testify/assert"
)
@@ -17,12 +17,12 @@ func TestExpired(t *testing.T) {
exp1 := time.Now()
cases := []struct {
desc string
key authn.Key
key auth.Key
expired bool
}{
{
desc: "not expired key",
key: authn.Key{
key: auth.Key{
IssuedAt: time.Now(),
ExpiresAt: exp,
},
@@ -30,7 +30,7 @@ func TestExpired(t *testing.T) {
},
{
desc: "expired key",
key: authn.Key{
key: auth.Key{
IssuedAt: time.Now().UTC().Add(2 * time.Minute),
ExpiresAt: exp1,
},
@@ -38,16 +38,16 @@ func TestExpired(t *testing.T) {
},
{
desc: "user key with no expiration date",
key: authn.Key{
key: auth.Key{
IssuedAt: time.Now(),
},
expired: true,
},
{
desc: "API key with no expiration date",
key: authn.Key{
key: auth.Key{
IssuedAt: time.Now(),
Type: authn.APIKey,
Type: auth.APIKey,
},
expired: false,
},
+318
View File
@@ -0,0 +1,318 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/mainflux/mainflux/auth"
)
var _ auth.GroupRepository = (*groupRepositoryMock)(nil)
type groupRepositoryMock struct {
mu sync.Mutex
// Map of groups, group id as a key.
// groups map[GroupID]auth.Group
groups map[string]auth.Group
// Map of groups with group id as key that are
// children (i.e. has same parent id) is element
// in children's map where parent id is key.
// children map[ParentID]map[GroupID]auth.Group
children map[string]map[string]auth.Group
// Map of parents' id with child group id as key.
// Each child has one parent.
// parents map[ChildID]ParentID
parents map[string]string
// Map of groups (with group id as key) which
// represent memberships is element in
// memberships' map where member id is a key.
// memberships map[MemberID]map[GroupID]auth.Group
memberships map[string]map[string]auth.Group
// Map of group members where member id is a key
// is an element in the map members where group id is a key.
// members map[type][GroupID]map[MemberID]MemberID
members map[string]map[string]map[string]string
}
// NewGroupRepository creates in-memory user repository
func NewGroupRepository() auth.GroupRepository {
return &groupRepositoryMock{
groups: make(map[string]auth.Group),
children: make(map[string]map[string]auth.Group),
parents: make(map[string]string),
memberships: make(map[string]map[string]auth.Group),
members: make(map[string]map[string]map[string]string),
}
}
func (grm *groupRepositoryMock) Save(ctx context.Context, group auth.Group) (auth.Group, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[group.ID]; ok {
return auth.Group{}, auth.ErrGroupConflict
}
path := group.ID
if group.ParentID != "" {
parent, ok := grm.groups[group.ParentID]
if !ok {
return auth.Group{}, auth.ErrCreateGroup
}
if _, ok := grm.children[group.ParentID]; !ok {
grm.children[group.ParentID] = make(map[string]auth.Group)
}
grm.children[group.ParentID][group.ID] = group
grm.parents[group.ID] = group.ParentID
path = fmt.Sprintf("%s.%s", parent.Path, path)
}
group.Path = path
group.Level = len(strings.Split(path, "."))
grm.groups[group.ID] = group
return group, nil
}
func (grm *groupRepositoryMock) Update(ctx context.Context, group auth.Group) (auth.Group, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
up, ok := grm.groups[group.ID]
if !ok {
return auth.Group{}, auth.ErrNotFound
}
up.Name = group.Name
up.Description = group.Description
up.Metadata = group.Metadata
up.UpdatedAt = time.Now()
grm.groups[group.ID] = up
return up, nil
}
func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[id]; !ok {
return auth.ErrGroupNotFound
}
if len(grm.members[id]) > 0 {
return auth.ErrGroupNotEmpty
}
// This is not quite exact, it should go in depth
for _, ch := range grm.children[id] {
if len(grm.members[ch.ID]) > 0 {
return auth.ErrGroupNotEmpty
}
}
// This is not quite exact, it should go in depth
delete(grm.groups, id)
for _, ch := range grm.children[id] {
delete(grm.members, ch.ID)
}
delete(grm.children, id)
return nil
}
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
val, ok := grm.groups[id]
if !ok {
return auth.Group{}, auth.ErrGroupNotFound
}
return val, nil
}
func (grm *groupRepositoryMock) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
var items []auth.Group
for _, g := range grm.groups {
items = append(items, g)
}
return auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Total: uint64(len(items)),
},
}, nil
}
func (grm *groupRepositoryMock) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[groupID]; !ok {
return auth.ErrGroupNotFound
}
for _, memberID := range memberIDs {
for typ, m := range grm.members[groupID] {
_, ok := m[memberID]
if !ok {
return auth.ErrGroupNotFound
}
delete(grm.members[groupID][typ], memberID)
delete(grm.memberships[memberID], groupID)
}
}
return nil
}
func (grm *groupRepositoryMock) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[groupID]; !ok {
return auth.ErrGroupNotFound
}
if _, ok := grm.members[groupID]; !ok {
grm.members[groupID] = make(map[string]map[string]string)
}
for _, memberID := range memberIDs {
if _, ok := grm.members[groupID][groupType]; !ok {
grm.members[groupID][groupType] = make(map[string]string)
}
if _, ok := grm.memberships[memberID]; !ok {
grm.memberships[memberID] = make(map[string]auth.Group)
}
grm.members[groupID][groupType][memberID] = memberID
grm.memberships[memberID][groupID] = grm.groups[groupID]
}
return nil
}
func (grm *groupRepositoryMock) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
var items []auth.Group
first := uint64(pm.Offset)
last := first + uint64(pm.Limit)
i := uint64(0)
for _, g := range grm.memberships[memberID] {
if i >= first && i < last {
items = append(items, g)
}
i++
}
return auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Limit: pm.Limit,
Offset: pm.Offset,
Total: uint64(len(items)),
},
}, nil
}
func (grm *groupRepositoryMock) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
var items []auth.Member
members, ok := grm.members[groupID][groupType]
if !ok {
return auth.MemberPage{}, auth.ErrGroupNotFound
}
first := uint64(pm.Offset)
last := first + uint64(pm.Limit)
i := uint64(0)
for _, g := range members {
if i >= first && i < last {
items = append(items, auth.Member{ID: g, Type: groupType})
}
i++
}
return auth.MemberPage{
Members: items,
PageMetadata: auth.PageMetadata{
Total: uint64(len(items)),
},
}, nil
}
func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
if groupID == "" {
return auth.GroupPage{}, nil
}
group, ok := grm.groups[groupID]
if !ok {
return auth.GroupPage{}, auth.ErrGroupNotFound
}
groups := make([]auth.Group, 0)
groups, err := grm.getParents(groups, group)
if err != nil {
return auth.GroupPage{}, err
}
return auth.GroupPage{
Groups: groups,
PageMetadata: auth.PageMetadata{
Total: uint64(len(groups)),
},
}, nil
}
func (grm *groupRepositoryMock) getParents(groups []auth.Group, group auth.Group) ([]auth.Group, error) {
groups = append(groups, group)
parentID, ok := grm.parents[group.ID]
if !ok && parentID == "" {
return groups, nil
}
parent, ok := grm.groups[parentID]
if !ok {
panic(fmt.Sprintf("parent with id: %s not found", parentID))
}
return grm.getParents(groups, parent)
}
func (grm *groupRepositoryMock) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
group, ok := grm.groups[groupID]
if !ok {
return auth.GroupPage{}, nil
}
groups := make([]auth.Group, 0)
groups = append(groups, group)
for ch := range grm.parents {
g, ok := grm.groups[ch]
if !ok {
panic(fmt.Sprintf("child with id %s not found", ch))
}
groups = append(groups, g)
}
return auth.GroupPage{
Groups: groups,
PageMetadata: auth.PageMetadata{
Total: uint64(len(groups)),
Offset: pm.Offset,
Limit: pm.Limit,
},
}, nil
}
+12 -12
View File
@@ -7,48 +7,48 @@ import (
"context"
"sync"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
)
var _ authn.KeyRepository = (*keyRepositoryMock)(nil)
var _ auth.KeyRepository = (*keyRepositoryMock)(nil)
type keyRepositoryMock struct {
mu sync.Mutex
keys map[string]authn.Key
keys map[string]auth.Key
}
// NewKeyRepository creates in-memory user repository
func NewKeyRepository() authn.KeyRepository {
func NewKeyRepository() auth.KeyRepository {
return &keyRepositoryMock{
keys: make(map[string]authn.Key),
keys: make(map[string]auth.Key),
}
}
func (krm *keyRepositoryMock) Save(ctx context.Context, key authn.Key) (string, error) {
func (krm *keyRepositoryMock) Save(ctx context.Context, key auth.Key) (string, error) {
krm.mu.Lock()
defer krm.mu.Unlock()
if _, ok := krm.keys[key.ID]; ok {
return "", authn.ErrConflict
return "", auth.ErrConflict
}
krm.keys[key.ID] = key
return key.ID, nil
}
func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, error) {
krm.mu.Lock()
defer krm.mu.Unlock()
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
if key, ok := krm.keys[id]; ok && key.IssuerID == issuerID {
return key, nil
}
return authn.Key{}, authn.ErrNotFound
return auth.Key{}, auth.ErrNotFound
}
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuer, id string) error {
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuerID, id string) error {
krm.mu.Lock()
defer krm.mu.Unlock()
if key, ok := krm.keys[id]; ok && key.Issuer == issuer {
if key, ok := krm.keys[id]; ok && key.IssuerID == issuerID {
delete(krm.keys, id)
}
return nil
+594
View File
@@ -0,0 +1,594 @@
openapi: 3.0.1
info:
title: Mainflux authentication service
description: HTTP API for managing platform API keys.
version: "1.0.0"
paths:
/keys:
post:
summary: Issue API key
description: |
Generates a new API key. Thew new API key will
be uniquely identified by its ID.
tags:
- auth
requestBody:
$ref: "#/components/requestBodies/KeyRequest"
responses:
'201':
description: Issued new key.
'400':
description: Failed due to malformed JSON.
'409':
description: Failed due to using already existing ID.
'415':
description: Missing or invalid content type.
'500':
$ref: "#/components/responses/ServiceError"
/keys/{id}:
get:
summary: Gets API key details.
description: |
Gets API key details for the given key.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ApiKeyId"
responses:
'200':
$ref: "#/components/responses/KeyRes"
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
delete:
summary: Revoke API key
description: |
Revoke API key identified by the given ID.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ApiKeyId"
responses:
'204':
description: Key revoked.
'403':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
/groups:
post:
summary: Creates new group
description: |
Creates new group that can be used for grouping entities - things, users.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/requestBodies/GroupCreateReq"
responses:
'201':
$ref: "#/components/responses/GroupCreateRes"
'400':
description: Failed due to malformed JSON.
'409':
description: Failed due to using an existing email address.
'415':
description: Missing or invalid content type.
'500':
$ref: "#/components/responses/ServiceError"
get:
summary: Gets all groups.
description: |
Gets all groups up to a max level of hierarchy that can be fetched in one
request ( max level = 5). Result can be filtered by metadata. Groups will
be returned as JSON array or JSON tree.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Tree"
responses:
'200':
$ref: "#/components/responses/GroupsPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
'500':
$ref: "#/components/responses/ServiceError"
/groups/{groupId}:
get:
summary: Gets group info.
description: |
Gets info on a group specified by id.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
responses:
'200':
$ref: "#/components/responses/GroupRes"
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
'500':
$ref: "#/components/responses/ServiceError"
put:
summary: Updates group data.
description: |
Updates Name, Description or Metadata of a group.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
requestBody:
$ref: "#/components/requestBodies/GroupUpdateReq"
responses:
'200':
description: Group updated.
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
'500':
$ref: "#/components/responses/ServiceError"
delete:
summary: Deletes group.
description: |
Deletes group. If group is parent and descendant groups do not have any members
child groups will be deleted. Group cannot be deleted if has members or if
any descendant group has members.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Tree"
responses:
'204':
description: Group removed.
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
'500':
$ref: "#/components/responses/ServiceError"
/groups/{groupId}/children:
get:
summary: Gets group children.
description: |
Gets the whole tree of descendants of group for given id including itself.
For performance reason request is limited up to a given level of hierarchy
(max. 5).
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Tree"
responses:
'200':
$ref: "#/components/responses/GroupsPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
'500':
$ref: "#/components/responses/ServiceError"
/groups/{groupId}/parents:
get:
summary: Gets group info.
description: |
Gets a direct line of ancestors for a group specified by id.
Result is up to a specified hierarchy level or up to a root group.
Result can be a JSON array or a JSON tree.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Tree"
responses:
'200':
$ref: "#/components/responses/GroupsPageRes"
'400':
description: Failed due to malformed query parameters.
'403':
description: Missing or invalid access token provided.
'404':
description: Group does not exist.
'500':
$ref: "#/components/responses/ServiceError"
/groups/{groupId}/members:
post:
summary: Assigns members to a group.
description: |
Assigns thing or user id to a group.
tags:
- auth
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/GroupId"
requestBody:
$ref: "#/components/requestBodies/MembersReq"
responses:
'201':
$ref: "#/components/responses/GroupCreateRes"
'400':
description: Failed due to malformed JSON.
'403':
description: Missing or invalid access token provided.
'409':
description: Failed due to using an existing email address.
'415':
description: Missing or invalid content type.
'500':
$ref: "#/components/responses/ServiceError"
get:
summary: Gets members of a group.
description: |
Array of member ids that are in the group specified with groupID.
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/MemberType"
- $ref: "#/components/parameters/GroupId"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Limit"
responses:
'200':
$ref: "#/components/responses/MembersRes"
'403':
description: Missing or invalid access token provided.
'500':
$ref: "#/components/responses/ServiceError"
components:
schemas:
Key:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: API key unique identifier
issuer_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: In ID of the entity that issued the token.
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
subject:
type: string
format: string
example: "test@example.com"
description: User's email or service identifier of API key subject.
issued_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the key is generated.
expires_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the Key expires. If this field is missing,
that means that Key is valid indefinitely.
GroupReqSchema:
type: object
properties:
name:
type: string
description: |
Free-form group name. Group name is unique on the given hierarchy level.
description:
type: string
description: Group description, free form text.
parent_id:
type: string
format: ulid
description: Id of parent group, it must be existing group.
metadata:
type: object
description: Arbitrary, object-encoded group's data.
GroupUpdateSchema:
type: object
properties:
name:
type: string
description: |
Free-form group name. Group name is unique on the given hierarchy level.
description:
type: string
description: Group description, free form text.
metadata:
type: object
description: Arbitrary, object-encoded group's data.
GroupResSchema:
type: object
properties:
id:
type: string
format: ulid
description: Unique group identifier generated by the service.
name:
type: string
description: Free-form group name.
parent_id:
type: string
description: Group ID of parent group.
owner_id:
type: string
format: uuid
description: UUID of user that created the group.
metadata:
type: object
description: Arbitrary, object-encoded group's data.
level:
type: integer
description: Level in hierarchy, distance from the root group.
path:
type: string
description: Hierarchy path, concatenated ids of group ancestors.
children:
type: object
# schema: GroupResSchema
created_at:
type: string
description: Datetime of group creation.
updated_at:
type: string
description: Datetime of last group updated.
required:
- id
- name
- owner_id
- description
- level
- path
- created_at
- updated_at
MembersReqSchema:
type: object
properties:
members:
type: array
minItems: 0
uniqueItems: true
items:
type: string
format: uuid | ulid
type:
type: string
description: Type of entity
GroupsPage:
type: object
properties:
groups:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/GroupResSchema"
total:
type: integer
description: Total number of items.
level:
type: integer
description: Level of hierarchy up to which groups are fetched.
required:
- groups
- total
- level
MembershipPage:
type: object
properties:
groups:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/GroupResSchema"
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
description: Maximum number of items to return in one page.
total:
type: integer
description: Total number of items.
required:
- groups
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.
in: path
schema:
type: string
format: uuid
required: true
GroupId:
name: groupId
description: Group ID.
in: path
schema:
type: string
format: uuid
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.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Level:
name: level
description: Level of hierarchy up to which to retrieve groups from given group id.
in: query
schema:
type: integer
minimum: 1
maximum: 5
required: false
Metadata:
name: metadata
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
in: query
required: false
schema:
type: object
additionalProperties: {}
Tree:
name: tree
description: Specify type of response, JSON array or tree.
in: query
required: false
schema:
type: boolean
default: false
requestBodies:
KeyRequest:
description: JSON-formatted document describing key request.
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
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:
description: JSON-formatted document describing group create request.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GroupReqSchema"
GroupUpdateReq:
description: JSON-formatted document describing group create request.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GroupUpdateSchema"
MembersReq:
description: JSON array of member IDs.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MembersReqSchema"
responses:
ServiceError:
description: Unexpected server-side error occurred.
KeyRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Key"
GroupCreateRes:
description: Group created.
headers:
Location:
content:
text/plain:
schema:
type: string
description: Created group's relative URL.
example: /groups/{groupId}
GroupRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/GroupResSchema"
GroupsPageRes:
description: Group data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/GroupsPage"
MembersRes:
description: Groups data retrieved. Groups assigned to a member.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipPage"
MembershipPageRes:
description: Groups data retrieved. Groups assigned to a member.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipPage"
+743
View File
@@ -0,0 +1,743 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/users"
)
var (
errStringToUUID = errors.New("error converting string")
errGetTotal = errors.New("failed to get total number of groups")
errCreateMetadataQuery = errors.New("failed to create query for metadata")
errTruncation = "string_data_right_truncation"
errFK = "foreign_key_violation"
groupIDFkeyy = "group_relations_group_id_fkey"
)
var _ auth.GroupRepository = (*groupRepository)(nil)
type groupRepository struct {
db Database
}
// NewGroupRepo instantiates a PostgreSQL implementation of group
// repository.
func NewGroupRepo(db Database) auth.GroupRepository {
return &groupRepository{
db: db,
}
}
func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
// For root group path is initialized with id
q := `INSERT INTO groups (name, description, id, path, owner_id, metadata, created_at, updated_at)
VALUES (:name, :description, :id, :id, :owner_id, :metadata, :created_at, :updated_at)
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
if g.ParentID != "" {
// Path is constructed in insert_group_tr - init.go
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at)
VALUES ( :name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at)
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
}
dbg, err := toDBGroup(g)
if err != nil {
return auth.Group{}, err
}
row, err := gr.db.NamedQueryContext(ctx, q, dbg)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
case errFK:
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, err)
case errDuplicate:
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
}
}
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, errors.New(pqErr.Message))
}
defer row.Close()
row.Next()
dbg = dbGroup{}
if err := row.StructScan(&dbg); err != nil {
return auth.Group{}, err
}
return toGroup(dbg)
}
func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
dbu, err := toDBGroup(g)
if err != nil {
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, err)
}
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
case errDuplicate:
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
}
}
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, 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 toGroup(dbu)
}
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
qd := `DELETE FROM groups WHERE id = :id`
group := auth.Group{
ID: groupID,
}
dbg, err := toDBGroup(group)
if err != nil {
return errors.Wrap(auth.ErrUpdateGroup, err)
}
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(auth.ErrMalformedEntity, err)
case errFK:
switch pqErr.Constraint {
case groupIDFkeyy:
return errors.Wrap(auth.ErrGroupNotEmpty, err)
}
return errors.Wrap(auth.ErrGroupConflict, err)
}
}
return errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(auth.ErrDeleteGroup, err)
}
if cnt != 1 {
return errors.Wrap(auth.ErrDeleteGroup, err)
}
return nil
}
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
dbu := dbGroup{
ID: id,
}
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups WHERE id = $1`
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return auth.Group{}, errors.Wrap(auth.ErrGroupNotFound, err)
}
return auth.Group{}, errors.Wrap(auth.ErrSelectEntity, err)
}
return toGroup(dbu)
}
func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
_, metaQuery, err := getGroupsMetadataQuery("groups", pm.Metadata)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
var mq string
if metaQuery != "" {
mq = fmt.Sprintf(" AND %s", metaQuery)
}
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
dbPage, err := toDBGroupPage("", "", pm)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
defer rows.Close()
items, err := gr.processRows(rows)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
cq := "SELECT COUNT(*) FROM groups"
if metaQuery != "" {
cq = fmt.Sprintf(" %s WHERE %s", cq, metaQuery)
}
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
page := auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Total: total,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
FROM groups parent, groups g
WHERE parent.id = :id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level`
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path @> parent.path`
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveParents, err)
}
return gp, nil
}
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
FROM groups parent, groups g
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) < :level`
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path <@ parent.path `
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveChildren, err)
}
return gp, nil
}
func (gr groupRepository) retrieve(ctx context.Context, groupID, retQuery, cntQuery string, pm auth.PageMetadata) (auth.GroupPage, error) {
if groupID == "" {
return auth.GroupPage{}, nil
}
_, mq, err := getGroupsMetadataQuery("g", pm.Metadata)
if err != nil {
return auth.GroupPage{}, err
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
retQuery = fmt.Sprintf(`%s %s`, retQuery, mq)
cntQuery = fmt.Sprintf(`%s %s`, cntQuery, mq)
dbPage, err := toDBGroupPage(groupID, "", pm)
if err != nil {
return auth.GroupPage{}, err
}
rows, err := gr.db.NamedQueryContext(ctx, retQuery, dbPage)
if err != nil {
return auth.GroupPage{}, err
}
defer rows.Close()
items, err := gr.processRows(rows)
if err != nil {
return auth.GroupPage{}, err
}
total, err := total(ctx, gr.db, cntQuery, dbPage)
if err != nil {
return auth.GroupPage{}, err
}
page := auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Level: pm.Level,
Total: total,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
if err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
q := fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
WHERE gr.group_id = :group_id AND gr.type = :type %s`, mq)
if groupType == "" {
q = fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
WHERE gr.group_id = :group_id %s`, mq)
}
params, err := toDBMemberPage("", groupID, groupType, pm)
if err != nil {
return auth.MemberPage{}, err
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
defer rows.Close()
var items []auth.Member
for rows.Next() {
member := dbMember{}
if err := rows.StructScan(&member); err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
if err != nil {
return auth.MemberPage{}, err
}
items = append(items, auth.Member{ID: member.MemberID, Type: member.Type})
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups g, group_relations gr
WHERE gr.group_id = :group_id AND gr.group_id = g.id AND gr.type = :type %s;`, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
page := auth.MemberPage{
Members: items,
PageMetadata: auth.PageMetadata{
Total: total,
Offset: pm.Offset,
Limit: pm.Limit,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.member_id = :member_id
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params, err := toDBMemberPage(memberID, "", "", pm)
if err != nil {
return auth.GroupPage{}, err
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
defer rows.Close()
var items []auth.Group
for rows.Next() {
dbg := dbGroup{}
if err := rows.StructScan(&dbg); err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
gr, err := toGroup(dbg)
if err != nil {
return auth.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.member_id = :member_id %s `, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
page := auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Total: total,
Offset: pm.Offset,
Limit: pm.Limit,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string, ids ...string) error {
tx, err := gr.db.BeginTxx(ctx, nil)
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
VALUES(:group_id, :member_id, :type, :created_at, :updated_at)`
for _, id := range ids {
dbg, err := toDBGroupRelation(id, groupID, groupType)
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
created := time.Now()
dbg.CreatedAt = created
dbg.UpdatedAt = created
if _, err := tx.NamedExecContext(ctx, qIns, dbg); err != nil {
tx.Rollback()
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(auth.ErrMalformedEntity, err)
case errFK:
return errors.Wrap(auth.ErrConflict, errors.New(pqErr.Detail))
case errDuplicate:
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pqErr.Detail))
}
}
return errors.Wrap(auth.ErrAssignToGroup, err)
}
}
if err = tx.Commit(); err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
return nil
}
func (gr groupRepository) Unassign(ctx context.Context, groupID string, ids ...string) error {
tx, err := gr.db.BeginTxx(ctx, nil)
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
qDel := `DELETE from group_relations WHERE group_id = :group_id AND member_id = :member_id`
for _, id := range ids {
dbg, err := toDBGroupRelation(id, groupID, "")
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
if _, err := tx.NamedExecContext(ctx, qDel, dbg); err != nil {
tx.Rollback()
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(auth.ErrMalformedEntity, err)
case errDuplicate:
return errors.Wrap(auth.ErrConflict, err)
}
}
return errors.Wrap(auth.ErrAssignToGroup, err)
}
}
if err = tx.Commit(); err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
return nil
}
type dbMember struct {
MemberID string `db:"member_id"`
GroupID string `db:"group_id"`
Type string `db:"type"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type dbGroup struct {
ID string `db:"id"`
ParentID sql.NullString `db:"parent_id"`
OwnerID uuid.NullUUID `db:"owner_id"`
Name string `db:"name"`
Description string `db:"description"`
Metadata dbMetadata `db:"metadata"`
Level int `db:"level"`
Path string `db:"path"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type dbGroupPage struct {
ID string `db:"id"`
ParentID string `db:"parent_id"`
OwnerID uuid.NullUUID `db:"owner_id"`
Metadata dbMetadata `db:"metadata"`
Path string `db:"path"`
Level uint64 `db:"level"`
Total uint64 `db:"total"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
}
type dbMemberPage struct {
GroupID string `db:"group_id"`
MemberID string `db:"member_id"`
Type string `db:"type"`
Metadata dbMetadata `db:"metadata"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Size uint64
}
func toUUID(id string) (uuid.NullUUID, error) {
var uid uuid.NullUUID
if id == "" {
return uuid.NullUUID{UUID: uuid.Nil, Valid: false}, nil
}
err := uid.Scan(id)
return uid, err
}
func toString(id uuid.NullUUID) (string, error) {
if id.Valid {
return id.UUID.String(), nil
}
if id.UUID == uuid.Nil {
return "", nil
}
return "", errStringToUUID
}
func toDBGroup(g auth.Group) (dbGroup, error) {
ownerID, err := toUUID(g.OwnerID)
if err != nil {
return dbGroup{}, err
}
var parentID sql.NullString
if g.ParentID != "" {
parentID = sql.NullString{String: g.ParentID, Valid: true}
}
meta := dbMetadata(g.Metadata)
return dbGroup{
ID: g.ID,
Name: g.Name,
ParentID: parentID,
OwnerID: ownerID,
Description: g.Description,
Metadata: meta,
Path: g.Path,
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
}, nil
}
func toDBGroupPage(id, path string, pm auth.PageMetadata) (dbGroupPage, error) {
level := auth.MaxLevel
if pm.Level < auth.MaxLevel {
level = pm.Level
}
return dbGroupPage{
Metadata: dbMetadata(pm.Metadata),
ID: id,
Path: path,
Level: level,
Total: pm.Total,
Offset: pm.Offset,
Limit: pm.Limit,
}, nil
}
func toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) {
return dbMemberPage{
GroupID: groupID,
MemberID: memberID,
Type: groupType,
Metadata: dbMetadata(pm.Metadata),
Offset: pm.Offset,
Limit: pm.Limit,
}, nil
}
func toGroup(dbu dbGroup) (auth.Group, error) {
ownerID, err := toString(dbu.OwnerID)
if err != nil {
return auth.Group{}, err
}
return auth.Group{
ID: dbu.ID,
Name: dbu.Name,
ParentID: dbu.ParentID.String,
OwnerID: ownerID,
Description: dbu.Description,
Metadata: auth.GroupMetadata(dbu.Metadata),
Level: dbu.Level,
Path: dbu.Path,
UpdatedAt: dbu.UpdatedAt,
CreatedAt: dbu.CreatedAt,
}, nil
}
type dbGroupRelation struct {
GroupID sql.NullString `db:"group_id"`
MemberID sql.NullString `db:"member_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Type string `db:"type"`
}
func toDBGroupRelation(memberID, groupID, groupType string) (dbGroupRelation, error) {
var grID sql.NullString
if groupID != "" {
grID = sql.NullString{String: groupID, Valid: true}
}
var mID sql.NullString
if memberID != "" {
mID = sql.NullString{String: memberID, Valid: true}
}
return dbGroupRelation{
GroupID: grID,
MemberID: mID,
Type: groupType,
}, nil
}
func getGroupsMetadataQuery(db string, m auth.GroupMetadata) (mb []byte, mq string, err error) {
if len(m) > 0 {
mq = `metadata @> :metadata`
if db != "" {
mq = db + "." + mq
}
b, err := json.Marshal(m)
if err != nil {
return nil, "", errors.Wrap(err, errCreateMetadataQuery)
}
mb = b
}
return mb, mq, nil
}
func (gr groupRepository) processRows(rows *sqlx.Rows) ([]auth.Group, error) {
var items []auth.Group
for rows.Next() {
dbg := dbGroup{}
if err := rows.StructScan(&dbg); err != nil {
return items, err
}
group, err := toGroup(dbg)
if err != nil {
return items, err
}
items = append(items, group)
}
return items, nil
}
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
rows, err := db.NamedQueryContext(ctx, query, params)
if err != nil {
return 0, errors.Wrap(errGetTotal, err)
}
defer rows.Close()
total := uint64(0)
if rows.Next() {
if err := rows.Scan(&total); err != nil {
return 0, errors.Wrap(errGetTotal, err)
}
}
return total, nil
}
// dbMetadata type for handling metadata properly in database/sql
type dbMetadata map[string]interface{}
// Scan - Implement the database/sql scanner interface
func (m *dbMetadata) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return users.ErrScanMetadata
}
if err := json.Unmarshal(b, m); err != nil {
return err
}
return nil
}
// Value Implements valuer
func (m dbMetadata) Value() (driver.Value, error) {
if len(m) == 0 {
return nil, nil
}
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
return b, err
}
+777
View File
@@ -0,0 +1,777 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/auth/postgres"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
maxNameSize = 254
maxDescSize = 1024
groupName = "Mainflux"
description = "description"
)
var (
invalidName = strings.Repeat("m", maxNameSize+1)
invalidDesc = strings.Repeat("m", maxDescSize+1)
metadata = auth.GroupMetadata{
"admin": "true",
}
)
func generateGroupID(t *testing.T) string {
grpID, err := ulidProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
return grpID
}
func TestGroupSave(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
usrID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := ulidProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
grpID := generateGroupID(t)
cases := []struct {
desc string
group auth.Group
err error
}{
{
desc: "create new group",
group: auth.Group{
ID: grpID,
OwnerID: usrID,
Name: groupName,
},
err: nil,
},
{
desc: "create new group with existing name",
group: auth.Group{
ID: grpID,
OwnerID: usrID,
Name: groupName,
},
err: auth.ErrGroupConflict,
},
{
desc: "create group with invalid name",
group: auth.Group{
ID: generateGroupID(t),
OwnerID: usrID,
Name: invalidName,
},
err: auth.ErrMalformedEntity,
},
{
desc: "create group with invalid description",
group: auth.Group{
ID: generateGroupID(t),
OwnerID: usrID,
Name: groupName,
Description: invalidDesc,
},
err: auth.ErrMalformedEntity,
},
{
desc: "create group with parent",
group: auth.Group{
ID: generateGroupID(t),
ParentID: grpID,
OwnerID: usrID,
Name: "withParent",
},
err: nil,
},
{
desc: "create group with parent and existing name",
group: auth.Group{
ID: generateGroupID(t),
ParentID: grpID,
OwnerID: usrID,
Name: groupName,
},
err: nil,
},
{
desc: "create group with wrong parent",
group: auth.Group{
ID: generateGroupID(t),
ParentID: wrongID,
OwnerID: usrID,
Name: "wrongParent",
},
err: auth.ErrCreateGroup,
},
}
for _, tc := range cases {
_, err := groupRepo.Save(context.Background(), tc.group)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestGroupRetrieveByID(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group1 := auth.Group{
ID: generateGroupID(t),
Name: groupName + "TestGroupRetrieveByID1",
OwnerID: uid,
}
_, err = groupRepo.Save(context.Background(), group1)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
retrieved, err := groupRepo.RetrieveByID(context.Background(), group1.ID)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.True(t, retrieved.ID == group1.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group1.ID, retrieved.ID))
// Round to milliseconds as otherwise saving and retriving from DB
// adds rounding error.
creationTime := time.Now().UTC().Round(time.Millisecond)
group2 := auth.Group{
ID: generateGroupID(t),
Name: groupName + "TestGroupRetrieveByID",
OwnerID: uid,
ParentID: group1.ID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
Description: description,
Metadata: metadata,
}
_, err = groupRepo.Save(context.Background(), group2)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
retrieved, err = groupRepo.RetrieveByID(context.Background(), group2.ID)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.True(t, retrieved.ID == group2.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group2.ID, retrieved.ID))
assert.True(t, retrieved.CreatedAt.Equal(creationTime), fmt.Sprintf("Save group, CreatedAt: expected %s got %s\n", creationTime, retrieved.CreatedAt))
assert.True(t, retrieved.UpdatedAt.Equal(creationTime), fmt.Sprintf("Save group, UpdatedAt: expected %s got %s\n", creationTime, retrieved.UpdatedAt))
assert.True(t, retrieved.Level == 2, fmt.Sprintf("Save group, Level: expected %d got %d\n", retrieved.Level, 2))
assert.True(t, retrieved.ParentID == group1.ID, fmt.Sprintf("Save group, Level: expected %s got %s\n", group1.ID, retrieved.ParentID))
assert.True(t, retrieved.Description == description, fmt.Sprintf("Save group, Description: expected %v got %v\n", retrieved.Description, description))
assert.True(t, retrieved.Path == fmt.Sprintf("%s.%s", group1.ID, group2.ID), fmt.Sprintf("Save group, Path: expected %s got %s\n", fmt.Sprintf("%s.%s", group1.ID, group2.ID), retrieved.Path))
retrieved, err = groupRepo.RetrieveByID(context.Background(), generateGroupID(t))
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", auth.ErrGroupNotFound, err))
}
func TestGroupUpdate(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
updateTime := time.Now().UTC()
groupID := generateGroupID(t)
group := auth.Group{
ID: groupID,
Name: groupName + "TestGroupUpdate",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
Description: description,
Metadata: metadata,
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
retrieved, err := groupRepo.RetrieveByID(context.Background(), group.ID)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
cases := []struct {
desc string
groupUpdate auth.Group
groupExpected auth.Group
err error
}{
{
desc: "update group for existing id",
groupUpdate: auth.Group{
ID: groupID,
Name: groupName + "Updated",
UpdatedAt: updateTime,
Metadata: auth.GroupMetadata{"admin": "false"},
},
groupExpected: auth.Group{
Name: groupName + "Updated",
UpdatedAt: updateTime,
Metadata: auth.GroupMetadata{"admin": "false"},
CreatedAt: retrieved.CreatedAt,
Path: retrieved.Path,
ParentID: retrieved.ParentID,
ID: retrieved.ID,
Level: retrieved.Level,
},
err: nil,
},
{
desc: "update group for non-existing id",
groupUpdate: auth.Group{
ID: "wrong",
Name: groupName + "-2",
},
err: auth.ErrUpdateGroup,
},
{
desc: "update group for invalid name",
groupUpdate: auth.Group{
ID: groupID,
Name: invalidName,
},
err: auth.ErrMalformedEntity,
},
{
desc: "update group for invalid description",
groupUpdate: auth.Group{
ID: groupID,
Description: invalidDesc,
},
err: auth.ErrMalformedEntity,
},
}
for _, tc := range cases {
updated, err := groupRepo.Update(context.Background(), tc.groupUpdate)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if tc.desc == "update group for existing id" {
assert.True(t, updated.Level == tc.groupExpected.Level, fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Level, updated.Level))
assert.True(t, updated.Name == tc.groupExpected.Name, fmt.Sprintf("%s:Name: expected %s got %s\n", tc.desc, tc.groupExpected.Name, updated.Name))
assert.True(t, updated.Metadata["admin"] == tc.groupExpected.Metadata["admin"], fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Metadata["admin"], updated.Metadata["admin"]))
}
}
}
func TestGroupDelete(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
groupParent := auth.Group{
ID: generateGroupID(t),
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
groupParent, err = groupRepo.Save(context.Background(), groupParent)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
creationTime = time.Now().UTC()
groupChild1 := auth.Group{
ID: generateGroupID(t),
ParentID: groupParent.ID,
Name: groupName + "child1",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
creationTime = time.Now().UTC()
groupChild2 := auth.Group{
ID: generateGroupID(t),
ParentID: groupParent.ID,
Name: groupName + "child2",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
meta := auth.PageMetadata{
Level: auth.MaxLevel,
}
groupChild1, err = groupRepo.Save(context.Background(), groupChild1)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
groupChild2, err = groupRepo.Save(context.Background(), groupChild2)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
gp, err := groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Retrieve children for parent: expected %v got %v\n", nil, err))
assert.True(t, gp.Total == 3, fmt.Sprintf("Number of children + parent: expected %d got %d\n", 3, gp.Total))
thingID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("thing id create unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), groupChild1.ID, "things", thingID)
require.Nil(t, err, fmt.Sprintf("thing assign got unexpected error: %s", err))
err = groupRepo.Delete(context.Background(), groupChild1.ID)
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete non empty group: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
err = groupRepo.Delete(context.Background(), groupChild2.ID)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete empty group: expected %v got %v\n", nil, err))
err = groupRepo.Delete(context.Background(), groupParent.ID)
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete parent with children with members: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
gp, err = groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("retrieve children after one child removed: expected %v got %v\n", nil, err))
assert.True(t, gp.Total == 2, fmt.Sprintf("number of children + parent: expected %d got %d\n", 2, gp.Total))
err = groupRepo.Unassign(context.Background(), groupChild1.ID, thingID)
require.Nil(t, err, fmt.Sprintf("failed to remove thing from a group error: %s", err))
err = groupRepo.Delete(context.Background(), groupParent.ID)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete parent with children with no members: expected %v got %v\n", nil, err))
_, err = groupRepo.RetrieveByID(context.Background(), groupChild1.ID)
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
}
func TestRetrieveAll(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
metadata := auth.PageMetadata{
Metadata: auth.GroupMetadata{
"field": "value",
},
Level: auth.MaxLevel,
}
wrongMeta := auth.PageMetadata{
Metadata: auth.GroupMetadata{
"wrong": "wrong",
},
Level: auth.MaxLevel,
}
metaNum := uint64(3)
n := uint64(auth.MaxLevel)
parentID := ""
for i := uint64(0); i < n; i++ {
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: fmt.Sprintf("%s-%d", groupName, i),
OwnerID: uid,
ParentID: parentID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
// Create Groups with metadata.
if i < metaNum {
group.Metadata = metadata.Metadata
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = group.ID
}
cases := map[string]struct {
Size uint64
Metadata auth.PageMetadata
}{
"retrieve all groups": {
Metadata: auth.PageMetadata{
Total: n,
Limit: n,
Level: auth.MaxLevel,
},
Size: n,
},
"retrieve groups with existing metadata": {
Metadata: auth.PageMetadata{
Total: metaNum,
Limit: n,
Level: auth.MaxLevel,
Metadata: metadata.Metadata,
},
Size: metaNum,
},
"retrieve groups with non-existing metadata": {
Metadata: auth.PageMetadata{
Total: uint64(0),
Limit: n,
Level: auth.MaxLevel,
Metadata: wrongMeta.Metadata,
},
Size: uint64(0),
},
"retrieve groups with hierarchy level depth": {
Metadata: auth.PageMetadata{
Total: uint64(metaNum),
Limit: n,
Level: auth.MaxLevel,
Metadata: metadata.Metadata,
},
Size: uint64(metaNum),
},
"retrieve groups with hierarchy level depth and existing metadata": {
Metadata: auth.PageMetadata{
Total: uint64(metaNum),
Limit: n,
Level: auth.MaxLevel,
Metadata: metadata.Metadata,
},
Size: uint64(metaNum),
},
}
for desc, tc := range cases {
page, err := groupRepo.RetrieveAll(context.Background(), tc.Metadata)
size := len(page.Groups)
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
assert.Equal(t, tc.Metadata.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Metadata.Total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
func TestRetrieveAllParents(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
metadata := auth.GroupMetadata{
"field": "value",
}
wrongMeta := auth.GroupMetadata{
"wrong": "wrong",
}
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Equal(t, uint64(0), p.Total, fmt.Sprintf("expected total %d got %d\n", 0, p.Total))
metaNum := uint64(3)
n := uint64(10)
parentID := ""
parentMiddle := ""
for i := uint64(0); i < n; i++ {
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: fmt.Sprintf("%s-%d", groupName, i),
OwnerID: uid,
ParentID: parentID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
// Create Groups with metadata.
if n-i <= metaNum {
group.Metadata = metadata
}
if i == n/2 {
parentMiddle = group.ID
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = group.ID
}
cases := map[string]struct {
level uint64
parentID string
Size uint64
Total uint64
Metadata auth.GroupMetadata
}{
"retrieve all parents": {
Total: n,
Size: auth.MaxLevel + 1,
level: auth.MaxLevel,
parentID: parentID,
},
"retrieve groups with existing metadata": {
Total: metaNum,
Size: metaNum,
Metadata: metadata,
parentID: parentID,
level: auth.MaxLevel,
},
"retrieve groups with non-existing metadata": {
Total: uint64(0),
Metadata: wrongMeta,
Size: uint64(0),
level: auth.MaxLevel,
parentID: parentID,
},
"retrieve groups with hierarchy level depth": {
Total: n,
Size: 2 + 1,
level: uint64(2),
parentID: parentID,
},
"retrieve groups with hierarchy level depth and existing metadata": {
Total: metaNum,
Size: metaNum,
level: 3,
Metadata: metadata,
parentID: parentID,
},
"retrieve parent groups from children in the middle": {
Total: n/2 + 1,
Size: n/2 + 1,
level: auth.MaxLevel,
parentID: parentMiddle,
},
}
for desc, tc := range cases {
page, err := groupRepo.RetrieveAllParents(context.Background(), tc.parentID, auth.PageMetadata{Level: tc.level, Metadata: tc.Metadata})
size := len(page.Groups)
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
assert.Equal(t, tc.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
func TestRetrieveAllChildren(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
metadata := auth.GroupMetadata{
"field": "value",
}
wrongMeta := auth.GroupMetadata{
"wrong": "wrong",
}
metaNum := uint64(3)
n := uint64(10)
groupID := generateGroupID(t)
firstParentID := groupID
parentID := ""
parentMiddle := ""
for i := uint64(0); i < n; i++ {
creationTime := time.Now().UTC()
group := auth.Group{
ID: groupID,
Name: fmt.Sprintf("%s-%d", groupName, i),
OwnerID: uid,
ParentID: parentID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
// Create Groups with metadata.
if i < metaNum {
group.Metadata = metadata
}
if i == n/2 {
parentMiddle = group.ID
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = group.ID
groupID = generateGroupID(t)
}
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Equal(t, n, p.Total, fmt.Sprintf("expected total %d got %d\n", n, p.Total))
cases := map[string]struct {
parentID string
size uint64
total uint64
metadata auth.PageMetadata
}{
"retrieve all children": {
size: auth.MaxLevel,
total: n,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
},
parentID: firstParentID,
},
"retrieve groups with existing metadata": {
size: metaNum,
total: metaNum,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
Metadata: metadata,
},
parentID: firstParentID,
},
"retrieve groups with non-existing metadata": {
total: 0,
size: 0,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
Metadata: wrongMeta,
},
parentID: firstParentID,
},
"retrieve groups with hierarchy level depth": {
total: n,
size: 2,
metadata: auth.PageMetadata{
Level: 2,
},
parentID: firstParentID,
},
"retrieve groups with hierarchy level depth and existing metadata": {
total: metaNum,
size: metaNum,
metadata: auth.PageMetadata{
Level: 3,
Metadata: metadata,
},
parentID: firstParentID,
},
"retrieve parent groups from children in the middle": {
total: n / 2,
size: n / 2,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
},
parentID: parentMiddle,
},
}
for desc, tc := range cases {
page, err := groupRepo.RetrieveAllChildren(context.Background(), tc.parentID, tc.metadata)
size := len(page.Groups)
assert.Equal(t, tc.size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
func TestAssign(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
pm := auth.PageMetadata{
Offset: 0,
Limit: 10,
}
group, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
mid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
assert.True(t, errors.Contains(err, auth.ErrMemberAlreadyAssigned), fmt.Sprintf("assign member again: expected %v got %v\n", auth.ErrMemberAlreadyAssigned, err))
}
func TestUnassign(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
pm := auth.PageMetadata{
Offset: 0,
Limit: 10,
}
group, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
mid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
mid, err = idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
assert.True(t, mp.Total == 2, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 2, mp.Total))
err = groupRepo.Unassign(context.Background(), group.ID, mid)
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
mp, err = groupRepo.Members(context.Background(), group.ID, "things", pm)
require.Nil(t, err, fmt.Sprintf("members retrieve unexpected error: %s", err))
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
}
func cleanUp(t *testing.T) {
_, err := db.Exec("delete from group_relations")
require.Nil(t, err, fmt.Sprintf("clean relations unexpected error: %s", err))
_, err = db.Exec("delete from groups")
require.Nil(t, err, fmt.Sprintf("clean groups unexpected error: %s", err))
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"fmt"
"github.com/jmoiron/sqlx"
migrate "github.com/rubenv/sql-migrate"
)
// Config defines the options that are used when connecting to a PostgreSQL instance
type Config struct {
Host string
Port string
User string
Pass string
Name string
SSLMode string
SSLCert string
SSLKey string
SSLRootCert string
}
// Connect creates a connection to the PostgreSQL instance and applies any
// unapplied database migrations. A non-nil error is returned to indicate failure.
func Connect(cfg Config) (*sqlx.DB, error) {
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
db, err := sqlx.Open("postgres", url)
if err != nil {
return nil, err
}
if err := migrateDB(db); err != nil {
return nil, err
}
return db, nil
}
func migrateDB(db *sqlx.DB) error {
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "auth_1",
Up: []string{
`CREATE TABLE IF NOT EXISTS keys (
id VARCHAR(254) NOT NULL,
type SMALLINT,
subject VARCHAR(254) NOT NULL,
issuer_id UUID NOT NULL,
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP,
PRIMARY KEY (id, issuer_id)
)`,
`CREATE EXTENSION IF NOT EXISTS LTREE`,
`CREATE TABLE IF NOT EXISTS groups (
id VARCHAR(254) UNIQUE NOT NULL,
parent_id VARCHAR(254),
owner_id VARCHAR(254),
name VARCHAR(254) NOT NULL,
description VARCHAR(1024),
metadata JSONB,
path LTREE,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
UNIQUE (owner_id, name, parent_id),
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS group_relations (
member_id VARCHAR(254) NOT NULL,
group_id VARCHAR(254) NOT NULL,
type VARCHAR(254),
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
FOREIGN KEY (group_id) REFERENCES groups (id),
PRIMARY KEY (member_id, group_id)
)`,
`CREATE INDEX path_gist_idx ON groups USING GIST (path);`,
`CREATE OR REPLACE FUNCTION inherit_group()
RETURNS trigger
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF NEW.parent_id IS NULL OR NEW.parent_id = '' THEN
RETURN NEW;
END IF;
IF NOT EXISTS (SELECT id FROM groups WHERE id = NEW.parent_id) THEN
RAISE EXCEPTION 'wrong parent id';
END IF;
SELECT text2ltree(ltree2text(path) || '.' || NEW.id) INTO NEW.path FROM groups WHERE id = NEW.parent_id;
RETURN NEW;
END;
$$`,
`CREATE TRIGGER inherit_group_tr
BEFORE INSERT
ON groups
FOR EACH ROW
EXECUTE PROCEDURE inherit_group();`,
},
Down: []string{
`DROP TABLE IF EXISTS keys`,
`DROP EXTENSION IF EXISTS LTREE`,
`DROP TABLE IF EXISTS groups`,
`DROP TABLE IF EXISTS group_relations`,
`DROP FUNCTION IF EXISTS inherit_group`,
`DROP TRIGGER IF EXISTS inherit_group_tr ON groups`,
},
},
},
}
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
return err
}
+25 -22
View File
@@ -6,7 +6,7 @@ import (
"time"
"github.com/lib/pq"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
@@ -15,7 +15,7 @@ var (
errRetrieve = errors.New("failed to retrieve key from database")
errDelete = errors.New("failed to delete key from database")
)
var _ authn.KeyRepository = (*repo)(nil)
var _ auth.KeyRepository = (*repo)(nil)
const (
errDuplicate = "unique_violation"
@@ -27,15 +27,15 @@ type repo struct {
}
// New instantiates a PostgreSQL implementation of key repository.
func New(db Database) authn.KeyRepository {
func New(db Database) auth.KeyRepository {
return &repo{
db: db,
}
}
func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
q := `INSERT INTO keys (id, type, issuer, issued_at, expires_at)
VALUES (:id, :type, :issuer, :issued_at, :expires_at)`
func (kr repo) Save(ctx context.Context, key auth.Key) (string, error) {
q := `INSERT INTO keys (id, type, issuer_id, subject, issued_at, expires_at)
VALUES (:id, :type, :issuer_id, :subject, :issued_at, :expires_at)`
dbKey := toDBKey(key)
if _, err := kr.db.NamedExecContext(ctx, q, dbKey); err != nil {
@@ -43,7 +43,7 @@ func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
pqErr, ok := err.(*pq.Error)
if ok {
if pqErr.Code.Name() == errDuplicate {
return "", errors.Wrap(authn.ErrConflict, pqErr)
return "", errors.Wrap(auth.ErrConflict, pqErr)
}
}
@@ -53,26 +53,26 @@ func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
return dbKey.ID, nil
}
func (kr repo) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
q := `SELECT id, type, issuer, issued_at, expires_at FROM keys WHERE issuer = $1 AND id = $2`
func (kr repo) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, error) {
q := `SELECT id, type, issuer_id, subject, issued_at, expires_at FROM keys WHERE issuer_id = $1 AND id = $2`
key := dbKey{}
if err := kr.db.QueryRowxContext(ctx, q, issuer, id).StructScan(&key); err != nil {
if err := kr.db.QueryRowxContext(ctx, q, issuerID, id).StructScan(&key); err != nil {
pqErr, ok := err.(*pq.Error)
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
return authn.Key{}, errors.Wrap(authn.ErrNotFound, err)
return auth.Key{}, errors.Wrap(auth.ErrNotFound, err)
}
return authn.Key{}, errors.Wrap(errRetrieve, err)
return auth.Key{}, errors.Wrap(errRetrieve, err)
}
return toKey(key), nil
}
func (kr repo) Remove(ctx context.Context, issuer, id string) error {
q := `DELETE FROM keys WHERE issuer = :issuer AND id = :id`
func (kr repo) Remove(ctx context.Context, issuerID, id string) error {
q := `DELETE FROM keys WHERE issuer_id = :issuer_id AND id = :id`
key := dbKey{
ID: id,
Issuer: issuer,
ID: id,
IssuerID: issuerID,
}
if _, err := kr.db.NamedExecContext(ctx, q, key); err != nil {
return errors.Wrap(errDelete, err)
@@ -84,17 +84,19 @@ func (kr repo) Remove(ctx context.Context, issuer, id string) error {
type dbKey struct {
ID string `db:"id"`
Type uint32 `db:"type"`
Issuer string `db:"issuer"`
IssuerID string `db:"issuer_id"`
Subject string `db:"subject"`
Revoked bool `db:"revoked"`
IssuedAt time.Time `db:"issued_at"`
ExpiresAt sql.NullTime `db:"expires_at"`
}
func toDBKey(key authn.Key) dbKey {
func toDBKey(key auth.Key) dbKey {
ret := dbKey{
ID: key.ID,
Type: key.Type,
Issuer: key.Issuer,
IssuerID: key.IssuerID,
Subject: key.Subject,
IssuedAt: key.IssuedAt,
}
if !key.ExpiresAt.IsZero() {
@@ -104,11 +106,12 @@ func toDBKey(key authn.Key) dbKey {
return ret
}
func toKey(key dbKey) authn.Key {
ret := authn.Key{
func toKey(key dbKey) auth.Key {
ret := auth.Key{
ID: key.ID,
Type: key.Type,
Issuer: key.Issuer,
IssuerID: key.IssuerID,
Subject: key.Subject,
IssuedAt: key.IssuedAt,
}
if key.ExpiresAt.Valid {
+160
View File
@@ -0,0 +1,160 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/auth/postgres"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/pkg/ulid"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const email = "user-save@example.com"
var (
expTime = time.Now().Add(5 * time.Minute)
idProvider = uuid.New()
ulidProvider = ulid.New()
)
func TestKeySave(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
id, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
cases := []struct {
desc string
key auth.Key
err error
}{
{
desc: "save a new key",
key: auth.Key{
Subject: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
IssuerID: id,
},
err: nil,
},
{
desc: "save with duplicate id",
key: auth.Key{
Subject: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
IssuerID: id,
},
err: auth.ErrConflict,
},
}
for _, tc := range cases {
_, err := repo.Save(context.Background(), tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestKeyRetrieve(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
id, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
key := auth.Key{
Subject: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
IssuerID: id,
}
_, err = repo.Save(context.Background(), key)
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
cases := []struct {
desc string
id string
owner string
err error
}{
{
desc: "retrieve an existing key",
id: key.ID,
owner: key.IssuerID,
err: nil,
},
{
desc: "retrieve unauthorized",
id: key.ID,
owner: "",
err: auth.ErrNotFound,
},
{
desc: "retrieve unknown key",
id: "",
owner: key.IssuerID,
err: auth.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.Retrieve(context.Background(), tc.owner, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestKeyRemove(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
id, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
key := auth.Key{
Subject: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
IssuerID: id,
}
_, err = repo.Save(opentracing.ContextWithSpan(context.Background(), opentracing.StartSpan("")), key)
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
cases := []struct {
desc string
id string
owner string
err error
}{
{
desc: "remove an existing key",
id: key.ID,
owner: key.IssuerID,
err: nil,
},
{
desc: "remove key that does not exist",
id: key.ID,
owner: key.IssuerID,
err: nil,
},
}
for _, tc := range cases {
err := repo.Remove(context.Background(), tc.owner, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
@@ -13,12 +13,10 @@ import (
"testing"
"github.com/jmoiron/sqlx"
"github.com/mainflux/mainflux/authn/postgres"
"github.com/mainflux/mainflux/auth/postgres"
dockertest "github.com/ory/dockertest/v3"
)
const wrong string = "wrong-value"
var db *sqlx.DB
func TestMain(m *testing.M) {
@@ -32,7 +30,7 @@ func TestMain(m *testing.M) {
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.2-alpine", cfg)
container, err := pool.Run("postgres", "10.8-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
@@ -21,6 +21,9 @@ type database struct {
type Database interface {
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
QueryxContext(context.Context, string, ...interface{}) (*sqlx.Rows, error)
NamedQueryContext(context.Context, string, interface{}) (*sqlx.Rows, error)
BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error)
}
// NewDatabase creates a ThingDatabase instance
@@ -30,6 +33,11 @@ func NewDatabase(db *sqlx.DB) Database {
}
}
func (d database) NamedQueryContext(ctx context.Context, query string, args interface{}) (*sqlx.Rows, error) {
addSpanTags(ctx, query)
return d.db.NamedQueryContext(ctx, query, args)
}
func (d database) NamedExecContext(ctx context.Context, query string, args interface{}) (sql.Result, error) {
addSpanTags(ctx, query)
return d.db.NamedExecContext(ctx, query, args)
@@ -40,6 +48,21 @@ func (d database) QueryRowxContext(ctx context.Context, query string, args ...in
return d.db.QueryRowxContext(ctx, query, args...)
}
func (d database) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
addSpanTags(ctx, query)
return d.db.QueryxContext(ctx, query, args...)
}
func (d database) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error) {
span := opentracing.SpanFromContext(ctx)
if span != nil {
span.SetTag("span.kind", "client")
span.SetTag("peer.service", "postgres")
span.SetTag("db.type", "sql")
}
return d.db.BeginTxx(ctx, opts)
}
func addSpanTags(ctx context.Context, query string) {
span := opentracing.SpanFromContext(ctx)
if span != nil {
+333
View File
@@ -0,0 +1,333 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"context"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/pkg/ulid"
)
const (
loginDuration = 10 * time.Hour
recoveryDuration = 5 * time.Minute
)
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")
// ErrFailedToRetrieveMembership failed to retrieve memberships
ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships")
// ErrFailedToRetrieveAll failed to retrieve groups.
ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups")
// ErrFailedToRetrieveParents failed to retrieve groups.
ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups")
// ErrFailedToRetrieveChildren failed to retrieve groups.
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
errIssueUser = errors.New("failed to issue new user key")
errIssueTmp = errors.New("failed to issue new temporary key")
errRevoke = errors.New("failed to remove key")
errRetrieve = errors.New("failed to retrieve key data")
errIdentify = errors.New("failed to validate token")
)
// Authn specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
// Token is a string value of the actual Key and is used to authenticate
// an Auth service request.
type Authn interface {
// Issue issues a new Key, returning its token value alongside.
Issue(ctx context.Context, token string, key Key) (Key, string, error)
// Revoke removes the Key with the provided id that is
// issued by the user identified by the provided key.
Revoke(ctx context.Context, token, id string) error
// Retrieve retrieves data for the Key identified by the provided
// ID, that is issued by the user identified by the provided key.
RetrieveKey(ctx context.Context, token, id string) (Key, error)
// Identify validates token token. If token is valid, content
// is returned. If token is invalid, or invocation failed for some
// other reason, non-nil error value is returned in response.
Identify(ctx context.Context, token string) (Identity, error)
}
// 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
// implementation, and all of its decorators (e.g. logging & metrics).
// Token is a string value of the actual Key and is used to authenticate
// an Auth service request.
type Service interface {
Authn
Authz
// 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
}
// New instantiates the auth service implementation.
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer) Service {
return &service{
tokenizer: tokenizer,
keys: keys,
groups: groups,
idProvider: idp,
ulidProvider: ulid.New(),
}
}
func (svc service) Issue(ctx context.Context, token string, key Key) (Key, string, error) {
if key.IssuedAt.IsZero() {
return Key{}, "", ErrInvalidKeyIssuedAt
}
switch key.Type {
case APIKey:
return svc.userKey(ctx, token, key)
case RecoveryKey:
return svc.tmpKey(recoveryDuration, key)
default:
return svc.tmpKey(loginDuration, key)
}
}
func (svc service) Revoke(ctx context.Context, token, id string) error {
issuerID, _, err := svc.login(token)
if err != nil {
return errors.Wrap(errRevoke, err)
}
if err := svc.keys.Remove(ctx, issuerID, id); err != nil {
return errors.Wrap(errRevoke, err)
}
return nil
}
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
issuerID, _, err := svc.login(token)
if err != nil {
return Key{}, errors.Wrap(errRetrieve, err)
}
return svc.keys.Retrieve(ctx, issuerID, id)
}
func (svc service) Identify(ctx context.Context, token string) (Identity, error) {
key, err := svc.tokenizer.Parse(token)
if err == ErrAPIKeyExpired {
err = svc.keys.Remove(ctx, key.IssuerID, key.ID)
return Identity{}, errors.Wrap(ErrAPIKeyExpired, err)
}
if err != nil {
return Identity{}, errors.Wrap(errIdentify, err)
}
switch key.Type {
case APIKey, RecoveryKey, UserKey:
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
default:
return Identity{}, ErrUnauthorizedAccess
}
}
func (svc service) Authorize(ctx context.Context, token, sub, obj, act string) (bool, error) {
return true, nil
}
func (svc service) tmpKey(duration time.Duration, key Key) (Key, string, error) {
key.ExpiresAt = key.IssuedAt.Add(duration)
secret, err := svc.tokenizer.Issue(key)
if err != nil {
return Key{}, "", errors.Wrap(errIssueTmp, err)
}
return key, secret, nil
}
func (svc service) userKey(ctx context.Context, token string, key Key) (Key, string, error) {
id, sub, err := svc.login(token)
if err != nil {
return Key{}, "", errors.Wrap(errIssueUser, err)
}
key.IssuerID = id
if key.Subject == "" {
key.Subject = sub
}
keyID, err := svc.idProvider.ID()
if err != nil {
return Key{}, "", errors.Wrap(errIssueUser, err)
}
key.ID = keyID
if _, err := svc.keys.Save(ctx, key); err != nil {
return Key{}, "", errors.Wrap(errIssueUser, err)
}
secret, err := svc.tokenizer.Issue(key)
if err != nil {
return Key{}, "", errors.Wrap(errIssueUser, err)
}
return key, secret, nil
}
func (svc service) login(token string) (string, string, error) {
key, err := svc.tokenizer.Parse(token)
if err != nil {
return "", "", err
}
// Only user key token is valid for login.
if key.Type != UserKey || key.IssuerID == "" {
return "", "", ErrUnauthorizedAccess
}
return key.IssuerID, key.Subject, nil
}
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
user, err := svc.Identify(ctx, token)
if err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
ulid, err := svc.ulidProvider.ID()
if err != nil {
return Group{}, errors.Wrap(ErrGenerateGroupID, err)
}
timestamp := getTimestmap()
group.UpdatedAt = timestamp
group.CreatedAt = timestamp
group.ID = ulid
group.OwnerID = user.ID
group, err = svc.groups.Save(ctx, group)
if err != nil {
return Group{}, err
}
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 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 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 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)
}
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
if err != nil {
return MemberPage{}, errors.Wrap(ErrFailedToRetrieveMembers, err)
}
return mp, nil
}
func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
if _, err := svc.Identify(ctx, token); err != nil {
return errors.Wrap(ErrUnauthorizedAccess, 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)
}
group.UpdatedAt = getTimestmap()
return svc.groups.Update(ctx, group)
}
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
if _, err := svc.Identify(ctx, token); err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, 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 svc.groups.Assign(ctx, groupID, groupType, memberIDs...)
}
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 svc.groups.Unassign(ctx, groupID, memberIDs...)
}
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 svc.groups.Memberships(ctx, memberID, pm)
}
func getTimestmap() time.Time {
return time.Now().UTC().Round(time.Millisecond)
}
+983
View File
@@ -0,0 +1,983 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package auth_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/auth/jwt"
"github.com/mainflux/mainflux/auth/mocks"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var idProvider = uuid.New()
const (
secret = "secret"
email = "test@example.com"
id = "testID"
groupName = "mfx"
description = "Description"
)
func newService() auth.Service {
repo := mocks.NewKeyRepository()
groupRepo := mocks.NewGroupRepository()
idProvider := uuid.NewMock()
t := jwt.New(secret)
return auth.New(repo, groupRepo, idProvider, t)
}
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})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
cases := []struct {
desc string
key auth.Key
token string
err error
}{
{
desc: "issue user key",
key: auth.Key{
Type: auth.UserKey,
IssuedAt: time.Now(),
},
token: secret,
err: nil,
},
{
desc: "issue user key with no time",
key: auth.Key{
Type: auth.UserKey,
},
token: secret,
err: auth.ErrInvalidKeyIssuedAt,
},
{
desc: "issue API key",
key: auth.Key{
Type: auth.APIKey,
IssuedAt: time.Now(),
},
token: secret,
err: nil,
},
{
desc: "issue API key unauthorized",
key: auth.Key{
Type: auth.APIKey,
IssuedAt: time.Now(),
},
token: "invalid",
err: auth.ErrUnauthorizedAccess,
},
{
desc: "issue API key with no time",
key: auth.Key{
Type: auth.APIKey,
},
token: secret,
err: auth.ErrInvalidKeyIssuedAt,
},
{
desc: "issue recovery key",
key: auth.Key{
Type: auth.RecoveryKey,
IssuedAt: time.Now(),
},
token: "",
err: nil,
},
{
desc: "issue recovery with no issue time",
key: auth.Key{
Type: auth.RecoveryKey,
},
token: secret,
err: auth.ErrInvalidKeyIssuedAt,
},
}
for _, tc := range cases {
_, _, err := svc.Issue(context.Background(), tc.token, tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRevoke(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, 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,
}
newKey, _, err := svc.Issue(context.Background(), secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
cases := []struct {
desc string
id string
token string
err error
}{
{
desc: "revoke user key",
id: newKey.ID,
token: secret,
err: nil,
},
{
desc: "revoke non-existing user key",
id: newKey.ID,
token: secret,
err: nil,
},
{
desc: "revoke unauthorized",
id: newKey.ID,
token: "",
err: auth.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.Revoke(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 TestRetrieve(t *testing.T) {
svc := newService()
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, 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",
Type: auth.APIKey,
IssuerID: id,
Subject: email,
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))
apiKey, apiToken, err := svc.Issue(context.Background(), secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user'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))
cases := []struct {
desc string
id string
token string
err error
}{
{
desc: "retrieve user key",
id: apiKey.ID,
token: userToken,
err: nil,
},
{
desc: "retrieve non-existing user key",
id: "invalid",
token: userToken,
err: auth.ErrNotFound,
},
{
desc: "retrieve unauthorized",
id: apiKey.ID,
token: "wrong",
err: auth.ErrUnauthorizedAccess,
},
{
desc: "retrieve with API token",
id: apiKey.ID,
token: apiToken,
err: auth.ErrUnauthorizedAccess,
},
{
desc: "retrieve with reset token",
id: apiKey.ID,
token: resetToken,
err: auth.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
_, err := svc.RetrieveKey(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 TestIdentify(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 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))
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))
_, 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))
cases := []struct {
desc string
key string
idt auth.Identity
err error
}{
{
desc: "identify login key",
key: loginSecret,
idt: auth.Identity{id, email},
err: nil,
},
{
desc: "identify recovery key",
key: recoverySecret,
idt: auth.Identity{id, email},
err: nil,
},
{
desc: "identify API key",
key: apiSecret,
idt: auth.Identity{id, email},
err: nil,
},
{
desc: "identify expired API key",
key: expSecret,
idt: auth.Identity{},
err: auth.ErrAPIKeyExpired,
},
{
desc: "identify expired key",
key: invalidSecret,
idt: auth.Identity{},
err: auth.ErrUnauthorizedAccess,
},
{
desc: "identify invalid key",
key: "invalid",
idt: auth.Identity{},
err: auth.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
idt, err := svc.Identify(context.Background(), tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.idt, idt))
}
}
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})
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))
group := auth.Group{
Name: "Group",
Description: description,
}
parentGroup := auth.Group{
Name: "ParentGroup",
Description: description,
}
parent, err := svc.CreateGroup(context.Background(), apiToken, parentGroup)
assert.Nil(t, err, fmt.Sprintf("Creating parent group expected to succeed: %s", err))
cases := []struct {
desc string
group auth.Group
err error
}{
{
desc: "create new group",
group: group,
err: nil,
},
{
desc: "create group with existing name",
group: group,
err: nil,
},
{
desc: "create group with parent",
group: auth.Group{
Name: groupName,
ParentID: parent.ID,
},
err: nil,
},
{
desc: "create group with invalid parent",
group: auth.Group{
Name: groupName,
ParentID: "xxxxxxxxxx",
},
err: auth.ErrCreateGroup,
},
}
for _, tc := range cases {
_, 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))
}
}
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})
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))
group := auth.Group{
Name: "Group",
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
group, err = svc.CreateGroup(context.Background(), apiToken, group)
assert.Nil(t, err, fmt.Sprintf("Creating parent group failed: %s", err))
cases := []struct {
desc string
group auth.Group
err error
}{
{
desc: "update group",
group: auth.Group{
ID: group.ID,
Name: "NewName",
Description: "NewDescription",
Metadata: auth.GroupMetadata{
"field": "value2",
},
},
err: nil,
},
}
for _, tc := range cases {
g, err := svc.UpdateGroup(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))
assert.Equal(t, g.ID, tc.group.ID, fmt.Sprintf("ID: expected %s got %s\n", g.ID, tc.group.ID))
assert.Equal(t, g.Name, tc.group.Name, fmt.Sprintf("Name: expected %s got %s\n", g.Name, tc.group.Name))
assert.Equal(t, g.Description, tc.group.Description, fmt.Sprintf("Description: expected %s got %s\n", g.Description, tc.group.Description))
assert.Equal(t, g.Metadata["field"], g.Metadata["field"], fmt.Sprintf("Metadata: expected %s got %s\n", g.Metadata, tc.group.Metadata))
}
}
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})
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))
group := auth.Group{
Name: "Group",
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
group, err = svc.CreateGroup(context.Background(), apiToken, group)
assert.Nil(t, err, fmt.Sprintf("Creating parent group failed: %s", err))
cases := []struct {
desc string
token string
groupID string
err error
}{
{
desc: "view group",
token: apiToken,
groupID: group.ID,
err: nil,
},
{
desc: "view group with unauthorized token",
token: "wrongtoken",
groupID: group.ID,
err: auth.ErrUnauthorizedAccess,
},
{
desc: "view group for wrong id",
token: apiToken,
groupID: "wrong",
err: auth.ErrGroupNotFound,
},
}
for _, tc := range cases {
_, err := svc.ViewGroup(context.Background(), tc.token, tc.groupID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
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})
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))
group := auth.Group{
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
n := uint64(10)
parentID := ""
for i := uint64(0); i < n; i++ {
group.Name = fmt.Sprintf("Group%d", i)
group.ParentID = parentID
g, err := svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = g.ID
}
cases := map[string]struct {
token string
level uint64
size uint64
metadata auth.GroupMetadata
err error
}{
"list all groups": {
token: apiToken,
level: 5,
size: n,
err: nil,
},
"list groups for level 1": {
token: apiToken,
level: 1,
size: n,
err: nil,
},
"list all groups with wrong token": {
token: "wrongToken",
level: 5,
size: 0,
err: auth.ErrUnauthorizedAccess,
},
}
for desc, tc := range cases {
page, err := svc.ListGroups(context.Background(), tc.token, auth.PageMetadata{Level: tc.level, Metadata: tc.metadata})
size := uint64(len(page.Groups))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
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})
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))
group := auth.Group{
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
n := uint64(10)
parentID := ""
groupIDs := make([]string, n)
for i := uint64(0); i < n; i++ {
group.Name = fmt.Sprintf("Group%d", i)
group.ParentID = parentID
g, err := svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = g.ID
groupIDs[i] = g.ID
}
cases := map[string]struct {
token string
level uint64
size uint64
id string
metadata auth.GroupMetadata
err error
}{
"list all children": {
token: apiToken,
level: 5,
id: groupIDs[0],
size: n,
err: nil,
},
"list all groups with wrong token": {
token: "wrongToken",
level: 5,
size: 0,
err: auth.ErrUnauthorizedAccess,
},
}
for desc, tc := range cases {
page, err := svc.ListChildren(context.Background(), tc.token, tc.id, auth.PageMetadata{Level: tc.level, Metadata: tc.metadata})
size := uint64(len(page.Groups))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
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})
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))
group := auth.Group{
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
n := uint64(10)
parentID := ""
groupIDs := make([]string, n)
for i := uint64(0); i < n; i++ {
group.Name = fmt.Sprintf("Group%d", i)
group.ParentID = parentID
g, err := svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = g.ID
groupIDs[i] = g.ID
}
cases := map[string]struct {
token string
level uint64
size uint64
id string
metadata auth.GroupMetadata
err error
}{
"list all parents": {
token: apiToken,
level: 5,
id: groupIDs[n-1],
size: n,
err: nil,
},
"list all parents with wrong token": {
token: "wrongToken",
level: 5,
size: 0,
err: auth.ErrUnauthorizedAccess,
},
}
for desc, tc := range cases {
page, err := svc.ListParents(context.Background(), tc.token, tc.id, auth.PageMetadata{Level: tc.level, Metadata: tc.metadata})
size := uint64(len(page.Groups))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
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})
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))
group := auth.Group{
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
g, err := svc.CreateGroup(context.Background(), apiToken, group)
assert.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
group.ID = g.ID
n := uint64(10)
for i := uint64(0); i < n; i++ {
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
err = svc.Assign(context.Background(), apiToken, group.ID, "things", uid)
require.Nil(t, err, fmt.Sprintf("Assign member expected to succeed: %s\n", err))
}
cases := map[string]struct {
token string
size uint64
offset uint64
limit uint64
group auth.Group
metadata auth.GroupMetadata
err error
}{
"list all members": {
token: apiToken,
offset: 0,
limit: n,
group: group,
size: n,
err: nil,
},
"list half members": {
token: apiToken,
offset: n / 2,
limit: n,
group: group,
size: n / 2,
err: nil,
},
"list all members with wrong token": {
token: "wrongToken",
offset: 0,
limit: n,
size: 0,
err: auth.ErrUnauthorizedAccess,
},
}
for desc, tc := range cases {
page, err := svc.ListMembers(context.Background(), tc.token, tc.group.ID, "things", auth.PageMetadata{Offset: tc.offset, Limit: tc.limit, Metadata: tc.metadata})
size := uint64(len(page.Members))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
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})
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))
group := auth.Group{
Description: description,
Metadata: auth.GroupMetadata{
"field": "value",
},
}
memberID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
n := uint64(10)
for i := uint64(0); i < n; i++ {
group.Name = fmt.Sprintf("Group%d", i)
g, err := svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
err = svc.Assign(context.Background(), apiToken, g.ID, "things", memberID)
require.Nil(t, err, fmt.Sprintf("Assign member expected to succeed: %s\n", err))
}
cases := map[string]struct {
token string
size uint64
offset uint64
limit uint64
group auth.Group
metadata auth.GroupMetadata
err error
}{
"list all members": {
token: apiToken,
offset: 0,
limit: n,
group: group,
size: n,
err: nil,
},
"list half members": {
token: apiToken,
offset: n / 2,
limit: n,
group: group,
size: n / 2,
err: nil,
},
"list all members with wrong token": {
token: "wrongToken",
offset: 0,
limit: n,
size: 0,
err: auth.ErrUnauthorizedAccess,
},
}
for desc, tc := range cases {
page, err := svc.ListMemberships(context.Background(), tc.token, memberID, auth.PageMetadata{Limit: tc.limit, Offset: tc.offset, Metadata: tc.metadata})
size := uint64(len(page.Groups))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
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})
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))
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
Name: groupName,
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
group, err = svc.CreateGroup(context.Background(), apiToken, group)
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))
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))
gp, err := svc.ListGroups(context.Background(), apiToken, auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("list groups unexpected error: %s", err))
assert.True(t, gp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, gp.Total))
err = svc.RemoveGroup(context.Background(), apiToken, group.ID)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
gp, err = svc.ListGroups(context.Background(), apiToken, auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("list groups save unexpected error: %s", err))
assert.True(t, gp.Total == 0, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 0, gp.Total))
}
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})
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))
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
group, err = svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
mid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = svc.Assign(context.Background(), apiToken, group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", 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))
}
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})
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))
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
group, err = svc.CreateGroup(context.Background(), apiToken, group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
mid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = svc.Assign(context.Background(), apiToken, group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
mp, err := svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Limit: 10, Offset: 0})
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.Unassign(context.Background(), apiToken, group.ID, mid)
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
mp, err = svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Limit: 10, Offset: 0})
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
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))
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))
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
package auth
// Tokenizer specifies API for encoding and decoding between string and Key.
type Tokenizer interface {
+129
View File
@@ -0,0 +1,129 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package tracing contains middlewares that will add spans to existing traces.
package tracing
import (
"context"
"github.com/mainflux/mainflux/auth"
opentracing "github.com/opentracing/opentracing-go"
)
const (
assign = "assign"
saveGroup = "save_group"
deleteGroup = "delete_group"
updateGroup = "update_group"
retrieveByID = "retrieve_by_id"
retrieveAllParents = "retrieve_all_parents"
retrieveAllChildren = "retrieve_all_children"
retrieveAll = "retrieve_all_groups"
memberships = "memberships"
members = "members"
unassign = "unassign"
)
var _ auth.GroupRepository = (*groupRepositoryMiddleware)(nil)
type groupRepositoryMiddleware struct {
tracer opentracing.Tracer
repo auth.GroupRepository
}
// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
func GroupRepositoryMiddleware(tracer opentracing.Tracer, gr auth.GroupRepository) auth.GroupRepository {
return groupRepositoryMiddleware{
tracer: tracer,
repo: gr,
}
}
func (grm groupRepositoryMiddleware) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
span := createSpan(ctx, grm.tracer, saveGroup)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Save(ctx, g)
}
func (grm groupRepositoryMiddleware) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
span := createSpan(ctx, grm.tracer, updateGroup)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Update(ctx, g)
}
func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string) error {
span := createSpan(ctx, grm.tracer, deleteGroup)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Delete(ctx, groupID)
}
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
span := createSpan(ctx, grm.tracer, retrieveByID)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveByID(ctx, id)
}
func (grm groupRepositoryMiddleware) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
span := createSpan(ctx, grm.tracer, retrieveAllParents)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveAllParents(ctx, groupID, pm)
}
func (grm groupRepositoryMiddleware) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
span := createSpan(ctx, grm.tracer, retrieveAllChildren)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveAllChildren(ctx, groupID, pm)
}
func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
span := createSpan(ctx, grm.tracer, retrieveAll)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveAll(ctx, pm)
}
func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
span := createSpan(ctx, grm.tracer, memberships)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Memberships(ctx, memberID, pm)
}
func (grm groupRepositoryMiddleware) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
span := createSpan(ctx, grm.tracer, members)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Members(ctx, groupID, groupType, pm)
}
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
span := createSpan(ctx, grm.tracer, assign)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Assign(ctx, groupID, groupType, memberIDs...)
}
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
span := createSpan(ctx, grm.tracer, unassign)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Unassign(ctx, groupID, memberIDs...)
}
@@ -8,7 +8,7 @@ package tracing
import (
"context"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/auth"
opentracing "github.com/opentracing/opentracing-go"
)
@@ -18,25 +18,26 @@ const (
revokeOp = "remove"
)
var _ authn.KeyRepository = (*keyRepositoryMiddleware)(nil)
var _ auth.KeyRepository = (*keyRepositoryMiddleware)(nil)
// keyRepositoryMiddleware tracks request and their latency, and adds spans
// to context.
type keyRepositoryMiddleware struct {
tracer opentracing.Tracer
repo authn.KeyRepository
repo auth.KeyRepository
}
// New tracks request and their latency, and adds spans
// to context.
func New(repo authn.KeyRepository, tracer opentracing.Tracer) authn.KeyRepository {
func New(repo auth.KeyRepository, tracer opentracing.Tracer) auth.KeyRepository {
return keyRepositoryMiddleware{
tracer: tracer,
repo: repo,
}
}
func (krm keyRepositoryMiddleware) Save(ctx context.Context, key authn.Key) (string, error) {
func (krm keyRepositoryMiddleware) Save(ctx context.Context, key auth.Key) (string, error) {
span := createSpan(ctx, krm.tracer, saveOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
@@ -44,7 +45,7 @@ func (krm keyRepositoryMiddleware) Save(ctx context.Context, key authn.Key) (str
return krm.repo.Save(ctx, key)
}
func (krm keyRepositoryMiddleware) Retrieve(ctx context.Context, owner, id string) (authn.Key, error) {
func (krm keyRepositoryMiddleware) Retrieve(ctx context.Context, owner, id string) (auth.Key, error) {
span := createSpan(ctx, krm.tracer, retrieveOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
-1666
View File
File diff suppressed because it is too large Load Diff
-49
View File
@@ -1,49 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package mainflux;
import "google/protobuf/empty.proto";
service ThingsService {
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
rpc Identify(Token) returns (ThingID) {}
}
service AuthNService {
rpc Issue(IssueReq) returns (Token) {}
rpc Identify(Token) returns (UserID) {}
}
message AccessByKeyReq {
string token = 1;
string chanID = 2;
}
message ThingID {
string value = 1;
}
message AccessByIDReq {
string thingID = 1;
string chanID = 2;
}
// If a token is not carrying any information itself, the type
// field can be used to determine how to validate the token.
// Also, different tokens can be encoded in different ways.
message Token {
string value = 1;
}
message UserID {
string value = 1;
}
message IssueReq {
string issuer = 1;
uint32 type = 2;
}
-106
View File
@@ -1,106 +0,0 @@
# Authentication service
Authentication service provides an API for managing authentication keys.
There are *three types of authentication keys*:
- user key - keys issued to the user upon login request
- API key - keys issued upon the user request
- recovery key - password recovery key
User keys are issued when user logs in. Each user request (other than `registration` and `login`) contains user key that is used to authenticate the user. API keys are similar to the User keys. The main difference is that API keys have configurable expiration time. If no time is set, the key will never expire. For that reason, API keys are _the only key type that can be revoked_. Recovery key is the password recovery key. It's short-lived token used for password recovery process.
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
The following actions are supported:
- create (all key types)
- verify (all key types)
- obtain (API keys only; secret is never obtained)
- revoke (API keys only)
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|---------------------------|--------------------------------------------------------------------------|---------------|
| MF_AUTHN_LOG_LEVEL | Service level (debug, info, warn, error) | error |
| MF_AUTHN_DB_HOST | Database host address | localhost |
| MF_AUTHN_DB_PORT | Database host port | 5432 |
| MF_AUTHN_DB_USER | Database user | mainflux |
| MF_AUTHN_DB_PASSWORD | Database password | mainflux |
| MF_AUTHN_DB | Name of the database used by the service | auth |
| MF_AUTHN_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| MF_AUTHN_DB_SSL_CERT | Path to the PEM encoded certificate file | |
| MF_AUTHN_DB_SSL_KEY | Path to the PEM encoded key file | |
| MF_AUTHN_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
| MF_AUTHN_HTTP_PORT | Authn service HTTP port | 8180 |
| MF_AUTHN_GRPC_PORT | Authn service gRPC port | 8181 |
| MF_AUTHN_SERVER_CERT | Path to server certificate in pem format | |
| MF_AUTHN_SERVER_KEY | Path to server key in pem format | |
| MF_AUTHN_SECRET | String used for signing tokens | auth |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
services:
authn:
image: mainflux/authn:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_AUTHN_LOG_LEVEL: [Service log level]
MF_AUTHN_DB_HOST: [Database host address]
MF_AUTHN_DB_PORT: [Database host port]
MF_AUTHN_DB_USER: [Database user]
MF_AUTHN_DB_PASS: [Database password]
MF_AUTHN_DB: [Name of the database used by the service]
MF_AUTHN_DB_SSL_MODE: [SSL mode to connect to the database with]
MF_AUTHN_DB_SSL_CERT: [Path to the PEM encoded certificate file]
MF_AUTHN_DB_SSL_KEY: [Path to the PEM encoded key file]
MF_AUTHN_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
MF_AUTHN_HTTP_PORT: [Service HTTP port]
MF_AUTHN_GRPC_PORT: [Service gRPC port]
MF_AUTHN_SECRET: [String used for signing tokens]
MF_AUTHN_SERVER_CERT: [String path to server certificate in pem format]
MF_AUTHN_SERVER_KEY: [String path to server key in pem format]
MF_JAEGER_URL: [Jaeger server URL]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the service
make authn
# copy binary to bin
make install
# set the environment variables and run the service
MF_AUTHN_LOG_LEVEL=[Service log level] MF_AUTHN_DB_HOST=[Database host address] MF_AUTHN_DB_PORT=[Database host port] MF_AUTHN_DB_USER=[Database user] MF_AUTHN_DB_PASS=[Database password] MF_AUTHN_DB=[Name of the database used by the service] MF_AUTHN_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTHN_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTHN_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTHN_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTHN_HTTP_PORT=[Service HTTP port] MF_AUTHN_GRPC_PORT=[Service gRPC port] MF_AUTHN_SECRET=[String used for signing tokens] MF_AUTHN_SERVER_CERT=[Path to server certificate] MF_AUTHN_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-authn
```
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
-93
View File
@@ -1,93 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
kitot "github.com/go-kit/kit/tracing/opentracing"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/mainflux/mainflux"
opentracing "github.com/opentracing/opentracing-go"
"google.golang.org/grpc"
)
var _ mainflux.AuthNServiceClient = (*grpcClient)(nil)
type grpcClient struct {
issue endpoint.Endpoint
identify endpoint.Endpoint
timeout time.Duration
}
// NewClient returns new gRPC client instance.
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthNServiceClient {
return &grpcClient{
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
conn,
"mainflux.AuthNService",
"Issue",
encodeIssueRequest,
decodeIssueResponse,
mainflux.UserID{},
).Endpoint()),
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
conn,
"mainflux.AuthNService",
"Identify",
encodeIdentifyRequest,
decodeIdentifyResponse,
mainflux.UserID{},
).Endpoint()),
timeout: timeout,
}
}
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.issue(ctx, issueReq{issuer: req.GetIssuer(), keyType: req.Type})
if err != nil {
return nil, err
}
ir := res.(identityRes)
return &mainflux.Token{Value: ir.id}, ir.err
}
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(issueReq)
return &mainflux.IssueReq{Issuer: req.issuer, Type: req.keyType}, nil
}
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.UserID)
return identityRes{res.GetValue(), nil}, nil
}
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserID, error) {
ctx, close := context.WithTimeout(ctx, client.timeout)
defer close()
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
if err != nil {
return nil, err
}
ir := res.(identityRes)
return &mainflux.UserID{Value: ir.id}, ir.err
}
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(identityReq)
return &mainflux.Token{Value: req.token}, nil
}
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.UserID)
return identityRes{res.GetValue(), nil}, nil
}
-5
View File
@@ -1,5 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package grpc contains implementation of AuthN service gRPC API.
package grpc
-50
View File
@@ -1,50 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/authn"
)
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(issueReq)
if err := req.validate(); err != nil {
return nil, err
}
now := time.Now().UTC()
key := authn.Key{
Type: req.keyType,
IssuedAt: now,
}
k, err := svc.Issue(ctx, req.issuer, key)
if err != nil {
return identityRes{}, err
}
return identityRes{k.Secret, nil}, nil
}
}
func identifyEndpoint(svc authn.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(identityReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.Identify(ctx, req.token)
if err != nil {
return identityRes{}, err
}
return identityRes{id, nil}, nil
}
}
-167
View File
@@ -1,167 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc_test
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/authn/mocks"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = 8081
secret = "secret"
email = "test@example.com"
)
var svc authn.Service
func newService() authn.Service {
repo := mocks.NewKeyRepository()
uuidProvider := uuid.NewMock()
t := jwt.New(secret)
return authn.New(repo, uuidProvider, t)
}
func startGRPCServer(svc authn.Service, port int) {
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
server := grpc.NewServer()
mainflux.RegisterAuthNServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
go server.Serve(listener)
}
func TestIssue(t *testing.T) {
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
cases := []struct {
desc string
id string
kind uint32
err error
code codes.Code
}{
{
desc: "issue for user with valid token",
id: email,
kind: authn.UserKey,
err: nil,
code: codes.OK,
},
{
desc: "issue recovery key",
id: email,
kind: authn.RecoveryKey,
err: nil,
code: codes.OK,
},
{
desc: "issue API key",
id: userKey.Secret,
kind: authn.APIKey,
err: nil,
code: codes.OK,
},
{
desc: "issue for invalid key type",
id: email,
kind: 32,
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
},
{
desc: "issue for user that exist",
id: "",
kind: authn.APIKey,
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
},
}
for _, tc := range cases {
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Issuer: tc.id, Type: tc.kind})
e, ok := status.FromError(err)
assert.True(t, ok, "gRPC status can't be extracted from the error")
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
}
}
func TestIdentify(t *testing.T) {
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
apiKey, err := svc.Issue(context.Background(), userKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
authAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
cases := []struct {
desc string
token string
id string
err error
code codes.Code
}{
{
desc: "identify user with recovery token",
token: recoveryKey.Secret,
id: email,
err: nil,
code: codes.OK,
},
{
desc: "identify user with API token",
token: apiKey.Secret,
id: email,
err: nil,
code: codes.OK,
},
{
desc: "identify user with invalid user token",
token: "invalid",
id: "",
err: status.Error(codes.Unauthenticated, "unauthorized access"),
code: codes.Unauthenticated,
},
{
desc: "identify user that doesn't exist",
token: "",
id: "",
err: status.Error(codes.InvalidArgument, "received invalid token request"),
code: codes.InvalidArgument,
},
}
for _, tc := range cases {
id, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.id, id.GetValue()))
e, ok := status.FromError(err)
assert.True(t, ok, "gRPC status can't be extracted from the error")
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
}
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import "github.com/mainflux/mainflux/authn"
type identityReq struct {
token string
kind uint32
}
func (req identityReq) validate() error {
if req.token == "" {
return authn.ErrMalformedEntity
}
if req.kind != authn.UserKey &&
req.kind != authn.APIKey &&
req.kind != authn.RecoveryKey {
return authn.ErrMalformedEntity
}
return nil
}
type issueReq struct {
issuer string
keyType uint32
}
func (req issueReq) validate() error {
if req.issuer == "" {
return authn.ErrUnauthorizedAccess
}
if req.keyType != authn.UserKey &&
req.keyType != authn.APIKey &&
req.keyType != authn.RecoveryKey {
return authn.ErrMalformedEntity
}
return nil
}
-91
View File
@@ -1,91 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
kitot "github.com/go-kit/kit/tracing/opentracing"
kitgrpc "github.com/go-kit/kit/transport/grpc"
mainflux "github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/pkg/errors"
opentracing "github.com/opentracing/opentracing-go"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ mainflux.AuthNServiceServer = (*grpcServer)(nil)
type grpcServer struct {
issue kitgrpc.Handler
identify kitgrpc.Handler
}
// NewServer returns new AuthnServiceServer instance.
func NewServer(tracer opentracing.Tracer, svc authn.Service) mainflux.AuthNServiceServer {
return &grpcServer{
issue: kitgrpc.NewServer(
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
decodeIssueRequest,
encodeIssueResponse,
),
identify: kitgrpc.NewServer(
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
decodeIdentifyRequest,
encodeIdentifyResponse,
),
}
}
func (s *grpcServer) Issue(ctx context.Context, req *mainflux.IssueReq) (*mainflux.Token, error) {
_, res, err := s.issue.ServeGRPC(ctx, req)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.Token), nil
}
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.UserID, error) {
_, res, err := s.identify.ServeGRPC(ctx, token)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.UserID), nil
}
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.IssueReq)
return issueReq{issuer: req.GetIssuer(), keyType: req.GetType()}, nil
}
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(identityRes)
return &mainflux.Token{Value: res.id}, encodeError(res.err)
}
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.Token)
return identityReq{token: req.GetValue()}, nil
}
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(identityRes)
return &mainflux.UserID{Value: res.id}, encodeError(res.err)
}
func encodeError(err error) error {
switch {
case errors.Contains(err, nil):
return nil
case errors.Contains(err, authn.ErrMalformedEntity):
return status.Error(codes.InvalidArgument, "received invalid token request")
case errors.Contains(err, authn.ErrUnauthorizedAccess):
return status.Error(codes.Unauthenticated, err.Error())
case errors.Contains(err, authn.ErrKeyExpired):
return status.Error(codes.Unauthenticated, err.Error())
default:
return status.Error(codes.Internal, "internal server error")
}
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// +build !test
package api
import (
"context"
"fmt"
"time"
"github.com/mainflux/mainflux/authn"
log "github.com/mainflux/mainflux/logger"
)
var _ authn.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc authn.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc authn.Service, logger log.Logger) authn.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Issue(ctx context.Context, issuer string, newKey authn.Key) (key authn.Key, err error) {
defer func(begin time.Time) {
d := "infinite duration"
if !key.ExpiresAt.IsZero() {
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
}
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Issue(ctx, issuer, newKey)
}
func (lm *loggingMiddleware) Revoke(ctx context.Context, owner, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Revoke(ctx, owner, id)
}
func (lm *loggingMiddleware) Retrieve(ctx context.Context, owner, id string) (key authn.Key, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Retrieve(ctx, owner, id)
}
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Identify(ctx, key)
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/authn"
)
var _ authn.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc authn.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc authn.Service, counter metrics.Counter, latency metrics.Histogram) authn.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) Issue(ctx context.Context, issuer string, key authn.Key) (authn.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "issue").Add(1)
ms.latency.With("method", "issue").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Issue(ctx, issuer, key)
}
func (ms *metricsMiddleware) Revoke(ctx context.Context, issuer, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "revoke").Add(1)
ms.latency.With("method", "revoke").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Revoke(ctx, issuer, id)
}
func (ms *metricsMiddleware) Retrieve(ctx context.Context, issuer, id string) (authn.Key, error) {
defer func(begin time.Time) {
ms.counter.With("method", "retrieve").Add(1)
ms.latency.With("method", "retrieve").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Retrieve(ctx, issuer, id)
}
func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "identify").Add(1)
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Identify(ctx, key)
}
-147
View File
@@ -1,147 +0,0 @@
openapi: 3.0.1
info:
title: Mainflux authentication service
description: HTTP API for managing platform API keys.
version: "1.0.0"
paths:
/keys:
post:
summary: Issue API key
description: |
Generates a new API key. Thew new API key will
be uniquely identified by its ID.
tags:
- authn
requestBody:
$ref: "#/components/requestBodies/KeyRequest"
responses:
201:
description: Issued new key.
400:
description: Failed due to malformed JSON.
409:
description: Failed due to using already existing ID.
415:
description: Missing or invalid content type.
500:
$ref: "#/components/responses/ServiceError"
/keys/{id}:
get:
summary: Gets API key details.
description: |
Gets API key details for the given key.
tags:
- authn
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ID"
responses:
200:
$ref: "#/components/responses/KeyRes"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/components/responses/ServiceError"
delete:
summary: Revoke API key
description: |
Revoke API key identified by the given ID.
tags:
- authn
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ID"
responses:
204:
description: Key revoked.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/components/responses/ServiceError"
components:
schemas:
Key:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: API key unique identifier
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently
issuer:
type: string
format: string
example: "test@example.com"
description: User's email or service identifier of API key issuer
secret:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
description: API Key value.
issued_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the key is generated
expires_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the Key expires
parameters:
Authorization:
name: Authorization
description: Login key secret (User's access token).
in: header
schema:
type: string
required: true
ID:
name: id
description: API Key id.
in: path
schema:
type: string
required: true
requestBodies:
KeyRequest:
description: JSON-formatted document describing key request.
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently
issuer:
type: string
format: e-mail
example: "test@example.com"
description: User's email or service identifier of API key issuer
duration:
type: number
format: integer
example: 23456
description: Number of seconds issued token is valid for.
responses:
ServiceError:
description: Unexpected server-side error occurred.
KeyRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Key"
-146
View File
@@ -1,146 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/authn/postgres"
"github.com/mainflux/mainflux/pkg/errors"
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
)
func TestKeySave(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
email := "user-save@example.com"
expTime := time.Now().Add(5 * time.Minute)
id, _ := uuidProvider.New().ID()
cases := []struct {
desc string
key authn.Key
err error
}{
{
desc: "save a new key",
key: authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
},
err: nil,
},
{
desc: "save with duplicate id",
key: authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
},
err: authn.ErrConflict,
},
}
for _, tc := range cases {
_, err := repo.Save(context.Background(), tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestKeyRetrieve(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
email := "user-save@example.com"
expTime := time.Now().Add(5 * time.Minute)
id, _ := uuidProvider.New().ID()
key := authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
}
_, err := repo.Save(context.Background(), key)
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "retrieve an existing key",
id: key.ID,
issuer: key.Issuer,
err: nil,
},
{
desc: "retrieve unauthorized",
id: key.ID,
issuer: "",
err: authn.ErrNotFound,
},
{
desc: "retrieve unknown key",
id: "",
issuer: key.Issuer,
err: authn.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.Retrieve(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestKeyRemove(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
email := "user-save@example.com"
expTime := time.Now().Add(5 * time.Minute)
id, _ := uuidProvider.New().ID()
key := authn.Key{
Issuer: email,
IssuedAt: time.Now(),
ExpiresAt: expTime,
ID: id,
}
_, err := repo.Save(opentracing.ContextWithSpan(context.Background(), opentracing.StartSpan("")), key)
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "remove an existing key",
id: key.ID,
issuer: key.Issuer,
err: nil,
},
{
desc: "remove key that does not exist",
id: key.ID,
issuer: key.Issuer,
err: nil,
},
}
for _, tc := range cases {
err := repo.Remove(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
-193
View File
@@ -1,193 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn
import (
"context"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/pkg/errors"
)
const (
loginDuration = 10 * time.Hour
recoveryDuration = 5 * time.Minute
issuerName = "mainflux.authn"
)
var (
// ErrUnauthorizedAccess represents unauthorized access.
ErrUnauthorizedAccess = errors.New("unauthorized access")
// ErrMalformedEntity indicates malformed entity specification (e.g.
// invalid owner or ID).
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrNotFound indicates a non-existing entity request.
ErrNotFound = errors.New("entity not found")
// ErrConflict indicates that entity already exists.
ErrConflict = errors.New("entity already exists")
errIssueUser = errors.New("failed to issue new user key")
errIssueTmp = errors.New("failed to issue new temporary key")
errRevoke = errors.New("failed to remove key")
errRetrieve = errors.New("failed to retrieve key data")
errIdentify = errors.New("failed to validate token")
)
// Service specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// Issue issues a new Key.
Issue(context.Context, string, Key) (Key, error)
// Revoke removes the Key with the provided id that is
// issued by the user identified by the provided key.
Revoke(context.Context, string, string) error
// Retrieve retrieves data for the Key identified by the provided
// ID, that is issued by the user identified by the provided key.
Retrieve(context.Context, string, string) (Key, error)
// Identify validates token token. If token is valid, content
// is returned. If token is invalid, or invocation failed for some
// other reason, non-nil error value is returned in response.
Identify(context.Context, string) (string, error)
}
var _ Service = (*service)(nil)
type service struct {
keys KeyRepository
uuidProvider mainflux.UUIDProvider
tokenizer Tokenizer
}
// New instantiates the auth service implementation.
func New(keys KeyRepository, up mainflux.UUIDProvider, tokenizer Tokenizer) Service {
return &service{
tokenizer: tokenizer,
keys: keys,
uuidProvider: up,
}
}
func (svc service) Issue(ctx context.Context, issuer string, key Key) (Key, error) {
if key.IssuedAt.IsZero() {
return Key{}, ErrInvalidKeyIssuedAt
}
switch key.Type {
case APIKey:
return svc.userKey(ctx, issuer, key)
case RecoveryKey:
return svc.tmpKey(issuer, recoveryDuration, key)
default:
return svc.tmpKey(issuer, loginDuration, key)
}
}
func (svc service) Revoke(ctx context.Context, issuer, id string) error {
email, err := svc.login(issuer)
if err != nil {
return errors.Wrap(errRevoke, err)
}
if err := svc.keys.Remove(ctx, email, id); err != nil {
return errors.Wrap(errRevoke, err)
}
return nil
}
func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error) {
email, err := svc.login(issuer)
if err != nil {
return Key{}, errors.Wrap(errRetrieve, err)
}
return svc.keys.Retrieve(ctx, email, id)
}
func (svc service) Identify(ctx context.Context, token string) (string, error) {
c, err := svc.tokenizer.Parse(token)
if err != nil {
return "", errors.Wrap(errIdentify, err)
}
switch c.Type {
case APIKey:
k, err := svc.keys.Retrieve(ctx, c.Issuer, c.ID)
if err != nil {
return "", err
}
// Auto revoke expired key.
if k.Expired() {
svc.keys.Remove(ctx, c.Issuer, c.ID)
return "", ErrKeyExpired
}
return c.Issuer, nil
case RecoveryKey, UserKey:
if c.Issuer != issuerName {
return "", ErrUnauthorizedAccess
}
return c.Secret, nil
default:
return "", ErrUnauthorizedAccess
}
}
func (svc service) tmpKey(issuer string, duration time.Duration, key Key) (Key, error) {
key.Secret = issuer
key.Issuer = issuerName
key.ExpiresAt = key.IssuedAt.Add(duration)
val, err := svc.tokenizer.Issue(key)
if err != nil {
return Key{}, errors.Wrap(errIssueTmp, err)
}
key.Secret = val
return key, nil
}
func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, error) {
email, err := svc.login(issuer)
if err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
key.Issuer = email
id, err := svc.uuidProvider.ID()
if err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
key.ID = id
value, err := svc.tokenizer.Issue(key)
if err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
key.Secret = value
if _, err := svc.keys.Save(ctx, key); err != nil {
return Key{}, errors.Wrap(errIssueUser, err)
}
return key, nil
}
func (svc service) login(token string) (string, error) {
c, err := svc.tokenizer.Parse(token)
if err != nil {
return "", err
}
// Only user key token is valid for login.
if c.Type != UserKey {
return "", ErrUnauthorizedAccess
}
if c.Secret == "" {
return "", ErrUnauthorizedAccess
}
return c.Secret, nil
}
-280
View File
@@ -1,280 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package authn_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/mainflux/mainflux/authn"
"github.com/mainflux/mainflux/authn/jwt"
"github.com/mainflux/mainflux/authn/mocks"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/stretchr/testify/assert"
)
const (
secret = "secret"
email = "test@example.com"
)
func newService() authn.Service {
repo := mocks.NewKeyRepository()
uuidProvider := uuid.NewMock()
t := jwt.New(secret)
return authn.New(repo, uuidProvider, t)
}
func TestIssue(t *testing.T) {
svc := newService()
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
cases := []struct {
desc string
key authn.Key
issuer string
err error
}{
{
desc: "issue user key",
key: authn.Key{
Type: authn.UserKey,
IssuedAt: time.Now(),
},
issuer: email,
err: nil,
},
{
desc: "issue user key no issue time",
key: authn.Key{
Type: authn.UserKey,
},
issuer: email,
err: authn.ErrInvalidKeyIssuedAt,
},
{
desc: "issue API key",
key: authn.Key{
Type: authn.APIKey,
IssuedAt: time.Now(),
},
issuer: userKey.Secret,
err: nil,
},
{
desc: "issue API key unauthorized",
key: authn.Key{
Type: authn.APIKey,
IssuedAt: time.Now(),
},
issuer: "",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "issue API key no issue time",
key: authn.Key{
Type: authn.APIKey,
},
issuer: userKey.Secret,
err: authn.ErrInvalidKeyIssuedAt,
},
{
desc: "issue recovery key",
key: authn.Key{
Type: authn.RecoveryKey,
IssuedAt: time.Now(),
},
issuer: userKey.Secret,
err: nil,
},
{
desc: "issue recovery key no issue time",
key: authn.Key{
Type: authn.RecoveryKey,
},
issuer: userKey.Secret,
err: authn.ErrInvalidKeyIssuedAt,
},
}
for _, tc := range cases {
_, err := svc.Issue(context.Background(), tc.issuer, tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRevoke(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := authn.Key{
Type: authn.APIKey,
IssuedAt: time.Now(),
}
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "revoke user key",
id: newKey.ID,
issuer: loginKey.Secret,
err: nil,
},
{
desc: "revoke non-existing user key",
id: newKey.ID,
issuer: loginKey.Secret,
err: nil,
},
{
desc: "revoke unauthorized",
id: newKey.ID,
issuer: "",
err: authn.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.Revoke(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieve(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := authn.Key{
ID: "id",
Type: authn.APIKey,
IssuedAt: time.Now(),
}
newKey, err := svc.Issue(context.Background(), loginKey.Secret, key)
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
resetKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
cases := []struct {
desc string
id string
issuer string
err error
}{
{
desc: "retrieve user key",
id: newKey.ID,
issuer: loginKey.Secret,
err: nil,
},
{
desc: "retrieve non-existing user key",
id: "invalid",
issuer: loginKey.Secret,
err: authn.ErrNotFound,
},
{
desc: "retrieve unauthorized",
id: newKey.ID,
issuer: "wrong",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "retrieve with user key",
id: newKey.ID,
issuer: userKey.Secret,
err: authn.ErrUnauthorizedAccess,
},
{
desc: "retrieve with reset key",
id: newKey.ID,
issuer: resetKey.Secret,
err: authn.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
_, err := svc.Retrieve(context.Background(), tc.issuer, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestIdentify(t *testing.T) {
svc := newService()
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
exp1 := time.Now().Add(-2 * time.Second)
expKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
assert.Nil(t, err, fmt.Sprintf("Issuing expired user key expected to succeed: %s", err))
invalidKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: 22, IssuedAt: time.Now()})
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "identify login key",
key: loginKey.Secret,
id: email,
err: nil,
},
{
desc: "identify recovery key",
key: recoveryKey.Secret,
id: email,
err: nil,
},
{
desc: "identify user key",
key: userKey.Secret,
id: email,
err: nil,
},
{
desc: "identify expired user key",
key: expKey.Secret,
id: "",
err: authn.ErrKeyExpired,
},
{
desc: "identify expired key",
key: invalidKey.Secret,
id: "",
err: authn.ErrUnauthorizedAccess,
},
{
desc: "identify invalid key",
key: "invalid",
id: "",
err: authn.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
id, err := svc.Identify(context.Background(), tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.id, id, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.id, id))
}
}
+8 -50
View File
@@ -36,7 +36,7 @@ Thing configuration also contains the so-called `external ID` and `external key`
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
| Variable | Description | Default |
|-------------------------------|-------------------------------------------------------------------------|----------------------- |
|-------------------------------|-------------------------------------------------------------------------|----------------------------------|
| MF_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | error |
| MF_BOOTSTRAP_DB_HOST | Database host address | localhost |
| MF_BOOTSTRAP_DB_PORT | Database host port | 5432 |
@@ -63,55 +63,13 @@ The service is configured using the environment variables presented in the follo
| MF_BOOTSTRAP_ES_DB | Bootstrap service event source database | 0 |
| MF_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
| MF_AUTHN_GRPC_URL | AuthN service gRPC URL | localhost:8181 |
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1s |
| MF_AUTH_GRPC_URL | Auth service gRPC URL | localhost:8181 |
| MF_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
bootstrap:
image: mainflux/bootstrap:latest
container_name: mainflux-bootstrap
depends_on:
- bootstrap-db
restart: on-failure
ports:
- 8200:8200
environment:
MF_BOOTSTRAP_LOG_LEVEL: [Bootstrap log level]
MF_BOOTSTRAP_DB_HOST: [Database host address]
MF_BOOTSTRAP_DB_PORT: [Database host port]
MF_BOOTSTRAP_DB_USER: [Database user]
MF_BOOTSTRAP_DB_PASS: [Database password]
MF_BOOTSTRAP_DB: [Name of the database used by the service]
MF_BOOTSTRAP_DB_SSL_MODE: [SSL mode to connect to the database with]
MF_BOOTSTRAP_DB_SSL_CERT: [Path to the PEM encoded certificate file]
MF_BOOTSTRAP_DB_SSL_KEY: [Path to the PEM encoded key file]
MF_BOOTSTRAP_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
MF_BOOTSTRAP_ENCRYPT_KEY: [Hex-encoded encryption key used for secure bootstrap]
MF_BOOTSTRAP_CLIENT_TLS: [Boolean value to enable/disable client TLS]
MF_BOOTSTRAP_CA_CERTS: [Path to trusted CAs in PEM format]
MF_BOOTSTRAP_PORT: 8200
MF_BOOTSTRAP_SERVER_CERT: [String path to server cert in pem format]
MF_BOOTSTRAP_SERVER_KEY: [String path to server key in pem format]
MF_SDK_BASE_URL: [Base SDK URL for the Mainflux services]
MF_SDK_THINGS_PREFIX: [SDK prefix for Things service]
MF_THINGS_ES_URL: [Things service event source URL]
MF_THINGS_ES_PASS: [Things service event source password]
MF_THINGS_ES_DB: [Things service event source database]
MF_BOOTSTRAP_ES_URL: [Bootstrap service event source URL]
MF_BOOTSTRAP_ES_PASS: [Bootstrap service event source password]
MF_BOOTSTRAP_ES_DB: [Bootstrap service event source database]
MF_BOOTSTRAP_EVENT_CONSUMER: [Bootstrap service event source consumer name]
MF_JAEGER_URL: [Jaeger server URL]
MF_AUTHN_GRPC_URL: [AuthN service gRPC URL]
MF_AUTHN_GRPC_TIMEOUT: [AuthN service gRPC request timeout in seconds]
```
The service itself is distributed as Docker container. Check the [`boostrap`](https://github.com/mainflux/mainflux/blob/master/docker/addons/bootstrap/docker-compose.yml#L32-L56) service section in
docker-compose to see how service is deployed.
To start the service outside of the container, execute the following shell script:
@@ -147,8 +105,8 @@ MF_BOOTSTRAP_SERVER_KEY=[Path to server key] \
MF_SDK_BASE_URL=[Base SDK URL for the Mainflux services] \
MF_SDK_THINGS_PREFIX=[SDK prefix for Things service] \
MF_JAEGER_URL=[Jaeger server URL] \
MF_AUTHN_GRPC_URL=[AuthN service gRPC URL] \
MF_AUTHN_GRPC_TIMEOUT=[AuthN service gRPC request timeout in seconds] \
MF_AUTH_GRPC_URL=[Auth service gRPC URL] \
MF_AUTH_GRPC_TIMEOUT=[Auth service gRPC request timeout in seconds] \
$GOBIN/mainflux-bootstrap
```
@@ -157,6 +115,6 @@ Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. Thi
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yml).
the [API documentation](openapi.yml).
[doc]: http://mainflux.readthedocs.io
+5 -5
View File
@@ -156,14 +156,14 @@ func dec(in []byte) ([]byte, error) {
return in, nil
}
func newService(authn mainflux.AuthNServiceClient, url string) bootstrap.Service {
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
things := mocks.NewConfigsRepository()
config := mfsdk.Config{
BaseURL: url,
}
sdk := mfsdk.NewSDK(config)
return bootstrap.New(authn, things, sdk, encKey)
return bootstrap.New(auth, things, sdk, encKey)
}
func generateChannels() map[string]things.Channel {
@@ -179,8 +179,8 @@ func generateChannels() map[string]things.Channel {
return channels
}
func newThingsService(authn mainflux.AuthNServiceClient) things.Service {
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), authn)
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), auth)
}
func newThingsServer(svc things.Service) *httptest.Server {
@@ -892,7 +892,7 @@ func TestList(t *testing.T) {
res: configPage{},
},
{
desc: "view list with invalid query params",
desc: "view list with invalid query parameters",
auth: validToken,
url: fmt.Sprintf("%s?offset=%d&limit=%d&state=%d&key=%%", path, 10, 10, bootstrap.Inactive),
status: http.StatusBadRequest,
+1 -2
View File
@@ -20,8 +20,7 @@ type metricsMiddleware struct {
svc bootstrap.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
// MetricsMiddleware instruments core service by tracking request count and latency.
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
return &metricsMiddleware{
counter: counter,
+14 -15
View File
@@ -27,12 +27,10 @@ const (
)
var (
errUnsupportedContentType = errors.New("unsupported content type")
errInvalidQueryParams = errors.New("invalid query params")
errInvalidLimitParam = errors.New("invalid limit query param")
errInvalidOffsetParam = errors.New("invalid offset query param")
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
partialMatch = []string{"name"}
errInvalidLimitParam = errors.New("invalid limit query param")
errInvalidOffsetParam = errors.New("invalid offset query param")
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
partialMatch = []string{"name"}
)
// MakeHandler returns a HTTP handler for API endpoints.
@@ -110,7 +108,7 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
req := addReq{token: r.Header.Get("Authorization")}
@@ -123,7 +121,7 @@ func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
req := updateReq{key: r.Header.Get("Authorization")}
@@ -137,7 +135,7 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
req := updateCertReq{
@@ -154,7 +152,7 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, e
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
req := updateConnReq{key: r.Header.Get("Authorization")}
@@ -169,7 +167,7 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errInvalidQueryParams
return nil, errors.ErrInvalidQueryParams
}
offset, limit, err := parsePagePrams(q)
@@ -200,7 +198,7 @@ func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, er
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
req := changeStateReq{key: r.Header.Get("Authorization")}
@@ -254,10 +252,11 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
case errors.Error:
w.Header().Set("Content-Type", contentType)
switch {
case errors.Contains(errorVal, errUnsupportedContentType):
case errors.Contains(errorVal, errors.ErrUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case errors.Contains(errorVal, errInvalidQueryParams):
case errors.Contains(errorVal, errors.ErrInvalidQueryParams):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, bootstrap.ErrMalformedEntity):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(errorVal, bootstrap.ErrNotFound):
@@ -292,7 +291,7 @@ func parseUint(s string) (uint64, error) {
ret, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, errInvalidQueryParams
return 0, errors.ErrInvalidQueryParams
}
return ret, nil
+21 -13
View File
@@ -19,17 +19,17 @@ type mainfluxThings struct {
counter uint64
things map[string]things.Thing
channels map[string]things.Channel
auth mainflux.AuthNServiceClient
auth mainflux.AuthServiceClient
connections map[string][]string
}
// NewThingsService returns Mainflux Things service mock.
// Only methods used by SDK are mocked.
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, authn mainflux.AuthNServiceClient) things.Service {
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, auth mainflux.AuthServiceClient) things.Service {
return &mainfluxThings{
things: things,
channels: channels,
auth: authn,
auth: auth,
connections: make(map[string][]string),
}
}
@@ -44,7 +44,7 @@ func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...
}
for i := range ths {
svc.counter++
ths[i].Owner = userID.Value
ths[i].Owner = userID.Email
ths[i].ID = strconv.FormatUint(svc.counter, 10)
ths[i].Key = ths[i].ID
svc.things[ths[i].ID] = ths[i]
@@ -62,7 +62,7 @@ func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (thing
return things.Thing{}, things.ErrUnauthorizedAccess
}
if t, ok := svc.things[id]; ok && t.Owner == userID.Value {
if t, ok := svc.things[id]; ok && t.Owner == userID.Email {
return t, nil
}
@@ -79,7 +79,7 @@ func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs
return things.ErrUnauthorizedAccess
}
for _, chID := range chIDs {
if svc.channels[chID].Owner != userID.Value {
if svc.channels[chID].Owner != userID.Email {
return things.ErrUnauthorizedAccess
}
for _, thID := range thIDs {
@@ -95,7 +95,7 @@ func (svc *mainfluxThings) Disconnect(_ context.Context, owner, chanID, thingID
defer svc.mu.Unlock()
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil || svc.channels[chanID].Owner != userID.Value {
if err != nil || svc.channels[chanID].Owner != userID.Email {
return things.ErrUnauthorizedAccess
}
@@ -131,7 +131,7 @@ func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) erro
return things.ErrUnauthorizedAccess
}
if t, ok := svc.things[id]; !ok || t.Owner != userID.Value {
if t, ok := svc.things[id]; !ok || t.Owner != userID.Email {
return things.ErrNotFound
}
@@ -167,15 +167,15 @@ func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) er
panic("not implemented")
}
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string, things.Metadata) (things.Page, error) {
func (svc *mainfluxThings) ListThings(context.Context, string, things.PageMetadata) (things.Page, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64, bool) (things.ChannelsPage, error) {
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, things.PageMetadata) (things.ChannelsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64, bool) (things.Page, error) {
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, things.PageMetadata) (things.Page, error) {
panic("not implemented")
}
@@ -189,7 +189,7 @@ func (svc *mainfluxThings) CreateChannels(_ context.Context, owner string, chs .
}
for i := range chs {
svc.counter++
chs[i].Owner = userID.Value
chs[i].Owner = userID.Email
chs[i].ID = strconv.FormatUint(svc.counter, 10)
svc.channels[chs[i].ID] = chs[i]
}
@@ -201,7 +201,7 @@ func (svc *mainfluxThings) UpdateChannel(context.Context, string, things.Channel
panic("not implemented")
}
func (svc *mainfluxThings) ListChannels(context.Context, string, uint64, uint64, string, things.Metadata) (things.ChannelsPage, error) {
func (svc *mainfluxThings) ListChannels(context.Context, string, things.PageMetadata) (things.ChannelsPage, error) {
panic("not implemented")
}
@@ -217,6 +217,10 @@ func (svc *mainfluxThings) CanAccessByID(context.Context, string, string) error
panic("not implemented")
}
func (svc *mainfluxThings) IsChannelOwner(context.Context, string, string) error {
panic("not implemented")
}
func (svc *mainfluxThings) Identify(context.Context, string) (string, error) {
panic("not implemented")
}
@@ -230,3 +234,7 @@ func findIndex(list []string, val string) int {
return -1
}
func (svc *mainfluxThings) ListMembers(ctx context.Context, token, groupID string, pm things.PageMetadata) (things.Page, error) {
panic("not implemented")
}
+18 -5
View File
@@ -6,31 +6,32 @@ package mocks
import (
"context"
"github.com/golang/protobuf/ptypes/empty"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/users"
"google.golang.org/grpc"
)
var _ mainflux.AuthNServiceClient = (*serviceMock)(nil)
var _ mainflux.AuthServiceClient = (*serviceMock)(nil)
type serviceMock struct {
users map[string]string
}
// NewUsersService creates mock of users service.
func NewUsersService(users map[string]string) mainflux.AuthNServiceClient {
func NewUsersService(users map[string]string) mainflux.AuthServiceClient {
return &serviceMock{users}
}
func (svc serviceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserID, error) {
func (svc serviceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserIdentity, error) {
if id, ok := svc.users[in.Value]; ok {
return &mainflux.UserID{Value: id}, nil
return &mainflux.UserIdentity{Email: id, Id: id}, nil
}
return nil, users.ErrUnauthorizedAccess
}
func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ...grpc.CallOption) (*mainflux.Token, error) {
if id, ok := svc.users[in.GetIssuer()]; ok {
if id, ok := svc.users[in.GetEmail()]; ok {
switch in.Type {
default:
return &mainflux.Token{Value: id}, nil
@@ -38,3 +39,15 @@ func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ..
}
return nil, users.ErrUnauthorizedAccess
}
func (svc serviceMock) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
panic("not implemented")
}
func (svc serviceMock) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
panic("not implemented")
}
func (svc serviceMock) Assign(ctx context.Context, req *mainflux.Assignment, _ ...grpc.CallOption) (r *empty.Empty, err error) {
panic("not implemented")
}
+61 -54
View File
@@ -18,15 +18,15 @@ paths:
requestBody:
$ref: "#/components/requestBodies/ConfigCreateReq"
responses:
201:
'201':
$ref: "#/components/responses/ConfigCreateRes"
400:
'400':
description: Failed due to malformed JSON.
403:
'403':
description: Missing or invalid access token provided.
415:
'415':
description: Missing or invalid content type.
500:
'500':
$ref: "#/components/responses/ServiceError"
get:
summary: Retrieves managed configs
@@ -44,13 +44,13 @@ paths:
- $ref: "#/components/parameters/State"
- $ref: "#/components/parameters/Name"
responses:
200:
'200':
$ref: "#/components/responses/ConfigListRes"
400:
'400':
description: Failed due to malformed query parameters.
403:
'403':
description: Missing or invalid access token provided.
500:
'500':
$ref: "#/components/responses/ServiceError"
/things/configs/{configId}:
get:
@@ -61,13 +61,13 @@ paths:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
responses:
200:
'200':
$ref: "#/components/responses/ConfigRes"
403:
'403':
description: Missing or invalid access token provided.
404:
'404':
description: Config does not exist.
500:
'500':
$ref: "#/components/responses/ServiceError"
put:
summary: Updates config info
@@ -83,17 +83,17 @@ paths:
requestBody:
$ref: "#/components/requestBodies/ConfigUpdateReq"
responses:
200:
'200':
description: Config updated.
400:
'400':
description: Failed due to malformed JSON.
403:
'403':
description: Missing or invalid access token provided.
404:
'404':
description: Config does not exist.
415:
'415':
description: Missing or invalid content type.
500:
'500':
$ref: "#/components/responses/ServiceError"
delete:
summary: Removes a Config
@@ -106,13 +106,13 @@ paths:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ConfigId"
responses:
204:
'204':
description: Config removed.
400:
'400':
description: Failed due to malformed config ID.
403:
'403':
description: Missing or invalid access token provided.
500:
'500':
$ref: "#/components/responses/ServiceError"
/things/configs/certs/{configId}:
patch:
@@ -128,17 +128,17 @@ paths:
requestBody:
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
responses:
200:
'200':
description: Config updated.
400:
'400':
description: Failed due to malformed JSON.
403:
'403':
description: Missing or invalid access token provided.
404:
'404':
description: Config does not exist.
415:
'415':
description: Missing or invalid content type.
500:
'500':
$ref: "#/components/responses/ServiceError"
/things/configs/connections/{configId}:
put:
@@ -154,17 +154,17 @@ paths:
requestBody:
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
responses:
200:
'200':
description: Config updated.
400:
'400':
description: Failed due to malformed JSON.
403:
'403':
description: Missing or invalid access token provided.
404:
'404':
description: Config does not exist.
415:
'415':
description: Missing or invalid content type.
500:
'500':
$ref: "#/components/responses/ServiceError"
/things/bootstrap/{externalId}:
get:
@@ -177,12 +177,12 @@ paths:
- $ref: "#/components/parameters/ConfigAuth"
- $ref: "#/components/parameters/ExternalId"
responses:
200:
'200':
$ref: "#/components/responses/BootstrapConfigRes"
404:
'404':
description: |
Failed to retrieve corresponding config.
500:
'500':
$ref: "#/components/responses/ServiceError"
/things/bootstrap/secure/{externalId}:
get:
@@ -195,12 +195,12 @@ paths:
- $ref: "#/components/parameters/EncConfigAuth"
- $ref: "#/components/parameters/ExternalId"
responses:
200:
'200':
$ref: "#/components/responses/BootstrapConfigRes"
404:
'404':
description: |
Failed to retrieve corresponding config.
500:
Failed to retrieve corresponding config.
'500':
$ref: "#/components/responses/ServiceError"
/things/state/{configId}:
put:
@@ -216,28 +216,30 @@ paths:
requestBody:
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
responses:
204:
'204':
description: Config removed.
400:
'400':
description: Failed due to malformed config's ID.
403:
'403':
description: Missing or invalid access token provided.
500:
'500':
$ref: "#/components/responses/ServiceError"
components:
schemas:
State:
type: integer
enum: [0, 1]
enum: [0, 1]
Config:
type: object
properties:
mainflux_id:
type: string
format: uuid
description: Corresponding Mainflux Thing ID.
mainflux_key:
type: string
format: uuid
description: Corresponding Mainflux Thing key.
mainflux_channels:
type: array
@@ -247,7 +249,8 @@ components:
properties:
id:
type: string
description: ID of the Channel.
format: uuid
description: Channel unique identifier.
name:
type: string
description: Name of the Channel.
@@ -267,7 +270,7 @@ components:
$ref: "#/components/schemas/State"
required:
- external_id
- external_key
- external_key
ConfigList:
type: object
properties:
@@ -298,9 +301,11 @@ components:
properties:
mainflux_id:
type: string
format: uuid
description: Corresponding Mainflux Thing ID.
mainflux_key:
type: string
format: uuid
description: Corresponding Mainflux Thing key.
mainflux_channels:
type: array
@@ -323,7 +328,7 @@ components:
- mainflux_id
- mainflux_key
- mainflux_channels
- content
- content
parameters:
Authorization:
@@ -332,6 +337,7 @@ components:
in: header
schema:
type: string
format: jwt
required: true
ConfigAuth:
name: configAuthorization
@@ -343,8 +349,8 @@ components:
EncConfigAuth:
name: configAuthorization
description: |
Hex-encoded configuration external key encrypted using
the AES algorithm and SHA256 sum of the external key
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:
@@ -356,6 +362,7 @@ components:
in: path
schema:
type: string
format: uuid
required: true
ExternalId:
name: externalId
@@ -452,12 +459,12 @@ components:
client_key:
type: string
ca_cert:
type: string
type: string
ConfigConnUpdateReq:
description: Array if IDs the thing is be connected to.
content:
application/json:
schema:
schema:
type: object
properties:
channels:
+1 -1
View File
@@ -36,7 +36,7 @@ func TestMain(m *testing.M) {
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.2-alpine", cfg)
container, err := pool.Run("postgres", "10.8-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
+2 -2
View File
@@ -61,7 +61,7 @@ var (
}
)
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
configs := mocks.NewConfigsRepository()
config := mfsdk.Config{
BaseURL: url,
@@ -71,7 +71,7 @@ func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service
return bootstrap.New(auth, configs, sdk, encKey)
}
func newThingsService(auth mainflux.AuthNServiceClient) things.Service {
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
channels := make(map[string]things.Channel, channelsNum)
for i := 0; i < channelsNum; i++ {
id := strconv.Itoa(i + 1)
+3 -3
View File
@@ -116,7 +116,7 @@ type ConfigReader interface {
}
type bootstrapService struct {
auth mainflux.AuthNServiceClient
auth mainflux.AuthServiceClient
configs ConfigRepository
sdk mfsdk.SDK
encKey []byte
@@ -124,7 +124,7 @@ type bootstrapService struct {
}
// New returns new Bootstrap service.
func New(auth mainflux.AuthNServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
func New(auth mainflux.AuthServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
return &bootstrapService{
configs: configs,
sdk: sdk,
@@ -388,7 +388,7 @@ func (bs bootstrapService) identify(token string) (string, error) {
return "", ErrUnauthorizedAccess
}
return res.GetValue(), nil
return res.GetEmail(), nil
}
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
+2 -2
View File
@@ -53,7 +53,7 @@ var (
}
)
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
things := mocks.NewConfigsRepository()
config := mfsdk.Config{
BaseURL: url,
@@ -63,7 +63,7 @@ func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service
return bootstrap.New(auth, things, sdk, encKey)
}
func newThingsService(auth mainflux.AuthNServiceClient) things.Service {
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
channels := make(map[string]things.Channel, channelsNum)
for i := 0; i < channelsNum; i++ {
id := strconv.Itoa(i + 1)
+2 -2
View File
@@ -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>, "rsa_bits":2048, "key_type":"rsa"}'
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"}'
```
```json
@@ -50,4 +50,4 @@ 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"}'
```
```
+18 -11
View File
@@ -18,9 +18,15 @@ func issueCert(svc certs.Service) endpoint.Endpoint {
}
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.Valid, req.KeyBits, req.KeyType)
if err != nil {
return certsResponse{Error: err.Error()}, nil
return certsRes{}, err
}
return res, nil
return certsRes{
CertSerial: res.Serial,
ThingID: res.ThingID,
CertKey: res.ClientKey,
Cert: res.ClientCert,
CACert: res.IssuingCA,
}, nil
}
}
@@ -31,11 +37,9 @@ func listCerts(svc certs.Service) endpoint.Endpoint {
return nil, err
}
page, err := svc.ListCerts(ctx, req.token, req.offset, req.limit)
page, err := svc.ListCerts(ctx, req.token, req.thingID, req.offset, req.limit)
if err != nil {
return certsPageRes{
Error: err.Error(),
}, err
return certsPageRes{}, err
}
res := certsPageRes{
pageRes: pageRes{
@@ -43,13 +47,16 @@ func listCerts(svc certs.Service) endpoint.Endpoint {
Offset: page.Offset,
Limit: page.Limit,
},
Certs: []certsResponse{},
Certs: []certsRes{},
}
for _, cert := range page.Certs {
view := certsResponse{
Serial: cert.Serial,
ThingID: cert.ThingID,
view := certsRes{
CertSerial: cert.Serial,
ThingID: cert.ThingID,
CertKey: cert.ClientKey,
Cert: cert.ClientCert,
CACert: cert.IssuingCA,
}
res.Certs = append(res.Certs, view)
}
@@ -63,6 +70,6 @@ func revokeCert(svc certs.Service) endpoint.Endpoint {
if err := req.validate(); err != nil {
return nil, err
}
return svc.RevokeCert(ctx, req.token, req.ThingID)
return svc.RevokeCert(ctx, req.token, req.certID)
}
}
+3 -3
View File
@@ -37,9 +37,9 @@ func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, days
return lm.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
}
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (cp certs.Page, err error) {
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (cp certs.Page, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_certs for token: %s took %s to complete", token, time.Since(begin))
message := fmt.Sprintf("Method list_certs 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
@@ -47,7 +47,7 @@ func (lm *loggingMiddleware) ListCerts(ctx context.Context, token string, offset
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListCerts(ctx, token, offset, limit)
return lm.svc.ListCerts(ctx, token, thingID, offset, limit)
}
func (lm *loggingMiddleware) RevokeCert(ctx context.Context, token, thingID string) (c certs.Revoke, err error) {
+3 -4
View File
@@ -19,8 +19,7 @@ type metricsMiddleware struct {
svc certs.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
// MetricsMiddleware instruments core service by tracking request count and latency.
func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metrics.Histogram) certs.Service {
return &metricsMiddleware{
counter: counter,
@@ -38,13 +37,13 @@ func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID strin
return ms.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
}
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (certs.Page, error) {
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (certs.Page, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_certs").Add(1)
ms.latency.With("method", "list_certs").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListCerts(ctx, token, offset, limit)
return ms.svc.ListCerts(ctx, token, thingID, offset, limit)
}
func (ms *metricsMiddleware) RevokeCert(ctx context.Context, token, thingID string) (certs.Revoke, error) {
+7 -6
View File
@@ -23,9 +23,10 @@ func (req addCertsReq) validate() error {
}
type listReq struct {
token string
offset uint64
limit uint64
thingID string
token string
offset uint64
limit uint64
}
func (req *listReq) validate() error {
@@ -39,12 +40,12 @@ func (req *listReq) validate() error {
}
type revokeReq struct {
token string
ThingID string `json:"thing_id"`
token string
certID string
}
func (req *revokeReq) validate() error {
if req.token == "" || req.ThingID == "" {
if req.token == "" || req.certID == "" {
return certs.ErrUnauthorizedAccess
}
+10 -12
View File
@@ -15,17 +15,15 @@ type pageRes struct {
type certsPageRes struct {
pageRes
Certs []certsResponse `json:"certs"`
Error string `json:"error,omitempty"`
Certs []certsRes `json:"certs"`
}
type certsResponse struct {
ClientCert map[string]string `json:"client_cert"`
ClientKey map[string]string `json:"client_key"`
Serial string `json:"serial"`
ThingID string `json:"thing_id"`
CACert string `json:"ca_cert"`
Error string `json:"error"`
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"`
}
func (res certsPageRes) Code() int {
@@ -40,14 +38,14 @@ func (res certsPageRes) Empty() bool {
return false
}
func (res certsResponse) Code() int {
func (res certsRes) Code() int {
return http.StatusCreated
}
func (res certsResponse) Headers() map[string]string {
func (res certsRes) Headers() map[string]string {
return map[string]string{}
}
func (res certsResponse) Empty() bool {
func (res certsRes) Empty() bool {
return false
}
+24 -50
View File
@@ -8,32 +8,27 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/pkg/errors"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/internal/httputil"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
contentType = "application/json"
offset = "offset"
limit = "limit"
defOffset = 0
defLimit = 10
offsetKey = "offset"
limitKey = "limit"
defOffset = 0
defLimit = 10
)
var (
errUnsupportedContentType = errors.New("unsupported content type")
errUnauthorized = errors.New("missing or invalid credentials provided")
errInvalidQueryParams = errors.New("invalid query params")
errMalformedEntity = errors.New("malformed entity")
errConflict = errors.New("entity already exists")
errUnauthorized = errors.New("missing or invalid credentials provided")
errConflict = errors.New("entity already exists")
)
// MakeHandler returns a HTTP handler for API endpoints.
@@ -51,14 +46,14 @@ func MakeHandler(svc certs.Service) http.Handler {
opts...,
))
r.Get("/certs", kithttp.NewServer(
r.Get("/certs/:thingId", kithttp.NewServer(
listCerts(svc),
decodeListCerts,
encodeResponse,
opts...,
))
r.Delete("/certs/revoke", kithttp.NewServer(
r.Delete("/certs/:certId", kithttp.NewServer(
revokeCert(svc),
decodeRevokeCerts,
encodeResponse,
@@ -90,44 +85,26 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
}
func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
l, err := readUintQuery(r, limit, defLimit)
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
o, err := readUintQuery(r, offset, defOffset)
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
req := listReq{
token: r.Header.Get("Authorization"),
limit: l,
offset: o,
token: r.Header.Get("Authorization"),
thingID: bone.GetValue(r, "thingId"),
limit: l,
offset: o,
}
return req, nil
}
func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return 0, errInvalidQueryParams
}
if len(vals) == 0 {
return def, nil
}
strval := vals[0]
val, err := strconv.ParseUint(strval, 10, 64)
if err != nil {
return 0, errInvalidQueryParams
}
return val, nil
}
func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
return nil, errors.ErrUnsupportedContentType
}
req := addCertsReq{token: r.Header.Get("Authorization")}
@@ -139,13 +116,9 @@ func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
}
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
req := revokeReq{token: r.Header.Get("Authorization")}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
req := revokeReq{
token: r.Header.Get("Authorization"),
certID: bone.GetValue(r, "certId"),
}
return req, nil
@@ -155,9 +128,10 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", contentType)
switch err {
case errUnsupportedContentType:
case errors.ErrUnsupportedContentType:
w.WriteHeader(http.StatusUnsupportedMediaType)
case io.EOF, errMalformedEntity:
case io.EOF, errors.ErrMalformedEntity,
errors.ErrInvalidQueryParams:
w.WriteHeader(http.StatusBadRequest)
case errConflict:
w.WriteHeader(http.StatusConflict)
+2 -2
View File
@@ -18,8 +18,8 @@ type Repository interface {
// Save saves cert for thing into database
Save(ctx context.Context, cert Cert) (string, error)
// RetrieveAll retrieve all issued certificates for given owner
RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (Page, error)
// RetrieveAll retrieve all issued certificates for given owner and thing id
RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (Page, error)
// Remove certificate from DB for given thing
Remove(ctx context.Context, thingID string) error
+87
View File
@@ -0,0 +1,87 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"sync"
"github.com/mainflux/mainflux/certs"
)
var _ certs.Repository = (*certsRepoMock)(nil)
type certsRepoMock struct {
mu sync.Mutex
counter uint64
certs map[string]certs.Cert
certsByThingID 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),
}
}
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
c.counter++
return cert.Serial, nil
}
func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID, thingID 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
var crts []certs.Cert
i := uint64(1)
for _, v := range c.certs {
if i >= first && i < last {
crts = append(crts, v)
}
i++
}
page := certs.Page{
Certs: crts,
Total: c.counter,
Offset: offset,
Limit: limit,
}
return page, nil
}
func (c *certsRepoMock) Remove(ctx context.Context, serial string) error {
c.mu.Lock()
defer c.mu.Unlock()
crt, ok := c.certs[serial]
if !ok {
return certs.ErrNotFound
}
delete(c.certs, crt.Serial)
delete(c.certsByThingID, crt.ThingID)
return nil
}
func (c *certsRepoMock) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
c.mu.Lock()
defer c.mu.Unlock()
crt, ok := c.certsByThingID[thingID]
if !ok {
return certs.Cert{}, certs.ErrNotFound
}
return crt, nil
}
+168
View File
@@ -0,0 +1,168 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"bufio"
"bytes"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
"github.com/mainflux/mainflux/certs/pki"
"github.com/mainflux/mainflux/pkg/errors"
)
var (
errPrivateKeyEmpty = errors.New("private key is empty")
errPrivateKeyUnsupportedType = errors.New("private key type is unsupported")
)
var _ pki.Agent = (*agent)(nil)
type agent struct {
AuthTimeout time.Duration
TLSCert tls.Certificate
X509Cert *x509.Certificate
RSABits int
HoursValid string
}
func NewPkiAgent(tlsCert tls.Certificate, caCert *x509.Certificate, keyBits int, hoursValid string, timeout time.Duration) pki.Agent {
return &agent{
AuthTimeout: timeout,
TLSCert: tlsCert,
X509Cert: caCert,
RSABits: keyBits,
HoursValid: hoursValid,
}
}
func (a *agent) IssueCert(cn string, ttl, keyType string, keyBits int) (pki.Cert, error) {
return a.certs(cn, ttl, keyBits)
}
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)
}
var priv interface{}
priv, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
if daysValid == "" {
daysValid = a.HoursValid
}
notBefore := time.Now()
validFor, err := time.ParseDuration(daysValid)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
tmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Mainflux"},
CommonName: cn,
OrganizationalUnit: []string{"mainflux"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
SubjectKeyId: []byte{1, 2, 3, 4, 6},
}
pubKey, err := publicKey(priv)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, a.X509Cert, pubKey, a.TLSCert.PrivateKey)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
x509cert, err := x509.ParseCertificate(derBytes)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
var bw, keyOut bytes.Buffer
buffWriter := bufio.NewWriter(&bw)
buffKeyOut := bufio.NewWriter(&keyOut)
if err := pem.Encode(buffWriter, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
buffWriter.Flush()
cert := bw.String()
block, err := pemBlockForKey(priv)
if err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
if err := pem.Encode(buffKeyOut, block); err != nil {
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
}
buffKeyOut.Flush()
key := keyOut.String()
return pki.Cert{
ClientCert: cert,
ClientKey: key,
Serial: x509cert.SerialNumber.String(),
Expire: x509cert.NotAfter,
IssuingCA: x509cert.Issuer.String(),
}, nil
}
func publicKey(priv interface{}) (interface{}, error) {
if priv == nil {
return nil, errPrivateKeyEmpty
}
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey, nil
case *ecdsa.PrivateKey:
return &k.PublicKey, nil
default:
return nil, errPrivateKeyUnsupportedType
}
}
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil, err
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
default:
return nil, nil
}
}
+26 -15
View File
@@ -16,13 +16,13 @@ paths:
requestBody:
$ref: "#/components/requestBodies/CertReq"
responses:
201:
'201':
description: Created
400:
'400':
description: Failed due to malformed JSON.
500:
'500':
description: Unexpected server-side error ocurred.
/certs/{thingID}:
/certs/{thingId}:
get:
summary: Retrieves certificates
description: |
@@ -33,30 +33,30 @@ paths:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingID"
responses:
200:
'200':
$ref: "#/components/responses/CertsRes"
404:
'404':
description: |
Failed to retrieve corresponding certificate.
500:
'500':
$ref: "#/components/responses/ServiceError"
/certs/revoke:
/certs/{certId}:
delete:
summary: Revokes certificate
description: |
Revokes a certificates for given thing ID .
Revokes a certificates for given cert ID .
tags:
- configs
parameters:
- $ref: "#/components/parameters/Authorization"
- $ref: "#/components/parameters/ThingID"
- $ref: "#/components/parameters/CertID"
responses:
200:
'200':
$ref: "#/components/responses/RevokeRes"
404:
'404':
description: |
Failed to revoke corresponding certificate.
500:
'500':
$ref: "#/components/responses/ServiceError"
components:
@@ -74,6 +74,15 @@ components:
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:
@@ -82,6 +91,7 @@ components:
properties:
thing_id:
type: string
format: uuid
description: Corresponding Mainflux Thing ID.
client_cert:
type: string
@@ -118,13 +128,14 @@ components:
required:
- thing_id
- days_valid
- rsa_bits
- key_bits
properties:
thing_id:
type: string
format: uuid
days_valid:
type: string
rsa_bits:
key_bits:
type: integer
responses:
-28
View File
@@ -1,28 +0,0 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package pki wraps vault client
package pki
import "time"
type Revoke struct {
RevocationTime time.Time `mapstructure:"revocation_time"`
}
type Cert struct {
ClientCert string `json:"client_cert" mapstructure:"certificate"`
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
ClientKey string `json:"client_key" mapstructure:"private_key"`
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
Serial string `json:"serial" mapstructure:"serial_number"`
Expire time.Time `json:"expire" mapstructure:"-"`
}
type Agent interface {
// IssueCert issues certificate on PKI
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
// Revoke revokes certificate from PKI
Revoke(serial string) (Revoke, error)
}
+38 -11
View File
@@ -22,10 +22,40 @@ const (
)
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")
// ErrFailedCertCreation indicates failed to certificate creation
ErrFailedCertCreation = errors.New("failed to create client certificate")
// ErrFailedCertRevocation indicates failed certificate revocation
ErrFailedCertRevocation = errors.New("failed to revoke certificate")
errFailedVaultCertIssue = errors.New("failed to issue vault certificate")
errFailedCertDecoding = errors.New("failed to decode response from vault service")
)
type Cert struct {
ClientCert string `json:"client_cert" mapstructure:"certificate"`
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
ClientKey string `json:"client_key" mapstructure:"private_key"`
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
Serial string `json:"serial" mapstructure:"serial_number"`
Expire time.Time `json:"expire" mapstructure:"-"`
}
type Agent interface {
// IssueCert issues certificate on PKI
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
// Revoke revokes certificate from PKI
Revoke(serial string) (time.Time, error)
}
type pkiAgent struct {
token string
path string
@@ -117,14 +147,14 @@ func (p *pkiAgent) IssueCert(cn string, ttl, keyType string, keyBits int) (Cert,
}
func (p *pkiAgent) Revoke(serial string) (Revoke, error) {
func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
cReq := certRevokeReq{
SerialNumber: serial,
}
r := p.client.NewRequest("POST", p.revokeURL)
if err := r.SetJSONBody(cReq); err != nil {
return Revoke{}, err
return time.Time{}, err
}
resp, err := p.client.RawRequest(r)
@@ -133,29 +163,26 @@ func (p *pkiAgent) Revoke(serial string) (Revoke, error) {
}
if err != nil {
return Revoke{}, err
return time.Time{}, err
}
if resp.StatusCode >= http.StatusBadRequest {
_, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Revoke{}, err
return time.Time{}, err
}
return Revoke{}, errors.Wrap(errFailedVaultCertIssue, err)
return time.Time{}, errors.Wrap(errFailedVaultCertIssue, err)
}
s, err := api.ParseSecret(resp.Body)
if err != nil {
return Revoke{}, err
return time.Time{}, err
}
rev, err := s.Data["revocation_time"].(json.Number).Float64()
if err != nil {
return Revoke{}, err
return time.Time{}, err
}
revTime := time.Unix(0, int64(rev)*int64(time.Millisecond))
return Revoke{
RevocationTime: revTime,
}, nil
return time.Unix(0, int64(rev)*int64(time.Millisecond)), nil
}
+1 -1
View File
@@ -45,7 +45,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 string, offset, limit uint64) (certs.Page, error) {
func (cr certsRepository) RetrieveAll(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 ORDER BY expire LIMIT $2 OFFSET $3;`
rows, err := cr.db.Query(q, ownerID, limit, offset)
if err != nil {
+1 -1
View File
@@ -64,7 +64,7 @@ func migrateDB(db *sqlx.DB) error {
owner_id TEXT NOT NULL,
expire TIMESTAMPTZ NOT NULL,
serial TEXT NOT NULL,
PRIMARY KEY (thing_id, owner_id)
PRIMARY KEY (thing_id, owner_id, serial)
);`,
},
Down: []string{
+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.2-alpine", cfg)
container, err := pool.Run("postgres", "10.8-alpine", cfg)
if err != nil {
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
}
+38 -162
View File
@@ -4,17 +4,9 @@
package certs
import (
"bufio"
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
"github.com/mainflux/mainflux"
@@ -27,25 +19,20 @@ var (
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
// ErrMalformedEntity indicates malformed entity specification.
// ErrMalformedEntity indicates malformed entity specification
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
errFailedKeyCreation = errors.New("failed to create client private key")
errFailedDateSetting = errors.New("failed to set date for certificate")
errKeyBitsValueWrong = errors.New("missing RSA bits for certificate creation")
errMissingCACertificate = errors.New("missing CA certificate for certificate signing")
errFailedSerialGeneration = errors.New("failed to generate certificate serial")
errFailedPemKeyWrite = errors.New("failed to write PEM key")
errFailedPemDataWrite = errors.New("failed to write pem data for certificate")
errPrivateKeyUnsupportedType = errors.New("private key type is unsupported")
errPrivateKeyEmpty = errors.New("private key is empty")
errFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
errFailedCertCreation = errors.New("failed to create client certificate")
errFailedCertRevocation = errors.New("failed to revoke certificate")
// ErrFailedCertCreation failed to create certificate
ErrFailedCertCreation = errors.New("failed to create client certificate")
// ErrFailedCertRevocation failed to revoke certificate
ErrFailedCertRevocation = errors.New("failed to revoke certificate")
errFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
)
var _ Service = (*certsService)(nil)
@@ -57,12 +44,13 @@ type Service interface {
IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (Cert, error)
// ListCerts lists all certificates issued for given owner
ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error)
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)
}
// Config defines the service parameters
type Config struct {
LogLevel string
ClientTLS bool
@@ -73,8 +61,8 @@ type Config struct {
BaseURL string
ThingsPrefix string
JaegerURL string
AuthnURL string
AuthnTimeout time.Duration
AuthURL string
AuthTimeout time.Duration
SignTLSCert tls.Certificate
SignX509Cert *x509.Certificate
SignRSABits int
@@ -86,7 +74,7 @@ type Config struct {
}
type certsService struct {
auth mainflux.AuthNServiceClient
auth mainflux.AuthServiceClient
certsRepo Repository
sdk mfsdk.SDK
conf Config
@@ -94,7 +82,7 @@ type certsService struct {
}
// New returns new Certs service.
func New(auth mainflux.AuthNServiceClient, certs Repository, sdk mfsdk.SDK, config Config, pki pki.Agent) Service {
func New(auth mainflux.AuthServiceClient, certs Repository, sdk mfsdk.SDK, config Config, pki pki.Agent) Service {
return &certsService{
certsRepo: certs,
sdk: sdk,
@@ -104,10 +92,12 @@ func New(auth mainflux.AuthNServiceClient, certs Repository, sdk mfsdk.SDK, conf
}
}
// Revoke defines the conditions to revoke a certificate
type Revoke struct {
RevocationTime time.Time `mapstructure:"revocation_time"`
}
// Cert defines the certificate paremeters
type Cert struct {
OwnerID string `json:"owner_id" mapstructure:"owner_id"`
ThingID string `json:"thing_id" mapstructure:"thing_id"`
@@ -121,40 +111,32 @@ type Cert struct {
}
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
var c Cert
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return c, errors.Wrap(ErrUnauthorizedAccess, err)
return Cert{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
thing, err := cs.sdk.Thing(thingID, token)
if err != nil {
return c, errors.Wrap(errFailedCertCreation, err)
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
}
// If PKIHost is not set we don't use 3rd party PKI service.
if cs.conf.PKIHost == "" {
c.ClientCert, c.ClientKey, err = cs.certs(thing.Key, daysValid, keyBits)
if err != nil {
return c, errors.Wrap(errFailedCertCreation, err)
}
return c, err
}
cert, err := cs.pki.IssueCert(thingID, daysValid, keyType, keyBits)
cert, err := cs.pki.IssueCert(thing.Key, daysValid, keyType, keyBits)
if err != nil {
return c, errors.Wrap(errFailedCertCreation, err)
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
}
c.ThingID = thingID
c.OwnerID = owner.GetValue()
c.ClientCert = cert.ClientCert
c.IssuingCA = cert.IssuingCA
c.CAChain = cert.CAChain
c.ClientKey = cert.ClientKey
c.PrivateKeyType = cert.PrivateKeyType
c.Serial = cert.Serial
c.Expire = cert.Expire
c := Cert{
ThingID: thingID,
OwnerID: owner.GetEmail(),
ClientCert: cert.ClientCert,
IssuingCA: cert.IssuingCA,
CAChain: cert.CAChain,
ClientKey: cert.ClientKey,
PrivateKeyType: cert.PrivateKeyType,
Serial: cert.Serial,
Expire: cert.Expire,
}
_, err = cs.certsRepo.Save(context.Background(), c)
return c, err
@@ -168,136 +150,30 @@ func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (
}
thing, err := cs.sdk.Thing(thingID, token)
if err != nil {
return revoke, errors.Wrap(errFailedCertRevocation, err)
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
cert, err := cs.certsRepo.RetrieveByThing(ctx, thing.ID)
if err != nil {
return revoke, errors.Wrap(errFailedCertRevocation, err)
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
r, err := cs.pki.Revoke(cert.Serial)
revTime, err := cs.pki.Revoke(cert.Serial)
if err != nil {
return revoke, errors.Wrap(errFailedCertRevocation, err)
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
}
revoke.RevocationTime = r.RevocationTime
revoke.RevocationTime = revTime
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
}
return revoke, nil
}
func (cs *certsService) ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error) {
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 cs.certsRepo.RetrieveAll(ctx, u.GetValue(), offset, limit)
}
func (cs *certsService) certs(thingKey, daysValid string, keyBits int) (string, string, error) {
if cs.conf.SignX509Cert == nil {
return "", "", errors.Wrap(errFailedCertCreation, errMissingCACertificate)
}
if keyBits == 0 {
return "", "", errors.Wrap(errFailedCertCreation, errKeyBitsValueWrong)
}
var priv interface{}
priv, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
return "", "", errors.Wrap(errFailedKeyCreation, err)
}
if daysValid == "" {
daysValid = cs.conf.SignHoursValid
}
notBefore := time.Now()
validFor, err := time.ParseDuration(daysValid)
if err != nil {
return "", "", errors.Wrap(errFailedDateSetting, err)
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return "", "", errors.Wrap(errFailedSerialGeneration, err)
}
tmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Mainflux"},
CommonName: thingKey,
OrganizationalUnit: []string{"mainflux"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
SubjectKeyId: []byte{1, 2, 3, 4, 6},
}
pubKey, err := publicKey(priv)
if err != nil {
return "", "", errors.Wrap(errFailedCertCreation, err)
}
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, cs.conf.SignX509Cert, pubKey, cs.conf.SignTLSCert.PrivateKey)
if err != nil {
return "", "", errors.Wrap(errFailedCertCreation, err)
}
var bw, keyOut bytes.Buffer
buffWriter := bufio.NewWriter(&bw)
buffKeyOut := bufio.NewWriter(&keyOut)
if err := pem.Encode(buffWriter, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return "", "", errors.Wrap(errFailedPemDataWrite, err)
}
buffWriter.Flush()
cert := bw.String()
block, err := pemBlockForKey(priv)
if err != nil {
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
}
if err := pem.Encode(buffKeyOut, block); err != nil {
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
}
buffKeyOut.Flush()
key := keyOut.String()
return cert, key, nil
}
func publicKey(priv interface{}) (interface{}, error) {
if priv == nil {
return nil, errPrivateKeyEmpty
}
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey, nil
case *ecdsa.PrivateKey:
return &k.PublicKey, nil
default:
return nil, errPrivateKeyUnsupportedType
}
}
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil, err
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
default:
return nil, nil
}
return cs.certsRepo.RetrieveAll(ctx, u.GetEmail(), thingID, offset, limit)
}
+340
View File
@@ -0,0 +1,340 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package certs_test
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/mainflux/mainflux"
bsmocks "github.com/mainflux/mainflux/bootstrap/mocks"
"github.com/mainflux/mainflux/certs"
"github.com/mainflux/mainflux/certs/mocks"
"github.com/mainflux/mainflux/pkg/errors"
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
"github.com/mainflux/mainflux/things"
httpapi "github.com/mainflux/mainflux/things/api/things/http"
thmocks "github.com/mainflux/mainflux/things/mocks"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
wrongValue = "wrong-value"
email = "user@example.com"
token = "token"
thingsNum = 1
thingKey = "thingKey"
thingID = "1"
daysValid = "1h"
keyBits = 2048
key = "rsa"
certNum = 10
cfgLogLevel = "error"
cfgClientTLS = false
cfgServerCert = ""
cfgServerKey = ""
cfgBaseURL = "http://localhost"
cfgThingsPrefix = ""
cfgJaegerURL = ""
cfgAuthURL = "localhost:8181"
cfgAuthTimeout = "1s"
caPath = "../docker/ssl/certs/ca.crt"
caKeyPath = "../docker/ssl/certs/ca.key"
cfgSignHoursValid = "24h"
cfgSignRSABits = 2048
)
func newService(tokens map[string]string) (certs.Service, error) {
users := bsmocks.NewUsersService(map[string]string{token: email})
server := newThingsServer(newThingsService(users))
auth := thmocks.NewAuthService(tokens)
config := mfsdk.Config{
BaseURL: server.URL,
}
sdk := mfsdk.NewSDK(config)
repo := mocks.NewCertsRepository()
tlsCert, caCert, err := loadCertificates(caPath, caKeyPath)
if err != nil {
return nil, err
}
authTimeout, err := time.ParseDuration(cfgAuthTimeout)
if err != nil {
return nil, err
}
c := certs.Config{
LogLevel: cfgLogLevel,
ClientTLS: cfgClientTLS,
ServerCert: cfgServerCert,
ServerKey: cfgServerKey,
BaseURL: cfgBaseURL,
ThingsPrefix: cfgThingsPrefix,
JaegerURL: cfgJaegerURL,
AuthURL: cfgAuthURL,
SignTLSCert: tlsCert,
SignX509Cert: caCert,
SignHoursValid: cfgSignHoursValid,
SignRSABits: cfgSignRSABits,
}
pki := mocks.NewPkiAgent(tlsCert, caCert, cfgSignRSABits, cfgSignHoursValid, authTimeout)
return certs.New(auth, repo, sdk, c, pki), nil
}
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
ths := make(map[string]things.Thing, thingsNum)
for i := 0; i < thingsNum; i++ {
id := strconv.Itoa(i + 1)
ths[id] = things.Thing{
ID: id,
Key: thingKey,
Owner: email,
}
}
return bsmocks.NewThingsService(ths, map[string]things.Channel{}, auth)
}
func TestIssueCert(t *testing.T) {
svc, err := newService(map[string]string{token: email})
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
}{
{
desc: "issue new cert",
token: token,
thingID: thingID,
daysValid: daysValid,
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: wrongValue,
thingID: thingID,
daysValid: daysValid,
key: key,
keyBits: 2048,
err: certs.ErrUnauthorizedAccess,
},
{
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,
daysValid: daysValid,
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)
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 {
assert.True(t, strings.Contains(cert.Subject.CommonName, thingKey), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
}
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)
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
cases := []struct {
token string
desc string
thingID string
err error
}{
{
desc: "revoke cert",
token: token,
thingID: thingID,
err: nil,
},
{
desc: "revoke cert for invalid token",
token: wrongValue,
thingID: thingID,
err: certs.ErrUnauthorizedAccess,
},
{
desc: "revoke cert for invalid thing id",
token: token,
thingID: "2",
err: certs.ErrFailedCertRevocation,
},
}
for _, tc := range cases {
_, err := svc.RevokeCert(context.Background(), tc.token, tc.thingID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListCerts(t *testing.T) {
svc, err := newService(map[string]string{token: email})
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)
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
}
cases := []struct {
token string
desc string
thingID string
offset uint64
limit uint64
size uint64
err error
}{
{
desc: "list all certs with valid token",
token: token,
thingID: thingID,
offset: 0,
limit: certNum,
size: certNum,
err: nil,
},
{
desc: "list all certs with invalid token",
token: wrongValue,
thingID: thingID,
offset: 0,
limit: certNum,
size: 0,
err: certs.ErrUnauthorizedAccess,
},
{
desc: "list half certs with invalid token",
token: token,
thingID: thingID,
offset: certNum / 2,
limit: certNum,
size: certNum / 2,
err: nil,
},
{
desc: "list last certs with invalid token",
token: token,
thingID: thingID,
offset: certNum - 1,
limit: certNum,
size: 1,
err: nil,
},
}
for _, tc := range cases {
page, err := svc.ListCerts(context.Background(), tc.token, tc.thingID, tc.offset, tc.limit)
size := uint64(len(page.Certs))
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 newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(mocktracer.New(), svc)
return httptest.NewServer(mux)
}
func loadCertificates(caPath, caKeyPath string) (tls.Certificate, *x509.Certificate, error) {
var tlsCert tls.Certificate
var caCert *x509.Certificate
if caPath == "" || caKeyPath == "" {
return tlsCert, caCert, nil
}
if _, err := os.Stat(caPath); os.IsNotExist(err) {
return tlsCert, caCert, err
}
if _, err := os.Stat(caKeyPath); os.IsNotExist(err) {
return tlsCert, caCert, err
}
tlsCert, err := tls.LoadX509KeyPair(caPath, caKeyPath)
if err != nil {
return tlsCert, caCert, errors.Wrap(err, err)
}
b, err := ioutil.ReadFile(caPath)
if err != nil {
return tlsCert, caCert, err
}
caCert, err = readCert(b)
if err != nil {
return tlsCert, caCert, errors.Wrap(err, err)
}
return tlsCert, caCert, nil
}
func readCert(b []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(b)
if block == nil {
return nil, errors.New("failed to decode PEM data")
}
return x509.ParseCertificate(block.Bytes)
}
+25 -23
View File
@@ -1,54 +1,56 @@
package cli
import (
"errors"
"strconv"
"github.com/spf13/cobra"
)
var cmdCerts = []cobra.Command{
cobra.Command{
// NewCertsCmd returns certificate command.
func NewCertsCmd() *cobra.Command {
var keySize uint16
var keyType string
var ttl uint32
issueCmd := cobra.Command{
Use: "issue",
Short: "issue <thing_id> <keybits> <keytype> <hoursvalid> <user_auth_token>",
Short: "issue <thing_id> <user_auth_token> [--keysize=2048] [--keytype=rsa] [--ttl=8760]",
Long: `Issues new certificate for a thing`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 5 {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
thingID := args[0]
keyBits, err := strconv.Atoi(args[1])
if err != nil {
logError(errors.New("invalid format for keybits"))
return
}
valid := strconv.FormatUint(uint64(ttl), 10)
keyType := args[2]
valid := args[3]
token := args[4]
c, err := sdk.IssueCert(thingID, keyBits, keyType, valid, token)
c, err := sdk.IssueCert(thingID, int(keySize), keyType, valid, args[1])
if err != nil {
logError(err)
return
}
logJSON(c)
},
},
}
}
issueCmd.Flags().Uint16Var(&keySize, "keysize", 2048, "certificate key strength in bits: 2048, 4096 (RSA) or 224, 256, 384, 512 (EC)")
issueCmd.Flags().StringVar(&keyType, "keytype", "rsa", "certificate key type: RSA or EC")
issueCmd.Flags().Uint32Var(&ttl, "ttl", 8760, "certificate time to live in hours")
// NewCertsCmd returns certificate command.
func NewCertsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "cert",
Short: "Certificate management",
Long: `Certificate management: create certificates for things"`,
Use: "certs",
Short: "Certificates management",
Long: `Certificates management: create certificates for things"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("cert issue <thing_id> <keybits> <keytype> <hoursvalid> <user_auth_token>")
logUsage("certs [issue]")
},
}
cmdCerts := []cobra.Command{
issueCmd,
}
for i := range cmdCerts {
cmd.AddCommand(&cmdCerts[i])
}
+96
View File
@@ -0,0 +1,96 @@
package cli
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/pelletier/go-toml"
)
type Config struct {
Offset uint `toml:"offset"`
Limit uint `toml:"limit"`
Name string `toml:"name"`
RawOutput bool `toml:"raw_output"`
}
// save - store config in a file
func save(c Config, file string) error {
b, err := toml.Marshal(c)
if err != nil {
return errors.New(fmt.Sprintf("failed to read config file: %s", err))
}
if err := ioutil.WriteFile(file, b, 0644); err != nil {
return errors.New(fmt.Sprintf("failed to write config TOML: %s", err))
}
return nil
}
// read - retrieve config from a file
func read(file string) (Config, error) {
data, err := ioutil.ReadFile(file)
c := Config{}
if err != nil {
return c, errors.New(fmt.Sprintf("failed to read config file: %s", err))
}
if err := toml.Unmarshal(data, &c); err != nil {
return Config{}, errors.New(fmt.Sprintf("failed to unmarshal config TOML: %s", err))
}
return c, nil
}
func getConfigPath() (string, error) {
// Check if a config path passed by user exists.
if ConfigPath != "" {
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
errConfigNotFound := errors.Wrap(errors.New("config file was not found"), err)
logError(errConfigNotFound)
return "", err
}
}
// If not, then read it from the user config directory.
if ConfigPath == "" {
userConfigDir, _ := os.UserConfigDir()
ConfigPath = path.Join(userConfigDir, "mainflux", "cli.toml")
}
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
return "", err
}
return ConfigPath, nil
}
func ParseConfig() {
path, err := getConfigPath()
if err != nil {
return
}
config, err := read(path)
if err != nil {
log.Fatal(err)
}
if config.Offset != 0 {
Offset = config.Offset
}
if config.Limit != 0 {
Limit = config.Limit
}
if config.Name != "" {
Name = config.Name
}
if config.RawOutput {
RawOutput = config.RawOutput
}
}
+53 -16
View File
@@ -45,7 +45,7 @@ var cmdGroups = []cobra.Command{
},
cobra.Command{
Use: "get",
Short: "get [all | children <group_id> | group_id] <user_auth_token>",
Short: "get [all | children <group_id> | parents <group_id> | group_id] <user_auth_token>",
Long: `Get all users groups, group children or group by id.
all - lists all groups
children <group_id> - lists all children groups of <group_id>
@@ -56,7 +56,11 @@ var cmdGroups = []cobra.Command{
return
}
if args[0] == "all" {
l, err := sdk.Groups(args[1], uint64(Offset), uint64(Limit), "")
if len(args) > 2 {
logUsage(cmd.Short)
return
}
l, err := sdk.Groups(uint64(Offset), uint64(Limit), args[1])
if err != nil {
logError(err)
return
@@ -65,7 +69,11 @@ var cmdGroups = []cobra.Command{
return
}
if args[0] == "children" {
l, err := sdk.Groups(args[2], uint64(Offset), uint64(Limit), args[1])
if len(args) > 3 {
logUsage(cmd.Short)
return
}
l, err := sdk.Children(args[1], uint64(Offset), uint64(Limit), args[2])
if err != nil {
logError(err)
return
@@ -73,6 +81,23 @@ var cmdGroups = []cobra.Command{
logJSON(l)
return
}
if args[0] == "parents" {
if len(args) > 3 {
logUsage(cmd.Short)
return
}
l, err := sdk.Parents(args[1], uint64(Offset), uint64(Limit), args[2])
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
if len(args) > 2 {
logUsage(cmd.Short)
return
}
t, err := sdk.Group(args[0], args[1])
if err != nil {
logError(err)
@@ -83,14 +108,20 @@ var cmdGroups = []cobra.Command{
},
cobra.Command{
Use: "assign",
Short: "assign <user_id> <group_id> <user_auth_token>",
Long: `Assign user to a group.`,
Short: "assign <member_ids> <member_type> <group_id> <user_auth_token>",
Long: `Assign members to a group.
member_ids - '["member_id",...]`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
if len(args) != 4 {
logUsage(cmd.Short)
return
}
if err := sdk.Assign(args[0], args[1], args[2]); err != nil {
var ids []string
if err := json.Unmarshal([]byte(args[0]), &ids); err != nil {
logError(err)
return
}
if err := sdk.Assign(ids, args[1], args[2], args[3]); err != nil {
logError(err)
return
}
@@ -99,14 +130,20 @@ var cmdGroups = []cobra.Command{
},
cobra.Command{
Use: "unassign",
Short: "unassign <user_id> <group_id> <user_auth_token>",
Long: `Unassign user from a group.`,
Short: "unassign <member_ids> <group_id> <user_auth_token>",
Long: `Unassign members from a group
member_ids - '["member_id",...]`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.Unassign(args[0], args[1], args[2]); err != nil {
var ids []string
if err := json.Unmarshal([]byte(args[0]), &ids); err != nil {
logError(err)
return
}
if err := sdk.Unassign(args[2], args[1], ids...); err != nil {
logError(err)
return
}
@@ -116,7 +153,7 @@ var cmdGroups = []cobra.Command{
cobra.Command{
Use: "delete",
Short: "delete <group_id> <user_auth_token>",
Long: `Delete users group.`,
Long: `Delete group.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
@@ -132,7 +169,7 @@ var cmdGroups = []cobra.Command{
cobra.Command{
Use: "members",
Short: "members <group_id> <user_auth_token>",
Long: `Lists all user members of a group.`,
Long: `Lists all members of a group.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
@@ -148,8 +185,8 @@ var cmdGroups = []cobra.Command{
},
cobra.Command{
Use: "membership",
Short: "membership <user_id> <user_auth_token>",
Long: `List user groups membership`,
Short: "membership <member_id> <user_auth_token>",
Long: `List member group's membership`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
@@ -170,9 +207,9 @@ func NewGroupsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "groups",
Short: "Groups management",
Long: `Groups management: create groups and assigns user to groups"`,
Long: `Groups management: create groups and assigns member to groups"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("Usage: Groups [create | get | delete | assign | unassign | members | membership]")
logUsage("groups [create | get | delete | assign | unassign | members | membership]")
},
}
for i := range cmdGroups {
+22 -15
View File
@@ -10,8 +10,9 @@ import (
"github.com/spf13/cobra"
)
var cmdUsers = []cobra.Command{
cobra.Command{
// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
createCmd := cobra.Command{
Use: "create",
Short: "create <username> <password>",
Long: `Creates new user`,
@@ -33,8 +34,9 @@ var cmdUsers = []cobra.Command{
logCreated(id)
},
},
cobra.Command{
}
getCmd := cobra.Command{
Use: "get",
Short: "get <user_auth_token>",
Long: `Returns user object`,
@@ -52,8 +54,9 @@ var cmdUsers = []cobra.Command{
logJSON(u)
},
},
cobra.Command{
}
tokenCmd := cobra.Command{
Use: "token",
Short: "token <username> <password>",
Long: `Creates new token`,
@@ -74,9 +77,11 @@ var cmdUsers = []cobra.Command{
}
logCreated(token)
},
},
cobra.Command{
}
updateCmd := cobra.Command{
Use: "update",
Short: "update <JSON_string> <user_auth_token>",
Long: `Update user metadata`,
@@ -99,8 +104,9 @@ var cmdUsers = []cobra.Command{
logOK()
},
},
cobra.Command{
}
passwordCmd := cobra.Command{
Use: "password",
Short: "password <old_password> <password> <user_auth_token>",
Long: `Update user password`,
@@ -117,20 +123,21 @@ var cmdUsers = []cobra.Command{
logOK()
},
},
}
}
// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
cmd := cobra.Command{
Use: "users",
Short: "Users management",
Long: `Users management: create accounts and tokens"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("Usage: users [create | get | update | token | password]")
logUsage("users [create | get | update | token | password]")
},
}
cmdUsers := []cobra.Command{
createCmd, getCmd, tokenCmd, updateCmd, passwordCmd,
}
for i := range cmdUsers {
cmd.AddCommand(&cmdUsers[i])
}
+13 -2
View File
@@ -18,6 +18,10 @@ var (
Offset uint = 0
// Name query parameter
Name string = ""
// ConfigPath config path parameter
ConfigPath string = ""
// RawOutput raw output mode
RawOutput bool = false
)
func logJSON(iList ...interface{}) {
@@ -43,7 +47,10 @@ func logUsage(u string) {
}
func logError(err error) {
fmt.Printf("\n%s\n\n", color.RedString(err.Error()))
boldRed := color.New(color.FgRed, color.Bold)
boldRed.Print("\nerror: ")
fmt.Printf("%s\n\n", color.RedString(err.Error()))
}
func logOK() {
@@ -51,5 +58,9 @@ func logOK() {
}
func logCreated(e string) {
fmt.Printf(color.BlueString("\ncreated: %s\n\n"), e)
if RawOutput {
fmt.Println(e)
} else {
fmt.Printf(color.BlueString("\ncreated: %s\n\n"), e)
}
}

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