mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
MF-1506 - Group-based Access Control (#1716)
* Move Things and Users to Clients Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * NOISSUE - Update Add and Delete Policies (#1792) * Remove Policy Action Ranks Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Rebase Issues Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix CI Test Errors Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Adding Check on Subject For Clients Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Remove Check Client Exists Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Check When Sharing Clients Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Only Add User to Group When Sharing Things Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Remove clientType Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Minor Fix on ShareClient and Fix Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Policies Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Clean Up Things Authorization Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Tests on RetrieveAll Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Test ShareThing Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Merge Conflicts Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Remove Adding Policies. Only Use Ownership Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Check If Subject is same as Object Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Move Back To Union As Sometimes Policy is Empty and Fails to Evaluate on Ownership Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Entity Type For Failing Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix BUG in policy evaluation Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Comments Regarding checkAdmin Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Tests On Rebase Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Combine Authorize For Things and Users Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Tests On Rebase Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Error on Things SVC `unsupported protocol scheme` Signed-off-by: rodneyosodo <blackd0t@protonmail.com> --------- Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Fix Bug on Things Authorization Cache (#1810) Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Use Password instead of username in MQTT handler Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Simplify MQTT authorization Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Fix MQTT tests Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * NOISSUE - Add More Functions to SDK (#1811) * Add More Functions to SDK Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Examples to GoDoc Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Update Unassign Interface Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Pass Subject as ID and Not Token on List Channels By Thing Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Bootstrap Errors For Element Check Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add empty line Before Return Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Reorder URLS in things mux Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Listing Things Policies Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Share Thing Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Examples to CLI Docs Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Update Identity To Update Another User Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Identify an Update Policies on Things Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Update Things Policies Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix GoDocs on Disconnect Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Change Authorize To Use AccessRequest Signed-off-by: rodneyosodo <blackd0t@protonmail.com> --------- Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * For Evaluate Policy Use AccessRequest (#1814) Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * NOISSUE - Add SDK Tests (#1812) * Add Things Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Channel Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Certs Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Consumer Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Enrich Group Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Tests For Health Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Tests For Tokens Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Rename SDK for Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Policies Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Linter Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Fix Tests Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Make Variable Defination Inline Signed-off-by: rodneyosodo <blackd0t@protonmail.com> --------- Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * NOISSUE - Make Cache Key Duration Configurable (#1815) * Make Cache Key Duration Configurable Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Rename ENV Var Signed-off-by: rodneyosodo <blackd0t@protonmail.com> --------- Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * NOISSUE - Update GoDocs (#1816) * Add GoDocs Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add Missing GoDoc Files Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Enable godot Signed-off-by: rodneyosodo <blackd0t@protonmail.com> * Add License Information Signed-off-by: rodneyosodo <blackd0t@protonmail.com> --------- Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * NOISSUE - Add Call Home Client to Mainflux services (#1751) * Move Things and Users to Clients Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: SammyOina <sammyoina@gmail.com> * collect and send data package Signed-off-by: SammyOina <sammyoina@gmail.com> * create telemetry migrations Signed-off-by: SammyOina <sammyoina@gmail.com> * add telemetry endpoints Signed-off-by: SammyOina <sammyoina@gmail.com> * add transport Signed-off-by: SammyOina <sammyoina@gmail.com> * create service Signed-off-by: SammyOina <sammyoina@gmail.com> * remove homing server Signed-off-by: SammyOina <sammyoina@gmail.com> * add call home to adapters Signed-off-by: SammyOina <sammyoina@gmail.com> * add last seen Signed-off-by: SammyOina <sammyoina@gmail.com> * rename logger Signed-off-by: SammyOina <sammyoina@gmail.com> * remove homing client Signed-off-by: SammyOina <sammyoina@gmail.com> * use unmerged repo Signed-off-by: SammyOina <sammyoina@gmail.com> * use renamed module Signed-off-by: SammyOina <sammyoina@gmail.com> * update call home version Signed-off-by: SammyOina <sammyoina@gmail.com> * edit documentation Signed-off-by: SammyOina <sammyoina@gmail.com> * align table Signed-off-by: SammyOina <sammyoina@gmail.com> * use alias for call home client Signed-off-by: SammyOina <sammyoina@gmail.com> * update callhome Signed-off-by: SammyOina <sammyoina@gmail.com> * update call home pkg Signed-off-by: SammyOina <sammyoina@gmail.com> * update call home Signed-off-by: SammyOina <sammyoina@gmail.com> * fix modules Signed-off-by: SammyOina <sammyoina@gmail.com> * use mf build version Signed-off-by: SammyOina <sammyoina@gmail.com> * use mf build version Signed-off-by: SammyOina <sammyoina@gmail.com> * restore default Signed-off-by: SammyOina <sammyoina@gmail.com> * add call home for users and things Signed-off-by: SammyOina <sammyoina@gmail.com> * enable opting on call home Signed-off-by: SammyOina <sammyoina@gmail.com> * remove full stops Signed-off-by: SammyOina <sammyoina@gmail.com> * update callhome client Signed-off-by: SammyOina <sammyoina@gmail.com> * add call home to all services Signed-off-by: SammyOina <sammyoina@gmail.com> * fix build Signed-off-by: SammyOina <sammyoina@gmail.com> * restore sdk tests Signed-off-by: SammyOina <sammyoina@gmail.com> * remove unnecessary changes Signed-off-by: SammyOina <sammyoina@gmail.com> * restore health_test.go Signed-off-by: SammyOina <sammyoina@gmail.com> --------- Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: SammyOina <sammyoina@gmail.com> Co-authored-by: b1ackd0t <blackd0t@protonmail.com> Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> --------- Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> Signed-off-by: rodneyosodo <blackd0t@protonmail.com> Signed-off-by: SammyOina <sammyoina@gmail.com> Co-authored-by: b1ackd0t <blackd0t@protonmail.com> Co-authored-by: Sammy Kerata Oina <44265300+SammyOina@users.noreply.github.com>
This commit is contained in:
@@ -5,3 +5,9 @@
|
||||
# https://digitalfortress.tech/tricks/creating-a-global-gitignore/
|
||||
|
||||
build
|
||||
|
||||
# tools
|
||||
tools/e2e/e2e
|
||||
tools/mqtt-bench/mqtt-bench
|
||||
tools/provision/provision
|
||||
tools/provision/mfconn.toml
|
||||
|
||||
@@ -5,7 +5,7 @@ MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http coap ws lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader timescale-writer timescale-reader cli \
|
||||
bootstrap opcua auth twins mqtt provision certs smtp-notifier smpp-notifier
|
||||
bootstrap opcua twins mqtt provision certs smtp-notifier smpp-notifier
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
@@ -78,7 +78,8 @@ test:
|
||||
|
||||
proto:
|
||||
protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto
|
||||
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
|
||||
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative users/policies/*.proto
|
||||
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative things/policies/*.proto
|
||||
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
|
||||
@@ -23,7 +23,7 @@ For more details, check out the [official documentation][docs].
|
||||
- Mutual TLS Authentication (mTLS) using X.509 Certificates
|
||||
- Fine-grained access control (policies, ABAC/RBAC)
|
||||
- Message persistence (Cassandra, InfluxDB, MongoDB and PostgresSQL)
|
||||
- Platform logging and instrumentation support (Prometheus and OpenTracing)
|
||||
- Platform logging and instrumentation support (Prometheus and OpenTelemetry)
|
||||
- Event sourcing
|
||||
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
|
||||
- [LoRaWAN][lora] network integration
|
||||
|
||||
@@ -1,803 +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:
|
||||
- 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"
|
||||
|
||||
get:
|
||||
summary: Lists API key
|
||||
description: |
|
||||
List the API keys issued by the logged in user.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Subject"
|
||||
- $ref: "#/components/parameters/Type"
|
||||
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/{keyID}:
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
'204':
|
||||
description: Key revoked.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups:
|
||||
post:
|
||||
summary: Creates new group
|
||||
description: |
|
||||
Creates new group that can be used for grouping entities - things, users.
|
||||
tags:
|
||||
- auth
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/GroupCreateReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets all groups.
|
||||
description: |
|
||||
Gets all groups up to a max level of hierarchy that can be fetched in one
|
||||
request ( max level = 5). Result can be filtered by metadata. Groups will
|
||||
be returned as JSON array or JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupID}:
|
||||
get:
|
||||
summary: Gets group info.
|
||||
description: |
|
||||
Gets info on a group specified by id.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates group data.
|
||||
description: |
|
||||
Updates Name, Description or Metadata of a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/GroupUpdateReq"
|
||||
responses:
|
||||
'200':
|
||||
description: Group updated.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Deletes group.
|
||||
description: |
|
||||
Deletes group. If group is parent and descendant groups do not have any members
|
||||
child groups will be deleted. Group cannot be deleted if has members or if
|
||||
any descendant group has members.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'204':
|
||||
description: Group removed.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupID}/children:
|
||||
get:
|
||||
summary: Gets group children.
|
||||
description: |
|
||||
Gets the whole tree of descendants of group for given id including itself.
|
||||
For performance reason request is limited up to a given level of hierarchy
|
||||
(max. 5).
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupID}/parents:
|
||||
get:
|
||||
summary: Gets group info.
|
||||
description: |
|
||||
Gets a direct line of ancestors for a group specified by id.
|
||||
Result is up to a specified hierarchy level or up to a root group.
|
||||
Result can be a JSON array or a JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupID}/members:
|
||||
get:
|
||||
summary: Gets members of a group.
|
||||
description: |
|
||||
Array of member ids that are in the group specified with groupID.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/MembersRes"
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupID}/members/assign:
|
||||
post:
|
||||
summary: Assigns members to a group.
|
||||
description: |
|
||||
Assigns thing or user id to a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MembersReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupID}/members/unassign:
|
||||
post:
|
||||
summary: Unassigns members to a group.
|
||||
description: |
|
||||
Unassigns thing or user id to a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MembersReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{userGroupID}/share:
|
||||
post:
|
||||
summary: Adds access rights on thing groups to user group with userGroupID.
|
||||
description: |
|
||||
Takes user group id through parameter and adds access rights for user group on thing group received via request body.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/UserGroupID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ShareGroupAccessReq"
|
||||
responses:
|
||||
'200':
|
||||
description: User group shared with thing group.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/members/{memberID}/groups:
|
||||
get:
|
||||
summary: Gets memberships for a member with member id.
|
||||
description: |
|
||||
Array of groups that member belongs to.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/MemberId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupRes"
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/policies:
|
||||
post:
|
||||
summary: Creates new policies.
|
||||
description: |
|
||||
Creates new policies. Only admin can use this endpoint. Therefore, you need an authentication token for the admin.
|
||||
Also, only policies defined on the system are allowed to add. For more details, please see the docs for Authorization.
|
||||
tags:
|
||||
- auth
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/PoliciesReq"
|
||||
responses:
|
||||
'201':
|
||||
description: Policies created.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'403':
|
||||
description: Unauthorized access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Deletes policies.
|
||||
description: |
|
||||
Deletes policies. Only admin can use this endpoint. Therefore, you need an authentication token for the admin.
|
||||
Also, only policies defined on the system are allowed to delete. For more details, please see the docs for Authorization.
|
||||
tags:
|
||||
- auth
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/PoliciesReq"
|
||||
responses:
|
||||
'204':
|
||||
description: Policies deleted.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
issuer_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "9118de62-c680-46b7-ad0a-21748a52833a"
|
||||
description: In ID of the entity that issued the token.
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
subject:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key subject.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated.
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires. If this field is missing,
|
||||
that means that Key is valid indefinitely.
|
||||
GroupReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
parent_id:
|
||||
type: string
|
||||
format: ulid
|
||||
description: Id of parent group, it must be existing group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
GroupUpdateSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
GroupResSchema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
description: Unique group identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form group name.
|
||||
parent_id:
|
||||
type: string
|
||||
description: Group ID of parent group.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of user that created the group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
level:
|
||||
type: integer
|
||||
description: Level in hierarchy, distance from the root group.
|
||||
path:
|
||||
type: string
|
||||
description: Hierarchy path, concatenated ids of group ancestors.
|
||||
children:
|
||||
type: object
|
||||
# schema: GroupResSchema
|
||||
created_at:
|
||||
type: string
|
||||
description: Datetime of group creation.
|
||||
updated_at:
|
||||
type: string
|
||||
description: Datetime of last group updated.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- owner_id
|
||||
- description
|
||||
- level
|
||||
- path
|
||||
- created_at
|
||||
- updated_at
|
||||
MembersReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
members:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
format: uuid | ulid
|
||||
type:
|
||||
type: string
|
||||
description: Type of entity
|
||||
ShareGroupAccessReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
thing_group_id:
|
||||
type: string
|
||||
description: Group ID of the Thing Group.
|
||||
format: uuid
|
||||
GroupsPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
level:
|
||||
type: integer
|
||||
description: Level of hierarchy up to which groups are fetched.
|
||||
required:
|
||||
- groups
|
||||
- total
|
||||
- level
|
||||
MembershipPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
required:
|
||||
- groups
|
||||
PoliciesReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
object:
|
||||
type: string
|
||||
description: |
|
||||
Specifies an object field for the field.
|
||||
Object indicates application objects such as ThingID.
|
||||
subjects:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
policies:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
|
||||
parameters:
|
||||
ApiKeyId:
|
||||
name: keyID
|
||||
description: API Key ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
UserGroupID:
|
||||
name: userGroupID
|
||||
description: User Group ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupID
|
||||
description: Group ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
MemberId:
|
||||
name: memberID
|
||||
description: Member id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid | ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Level:
|
||||
name: level
|
||||
description: Level of hierarchy up to which to retrieve groups from given group id.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 5
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
Tree:
|
||||
name: tree
|
||||
description: Specify type of response, JSON array or tree.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
Type:
|
||||
name: type
|
||||
description: The type of the API Key.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Subject:
|
||||
name: subject
|
||||
description: The subject of an API Key
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
GroupCreateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupReqSchema"
|
||||
GroupUpdateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupUpdateSchema"
|
||||
MembersReq:
|
||||
description: JSON array of member IDs.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembersReqSchema"
|
||||
ShareGroupAccessReq:
|
||||
description: test
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareGroupAccessReqSchema"
|
||||
PoliciesReq:
|
||||
description: JSON-formatted document describing adding policies request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PoliciesReqSchema"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
GroupCreateRes:
|
||||
description: Group created.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created group's relative URL.
|
||||
example: /groups/{groupId}
|
||||
ShareAccessRightRes:
|
||||
description: User group shared with thing group.
|
||||
GroupRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
GroupsPageRes:
|
||||
description: Group data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupsPage"
|
||||
MembersRes:
|
||||
description: Groups data retrieved. Groups assigned to a member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
MembershipPageRes:
|
||||
description: Groups data retrieved. Groups assigned to a member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -49,7 +49,7 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/{configID}:
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
@@ -108,7 +108,7 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/certs/{configID}:
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
summary: Updates certs
|
||||
description: |
|
||||
@@ -133,7 +133,7 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/connections/{configID}:
|
||||
/things/configs/connections/{configId}:
|
||||
put:
|
||||
summary: Updates channels the thing is connected to
|
||||
description: |
|
||||
@@ -158,7 +158,7 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/{externalID}:
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
@@ -180,7 +180,7 @@ paths:
|
||||
description: Failed to retrieve corresponding config.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalID}:
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
@@ -199,7 +199,7 @@ paths:
|
||||
Failed to retrieve corresponding config.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/state/{configID}:
|
||||
/things/state/{configId}:
|
||||
put:
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
@@ -338,7 +338,7 @@ components:
|
||||
|
||||
parameters:
|
||||
ConfigId:
|
||||
name: configID
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
schema:
|
||||
@@ -346,7 +346,7 @@ components:
|
||||
format: uuid
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalID
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
|
||||
@@ -135,6 +135,12 @@ components:
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Serial:
|
||||
type: object
|
||||
properties:
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
CertsPage:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -42,7 +42,7 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/subscriptions/{subID}:
|
||||
/subscriptions/{id}:
|
||||
get:
|
||||
summary: Get subscription with the provided id
|
||||
description: Retrieves a subscription with the provided id.
|
||||
@@ -137,7 +137,7 @@ components:
|
||||
|
||||
parameters:
|
||||
Id:
|
||||
name: subID
|
||||
name: id
|
||||
description: Unique identifier.
|
||||
in: path
|
||||
schema:
|
||||
@@ -197,7 +197,7 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
description: Created subscription relative URL
|
||||
example: /subscriptions/{subId}
|
||||
example: /subscriptions/{id}
|
||||
View:
|
||||
description: View subscription.
|
||||
content:
|
||||
|
||||
@@ -4,7 +4,7 @@ info:
|
||||
description: HTTP API for sending messages through communication channels.
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/channels/{chanID}/messages:
|
||||
/channels/{id}/messages:
|
||||
post:
|
||||
summary: Sends message to the communication channel
|
||||
description: |
|
||||
@@ -106,7 +106,7 @@ components:
|
||||
|
||||
parameters:
|
||||
ID:
|
||||
name: chanID
|
||||
name: id
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
|
||||
@@ -5,7 +5,7 @@ info:
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/channels/{chanID}/messages:
|
||||
/channels/{chanId}/messages:
|
||||
get:
|
||||
summary: Retrieves messages sent to single channel
|
||||
description: |
|
||||
@@ -107,7 +107,7 @@ components:
|
||||
|
||||
parameters:
|
||||
ChanId:
|
||||
name: chanID
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
|
||||
+1429
-524
File diff suppressed because it is too large
Load Diff
+1663
-223
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux ws adapter
|
||||
description: WebSocket API for sending messages through communication channels.
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/channels/{id}/messages:
|
||||
post:
|
||||
summary: Sends message to the communication channel
|
||||
description: |
|
||||
Sends message to the communication channel. Messages can be sent as
|
||||
JSON formatted SenML or as blob.
|
||||
tags:
|
||||
- messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MessageReq"
|
||||
responses:
|
||||
"202":
|
||||
description: Message is accepted for processing.
|
||||
"400":
|
||||
description: Message discarded due to its malformed content.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Message discarded due to invalid channel id.
|
||||
"415":
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
-1571
File diff suppressed because it is too large
Load Diff
-131
@@ -1,131 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package mainflux;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
option go_package = "./mainflux";
|
||||
|
||||
service ThingsService {
|
||||
rpc CanAccessByKey(AccessByKeyReq) returns (ThingID) {}
|
||||
rpc IsChannelOwner(ChannelOwnerReq) returns (google.protobuf.Empty) {}
|
||||
rpc CanAccessByID(AccessByIDReq) returns (google.protobuf.Empty) {}
|
||||
rpc Identify(Token) returns (ThingID) {}
|
||||
}
|
||||
|
||||
service AuthService {
|
||||
rpc Issue(IssueReq) returns (Token) {}
|
||||
rpc Identify(Token) returns (UserIdentity) {}
|
||||
rpc Authorize(AuthorizeReq) returns (AuthorizeRes) {}
|
||||
rpc AddPolicy(AddPolicyReq) returns (AddPolicyRes) {}
|
||||
rpc DeletePolicy(DeletePolicyReq) returns (DeletePolicyRes) {}
|
||||
rpc ListPolicies(ListPoliciesReq) returns (ListPoliciesRes) {}
|
||||
rpc Assign(Assignment) returns(google.protobuf.Empty) {}
|
||||
rpc Members(MembersReq) returns (MembersRes) {}
|
||||
}
|
||||
|
||||
message AccessByKeyReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ChannelOwnerReq {
|
||||
string owner = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message ThingID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message ChannelID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message AccessByIDReq {
|
||||
string thingID = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
// If a token is not carrying any information itself, the type
|
||||
// field can be used to determine how to validate the token.
|
||||
// Also, different tokens can be encoded in different ways.
|
||||
message Token {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message UserIdentity {
|
||||
string id = 1;
|
||||
string email = 2;
|
||||
}
|
||||
|
||||
message IssueReq {
|
||||
string id = 1;
|
||||
string email = 2;
|
||||
uint32 type = 3;
|
||||
}
|
||||
|
||||
message AuthorizeReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message AuthorizeRes {
|
||||
bool authorized = 1;
|
||||
}
|
||||
|
||||
message AddPolicyReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message AddPolicyRes {
|
||||
bool authorized = 1;
|
||||
}
|
||||
|
||||
message DeletePolicyReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message DeletePolicyRes {
|
||||
bool deleted = 1;
|
||||
}
|
||||
|
||||
message ListPoliciesReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message ListPoliciesRes {
|
||||
repeated string policies = 1;
|
||||
}
|
||||
|
||||
message Assignment {
|
||||
string token = 1;
|
||||
string groupID = 2;
|
||||
string memberID = 3;
|
||||
}
|
||||
|
||||
message MembersReq {
|
||||
string token = 1;
|
||||
string groupID = 2;
|
||||
uint64 offset = 3;
|
||||
uint64 limit = 4;
|
||||
string type = 5;
|
||||
}
|
||||
|
||||
message MembersRes {
|
||||
uint64 total = 1;
|
||||
uint64 offset = 2;
|
||||
uint64 limit = 3;
|
||||
string type = 4;
|
||||
repeated string members = 5;
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
# 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) | info |
|
||||
| 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 | 9020 |
|
||||
| MF_AUTH_GRPC_PORT | Auth service gRPC port | 7001 |
|
||||
| MF_AUTH_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTH_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTH_SECRET | String used for signing tokens | auth |
|
||||
| MF_AUTH_LOGIN_TOKEN_DURATION | The login token expiration period | 10h |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_KETO_READ_REMOTE_HOST | Keto Read Host | mainflux-keto |
|
||||
| MF_KETO_WRITE_REMOTE_HOST | Keto Write Host | mainflux-keto |
|
||||
| MF_KETO_READ_REMOTE_PORT | Keto Read Port | 4466 |
|
||||
| MF_KETO_WRITE_REMOTE_PORT | Keto Write Port | 4467 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`auth`](https://github.com/mainflux/mainflux/blob/master/docker/docker-compose.yml#L71-L94) service section in
|
||||
docker-compose to see how service is deployed.
|
||||
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the service
|
||||
make auth
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_AUTH_LOG_LEVEL=[Service log level] MF_AUTH_DB_HOST=[Database host address] MF_AUTH_DB_PORT=[Database host port] MF_AUTH_DB_USER=[Database user] MF_AUTH_DB_PASS=[Database password] MF_AUTH_DB=[Name of the database used by the service] MF_AUTH_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTH_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTH_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTH_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTH_HTTP_PORT=[Service HTTP port] MF_AUTH_GRPC_PORT=[Service gRPC port] MF_AUTH_SECRET=[String used for signing tokens] MF_AUTH_SERVER_CERT=[Path to server certificate] MF_AUTH_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] MF_AUTH_LOGIN_TOKEN_DURATION=[The login token expiration period] $GOBIN/mainflux-auth
|
||||
```
|
||||
|
||||
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](https://api.mainflux.io/?urls.primaryName=auth-openapi.yml).
|
||||
|
||||
[doc]: https://docs.mainflux.io
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of Auth service HTTP API.
|
||||
package api
|
||||
@@ -1,334 +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/golang/protobuf/ptypes/empty"
|
||||
"github.com/mainflux/mainflux"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "mainflux.AuthService"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
addPolicy endpoint.Endpoint
|
||||
deletePolicy endpoint.Endpoint
|
||||
listPolicies endpoint.Endpoint
|
||||
assign endpoint.Endpoint
|
||||
members endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) mainflux.AuthServiceClient {
|
||||
return &grpcClient{
|
||||
issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Issue",
|
||||
encodeIssueRequest,
|
||||
decodeIssueResponse,
|
||||
mainflux.UserIdentity{},
|
||||
).Endpoint()),
|
||||
identify: kitot.TraceClient(tracer, "identify")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Identify",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
mainflux.UserIdentity{},
|
||||
).Endpoint()),
|
||||
authorize: kitot.TraceClient(tracer, "authorize")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Authorize",
|
||||
encodeAuthorizeRequest,
|
||||
decodeAuthorizeResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
).Endpoint()),
|
||||
addPolicy: kitot.TraceClient(tracer, "add_policy")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"AddPolicy",
|
||||
encodeAddPolicyRequest,
|
||||
decodeAddPolicyResponse,
|
||||
mainflux.AddPolicyRes{},
|
||||
).Endpoint()),
|
||||
deletePolicy: kitot.TraceClient(tracer, "delete_policy")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"DeletePolicy",
|
||||
encodeDeletePolicyRequest,
|
||||
decodeDeletePolicyResponse,
|
||||
mainflux.DeletePolicyRes{},
|
||||
).Endpoint()),
|
||||
listPolicies: kitot.TraceClient(tracer, "list_policies")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"ListPolicies",
|
||||
encodeListPoliciesRequest,
|
||||
decodeListPoliciesResponse,
|
||||
mainflux.ListPoliciesRes{},
|
||||
).Endpoint()),
|
||||
assign: kitot.TraceClient(tracer, "assign")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Assign",
|
||||
encodeAssignRequest,
|
||||
decodeAssignResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
).Endpoint()),
|
||||
members: kitot.TraceClient(tracer, "members")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Members",
|
||||
encodeMembersRequest,
|
||||
decodeMembersResponse,
|
||||
mainflux.MembersRes{},
|
||||
).Endpoint()),
|
||||
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (client grpcClient) Issue(ctx context.Context, req *mainflux.IssueReq, _ ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.issue(ctx, issueReq{id: req.GetId(), email: req.GetEmail(), keyType: req.Type})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.Token{Value: ir.id}, nil
|
||||
}
|
||||
|
||||
func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &mainflux.IssueReq{Id: req.id, Email: req.email, Type: req.keyType}, nil
|
||||
}
|
||||
|
||||
func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserIdentity)
|
||||
return identityRes{id: res.GetId(), email: res.GetEmail()}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.UserIdentity, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.identify(ctx, identityReq{token: token.GetValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.UserIdentity{Id: ir.id, Email: ir.email}, nil
|
||||
}
|
||||
|
||||
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(identityReq)
|
||||
return &mainflux.Token{Value: req.token}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.UserIdentity)
|
||||
return identityRes{id: res.GetId(), email: res.GetEmail()}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.authorize(ctx, authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.AuthorizeRes{}, err
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
return &mainflux.AuthorizeRes{Authorized: ar.authorized}, err
|
||||
}
|
||||
|
||||
func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AuthorizeRes)
|
||||
return authorizeRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &mainflux.AuthorizeReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) AddPolicy(ctx context.Context, in *mainflux.AddPolicyReq, opts ...grpc.CallOption) (*mainflux.AddPolicyRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.addPolicy(ctx, policyReq{Act: in.GetAct(), Obj: in.GetObj(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.AddPolicyRes{}, err
|
||||
}
|
||||
|
||||
apr := res.(addPolicyRes)
|
||||
return &mainflux.AddPolicyRes{Authorized: apr.authorized}, err
|
||||
}
|
||||
|
||||
func decodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AddPolicyRes)
|
||||
return addPolicyRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAddPolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(policyReq)
|
||||
return &mainflux.AddPolicyReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) DeletePolicy(ctx context.Context, in *mainflux.DeletePolicyReq, opts ...grpc.CallOption) (*mainflux.DeletePolicyRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.deletePolicy(ctx, policyReq{Act: in.GetAct(), Obj: in.GetObj(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.DeletePolicyRes{}, err
|
||||
}
|
||||
|
||||
dpr := res.(deletePolicyRes)
|
||||
return &mainflux.DeletePolicyRes{Deleted: dpr.deleted}, err
|
||||
}
|
||||
|
||||
func decodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.DeletePolicyRes)
|
||||
return deletePolicyRes{deleted: res.GetDeleted()}, nil
|
||||
}
|
||||
|
||||
func encodeDeletePolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(policyReq)
|
||||
return &mainflux.DeletePolicyReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) ListPolicies(ctx context.Context, in *mainflux.ListPoliciesReq, opts ...grpc.CallOption) (*mainflux.ListPoliciesRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.listPolicies(ctx, listPoliciesReq{Obj: in.GetObj(), Act: in.GetAct(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.ListPoliciesRes{}, err
|
||||
}
|
||||
|
||||
lpr := res.(listPoliciesRes)
|
||||
return &mainflux.ListPoliciesRes{Policies: lpr.policies}, err
|
||||
}
|
||||
|
||||
func decodeListPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.ListPoliciesRes)
|
||||
return listPoliciesRes{policies: res.GetPolicies()}, nil
|
||||
}
|
||||
|
||||
func encodeListPoliciesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(listPoliciesReq)
|
||||
return &mainflux.ListPoliciesReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.members(ctx, membersReq{
|
||||
token: req.GetToken(),
|
||||
groupID: req.GetGroupID(),
|
||||
memberType: req.GetType(),
|
||||
offset: req.GetOffset(),
|
||||
limit: req.GetLimit(),
|
||||
})
|
||||
if err != nil {
|
||||
return &mainflux.MembersRes{}, err
|
||||
}
|
||||
|
||||
mr := res.(membersRes)
|
||||
|
||||
return &mainflux.MembersRes{
|
||||
Offset: mr.offset,
|
||||
Limit: mr.limit,
|
||||
Total: mr.total,
|
||||
Type: mr.groupType,
|
||||
Members: mr.members,
|
||||
}, err
|
||||
}
|
||||
|
||||
func encodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(membersReq)
|
||||
return &mainflux.MembersReq{
|
||||
Token: req.token,
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
GroupID: req.groupID,
|
||||
Type: req.memberType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.MembersRes)
|
||||
return membersRes{
|
||||
offset: res.Offset,
|
||||
limit: res.Limit,
|
||||
total: res.Total,
|
||||
members: res.Members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Assign(ctx context.Context, req *mainflux.Assignment, _ ...grpc.CallOption) (r *empty.Empty, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
_, err = client.assign(ctx, assignReq{token: req.GetToken(), groupID: req.GetGroupID(), memberID: req.GetMemberID()})
|
||||
if err != nil {
|
||||
return &empty.Empty{}, err
|
||||
}
|
||||
|
||||
return &empty.Empty{}, err
|
||||
}
|
||||
|
||||
func encodeAssignRequest(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AuthorizeRes)
|
||||
return authorizeRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAssignResponse(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &mainflux.AuthorizeReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,163 +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/auth"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(issueReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
|
||||
key := auth.Key{
|
||||
Type: req.keyType,
|
||||
Subject: req.email,
|
||||
IssuerID: req.id,
|
||||
IssuedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
_, secret, err := svc.Issue(ctx, "", key)
|
||||
if err != nil {
|
||||
return issueRes{}, err
|
||||
}
|
||||
|
||||
return issueRes{secret}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identifyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
id, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
ret := identityRes{
|
||||
id: id.ID,
|
||||
email: id.Email,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(authReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
err := svc.Authorize(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
return authorizeRes{authorized: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
func addPolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return addPolicyRes{}, err
|
||||
}
|
||||
|
||||
err := svc.AddPolicy(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return addPolicyRes{}, err
|
||||
}
|
||||
return addPolicyRes{authorized: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
func deletePolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
|
||||
err := svc.DeletePolicy(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
return deletePolicyRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listPoliciesEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listPoliciesReq)
|
||||
|
||||
page, err := svc.ListPolicies(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
return listPoliciesRes{policies: page.Policies}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(assignReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
|
||||
_, err := svc.Identify(ctx, req.token)
|
||||
if err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
|
||||
err = svc.Assign(ctx, req.token, req.memberID, req.groupID, req.groupType)
|
||||
if err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
return emptyRes{}, nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func membersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(membersReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
}
|
||||
mp, err := svc.ListMembers(ctx, req.token, req.groupID, req.memberType, pm)
|
||||
if err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
var members []string
|
||||
for _, m := range mp.Members {
|
||||
members = append(members, m.ID)
|
||||
}
|
||||
return membersRes{
|
||||
offset: req.offset,
|
||||
limit: req.limit,
|
||||
total: mp.PageMetadata.Total,
|
||||
members: members,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
grpcapi "github.com/mainflux/mainflux/auth/api/grpc"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 7001
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
thingsType = "things"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
|
||||
numOfThings = 5
|
||||
numOfUsers = 5
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
var svc auth.Service
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, err := grpc.Dial(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error while creating client connection: %s", err))
|
||||
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
email string
|
||||
kind uint32
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.LoginKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.RecoveryKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthenticated",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.APIKey,
|
||||
err: nil,
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: 32,
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
email: "",
|
||||
kind: auth.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Id: tc.id, Email: tc.email, Type: tc.kind})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.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})
|
||||
require.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})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, err := grpc.Dial(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error while creating client connection: %s", err))
|
||||
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
idt *mainflux.UserIdentity
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "identify user with user token",
|
||||
token: loginSecret,
|
||||
idt: &mainflux.UserIdentity{Email: email, Id: id},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with recovery token",
|
||||
token: recoverySecret,
|
||||
idt: &mainflux.UserIdentity{Email: email, Id: id},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with API token",
|
||||
token: apiSecret,
|
||||
idt: &mainflux.UserIdentity{Email: email, Id: id},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
idt: &mainflux.UserIdentity{},
|
||||
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user with empty token",
|
||||
token: "",
|
||||
idt: &mainflux.UserIdentity{},
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
idt, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
|
||||
}
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, err := grpc.Dial(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error while creating client connection: %s", err))
|
||||
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
ar *mainflux.AuthorizeRes
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: authoritiesObj,
|
||||
relation: memberRelation,
|
||||
ar: &mainflux.AuthorizeRes{Authorized: true},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized relation",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: authoritiesObj,
|
||||
relation: "unauthorizedRelation",
|
||||
ar: &mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized object",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: "unauthorizedobject",
|
||||
relation: memberRelation,
|
||||
ar: &mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized subject",
|
||||
token: loginSecret,
|
||||
subject: "unauthorizedSubject",
|
||||
object: authoritiesObj,
|
||||
relation: memberRelation,
|
||||
ar: &mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with invalid ACL",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
ar: &mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ar, err := client.Authorize(context.Background(), &mainflux.AuthorizeReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.ar, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.ar, ar))
|
||||
}
|
||||
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPolicy(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, err := grpc.Dial(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error while creating client connection: %s", err))
|
||||
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
groupAdminObj := "groupadmin"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
ar *mainflux.AddPolicyRes
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "add groupadmin policy to user",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: groupAdminObj,
|
||||
relation: memberRelation,
|
||||
ar: &mainflux.AddPolicyRes{Authorized: true},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "add policy to user with invalid ACL",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
ar: &mainflux.AddPolicyRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
apr, err := client.AddPolicy(context.Background(), &mainflux.AddPolicyReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
if apr != nil {
|
||||
assert.Equal(t, tc.ar, apr, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.ar, apr))
|
||||
}
|
||||
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePolicy(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, err := grpc.Dial(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error while creating client connection: %s", err))
|
||||
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
readRelation := "read"
|
||||
thingID := "thing"
|
||||
|
||||
apr, err := client.AddPolicy(context.Background(), &mainflux.AddPolicyReq{Sub: id, Obj: thingID, Act: readRelation})
|
||||
require.Nil(t, err, fmt.Sprintf("Adding read policy to user expected to succeed: %s", err))
|
||||
require.True(t, apr.GetAuthorized(), fmt.Sprintf("Adding read policy expected to make user authorized, expected %v got %v", true, apr.GetAuthorized()))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
dpr *mainflux.DeletePolicyRes
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "delete valid policy",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: thingID,
|
||||
relation: readRelation,
|
||||
dpr: &mainflux.DeletePolicyRes{Deleted: true},
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "delete invalid policy",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
dpr: &mainflux.DeletePolicyRes{Deleted: false},
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
dpr, err := client.DeletePolicy(context.Background(), &mainflux.DeletePolicyReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
assert.Equal(t, tc.dpr.GetDeleted(), dpr.GetDeleted(), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.dpr.GetDeleted(), dpr.GetDeleted()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMembers(t *testing.T) {
|
||||
_, token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Name: "Mainflux",
|
||||
Description: description,
|
||||
}
|
||||
|
||||
var things []string
|
||||
for i := 0; i < numOfThings; i++ {
|
||||
thID, err := uuid.New().ID()
|
||||
require.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
|
||||
|
||||
err = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: thID, Relation: "owner"})
|
||||
require.Nil(t, err, fmt.Sprintf("Adding a policy expected to succeed: %s", err))
|
||||
|
||||
things = append(things, thID)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for i := 0; i < numOfUsers; i++ {
|
||||
id, err := uuid.New().ID()
|
||||
require.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)
|
||||
require.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
|
||||
err = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: group.ID, Relation: "groupadmin"})
|
||||
require.Nil(t, err, fmt.Sprintf("Adding a policy expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, thingsType, things...)
|
||||
require.Nil(t, err, fmt.Sprintf("Assign members to expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, usersType, users...)
|
||||
require.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, err := grpc.Dial(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error while creating client connection: %s", err))
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
kind uint32
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.kind != auth.LoginKey &&
|
||||
req.kind != auth.APIKey &&
|
||||
req.kind != auth.RecoveryKey {
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type issueReq struct {
|
||||
id string
|
||||
email string
|
||||
keyType uint32
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.email == "" {
|
||||
return apiutil.ErrMissingEmail
|
||||
}
|
||||
if req.keyType != auth.LoginKey &&
|
||||
req.keyType != auth.APIKey &&
|
||||
req.keyType != auth.RecoveryKey {
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type assignReq struct {
|
||||
token string
|
||||
groupID string
|
||||
memberID string
|
||||
groupType string
|
||||
}
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.groupID == "" || req.memberID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type membersReq struct {
|
||||
token string
|
||||
groupID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
memberType string
|
||||
}
|
||||
|
||||
func (req membersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.memberType == "" {
|
||||
return apiutil.ErrMissingMemberType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authReq represents authorization request. It contains:
|
||||
// 1. subject - an action invoker
|
||||
// 2. object - an entity over which action will be executed
|
||||
// 3. action - type of action that will be executed (read/write)
|
||||
type authReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
func (req authReq) validate() error {
|
||||
if req.Sub == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Obj == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Act == "" {
|
||||
return apiutil.ErrMissingPolicyAct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type policyReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
func (req policyReq) validate() error {
|
||||
if req.Sub == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Obj == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Act == "" {
|
||||
return apiutil.ErrMissingPolicyAct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listPoliciesReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
@@ -1,259 +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"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
issue kitgrpc.Handler
|
||||
identify kitgrpc.Handler
|
||||
authorize kitgrpc.Handler
|
||||
addPolicy kitgrpc.Handler
|
||||
deletePolicy kitgrpc.Handler
|
||||
listPolicies kitgrpc.Handler
|
||||
assign kitgrpc.Handler
|
||||
members kitgrpc.Handler
|
||||
mainflux.UnimplementedAuthServiceServer
|
||||
}
|
||||
|
||||
// NewServer returns new AuthServiceServer instance.
|
||||
func NewServer(tracer opentracing.Tracer, svc auth.Service) mainflux.AuthServiceServer {
|
||||
return &grpcServer{
|
||||
issue: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssueRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
identify: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "identify")(identifyEndpoint(svc)),
|
||||
decodeIdentifyRequest,
|
||||
encodeIdentifyResponse,
|
||||
),
|
||||
authorize: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "authorize")(authorizeEndpoint(svc)),
|
||||
decodeAuthorizeRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
addPolicy: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "add_policy")(addPolicyEndpoint(svc)),
|
||||
decodeAddPolicyRequest,
|
||||
encodeAddPolicyResponse,
|
||||
),
|
||||
deletePolicy: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_policy")(deletePolicyEndpoint(svc)),
|
||||
decodeDeletePolicyRequest,
|
||||
encodeDeletePolicyResponse,
|
||||
),
|
||||
listPolicies: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "list_policies")(listPoliciesEndpoint(svc)),
|
||||
decodeListPoliciesRequest,
|
||||
encodeListPoliciesResponse,
|
||||
),
|
||||
assign: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
encodeEmptyResponse,
|
||||
),
|
||||
members: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "members")(membersEndpoint(svc)),
|
||||
decodeMembersRequest,
|
||||
encodeMembersResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) Issue(ctx context.Context, req *mainflux.IssueReq) (*mainflux.Token, error) {
|
||||
_, res, err := s.issue.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.Token), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.UserIdentity, error) {
|
||||
_, res, err := s.identify.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.UserIdentity), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Authorize(ctx context.Context, req *mainflux.AuthorizeReq) (*mainflux.AuthorizeRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.AuthorizeRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) AddPolicy(ctx context.Context, req *mainflux.AddPolicyReq) (*mainflux.AddPolicyRes, error) {
|
||||
_, res, err := s.addPolicy.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.AddPolicyRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) DeletePolicy(ctx context.Context, req *mainflux.DeletePolicyReq) (*mainflux.DeletePolicyRes, error) {
|
||||
_, res, err := s.deletePolicy.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.DeletePolicyRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) ListPolicies(ctx context.Context, req *mainflux.ListPoliciesReq) (*mainflux.ListPoliciesRes, error) {
|
||||
_, res, err := s.listPolicies.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.ListPoliciesRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Assign(ctx context.Context, token *mainflux.Assignment) (*empty.Empty, error) {
|
||||
_, res, err := s.assign.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*empty.Empty), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Members(ctx context.Context, req *mainflux.MembersReq) (*mainflux.MembersRes, error) {
|
||||
_, res, err := s.members.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.MembersRes), nil
|
||||
}
|
||||
|
||||
func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.IssueReq)
|
||||
return issueReq{id: req.GetId(), email: req.GetEmail(), keyType: req.GetType()}, nil
|
||||
}
|
||||
|
||||
func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(issueRes)
|
||||
return &mainflux.Token{Value: res.value}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.Token)
|
||||
return identityReq{token: req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(identityRes)
|
||||
return &mainflux.UserIdentity{Id: res.id, Email: res.email}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AuthorizeReq)
|
||||
return authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &mainflux.AuthorizeRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAddPolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AddPolicyReq)
|
||||
return policyReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(addPolicyRes)
|
||||
return &mainflux.AddPolicyRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.Token)
|
||||
return assignReq{token: req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func decodeDeletePolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.DeletePolicyReq)
|
||||
return policyReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(deletePolicyRes)
|
||||
return &mainflux.DeletePolicyRes{Deleted: res.deleted}, nil
|
||||
}
|
||||
|
||||
func decodeListPoliciesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.ListPoliciesReq)
|
||||
return listPoliciesReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeListPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(listPoliciesRes)
|
||||
return &mainflux.ListPoliciesRes{Policies: res.policies}, nil
|
||||
}
|
||||
|
||||
func decodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.MembersReq)
|
||||
return membersReq{
|
||||
token: req.GetToken(),
|
||||
groupID: req.GetGroupID(),
|
||||
memberType: req.GetType(),
|
||||
offset: req.Offset,
|
||||
limit: req.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(membersRes)
|
||||
return &mainflux.MembersRes{
|
||||
Total: res.total,
|
||||
Offset: res.offset,
|
||||
Limit: res.limit,
|
||||
Type: res.groupType,
|
||||
Members: res.members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeEmptyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(emptyRes)
|
||||
return &empty.Empty{}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrInvalidAuthKey,
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMissingPolicyAct:
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
errors.Contains(err, auth.ErrKeyExpired),
|
||||
err == apiutil.ErrMissingEmail,
|
||||
err == apiutil.ErrBearerToken:
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
serverErr := make(chan error)
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
log.Fatalf("got unexpected error while creating new listerner: %s", err)
|
||||
}
|
||||
|
||||
svc = newService()
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterAuthServiceServer(server, grpcapi.NewServer(mocktracer.New(), svc))
|
||||
|
||||
// Start gRPC server in detached mode.
|
||||
go func() {
|
||||
serverErr <- server.Serve(listener)
|
||||
}()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
err = <-serverErr
|
||||
if err != nil {
|
||||
log.Fatalln("gPRC Server Terminated : ", err)
|
||||
}
|
||||
close(serverErr)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: authoritiesObj, Relation: memberRelation})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
tokenizer := jwt.New(secret)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, tokenizer, ketoMock, loginDuration)
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func createGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
group := auth.Group{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
ParentID: req.ParentID,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
group, err := svc.CreateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
return groupRes{created: true, id: group.ID}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
|
||||
group, err := svc.ViewGroup(ctx, req.token, req.id)
|
||||
if err != nil {
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
|
||||
res := viewGroupRes{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
group := auth.Group{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
_, err := svc.UpdateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
res := groupRes{created: false}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.RemoveGroup(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deleteRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listGroupsEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListGroups(ctx, req.token, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMemberships(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMembershipsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
|
||||
page, err := svc.ListMemberships(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func shareGroupAccessEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(shareGroupAccessReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return shareGroupRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.AssignGroupAccessRights(ctx, req.token, req.ThingGroupID, req.userGroupID); err != nil {
|
||||
return shareGroupRes{}, err
|
||||
}
|
||||
return shareGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listChildrenEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListChildren(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func listParentsEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
|
||||
page, err := svc.ListParents(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(assignReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Assign(ctx, req.token, req.groupID, req.Type, req.Members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assignRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func unassignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(unassignReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Unassign(ctx, req.token, req.groupID, req.Members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unassignRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMembersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMembersReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListMembers(ctx, req.token, req.id, req.groupType, pm)
|
||||
if err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
return buildUsersResponse(page, req.groupType), nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildGroupsResponseTree(page auth.GroupPage) groupPageRes {
|
||||
groupsMap := map[string]*auth.Group{}
|
||||
// Parents' map keeps its array of children.
|
||||
parentsMap := map[string][]*auth.Group{}
|
||||
for i := range page.Groups {
|
||||
if _, ok := groupsMap[page.Groups[i].ID]; !ok {
|
||||
groupsMap[page.Groups[i].ID] = &page.Groups[i]
|
||||
parentsMap[page.Groups[i].ID] = make([]*auth.Group, 0)
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
if children, ok := parentsMap[group.ParentID]; ok {
|
||||
children = append(children, group)
|
||||
parentsMap[group.ParentID] = children
|
||||
}
|
||||
}
|
||||
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Limit: page.Limit,
|
||||
Offset: page.Offset,
|
||||
Total: page.Total,
|
||||
Level: page.Level,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
if children, ok := parentsMap[group.ID]; ok {
|
||||
group.Children = children
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
view := toViewGroupRes(*group)
|
||||
if children, ok := parentsMap[group.ParentID]; len(children) == 0 || !ok {
|
||||
res.Groups = append(res.Groups, view)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func toViewGroupRes(group auth.Group) viewGroupRes {
|
||||
view := viewGroupRes{
|
||||
ID: group.ID,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
Level: group.Level,
|
||||
Path: group.Path,
|
||||
Children: make([]*viewGroupRes, 0),
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
|
||||
for _, ch := range group.Children {
|
||||
child := toViewGroupRes(*ch)
|
||||
view.Children = append(view.Children, &child)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func buildGroupsResponse(gp auth.GroupPage) groupPageRes {
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: gp.Total,
|
||||
Level: gp.Level,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
|
||||
for _, group := range gp.Groups {
|
||||
view := viewGroupRes{
|
||||
ID: group.ID,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
Level: group.Level,
|
||||
Path: group.Path,
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
res.Groups = append(res.Groups, view)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func buildUsersResponse(mp auth.MemberPage, groupType string) memberPageRes {
|
||||
res := memberPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: mp.Total,
|
||||
Offset: mp.Offset,
|
||||
Limit: mp.Limit,
|
||||
Name: mp.Name,
|
||||
},
|
||||
Type: groupType,
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
for _, m := range mp.Members {
|
||||
res.Members = append(res.Members, m.ID)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package groups_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
email = "user@example.com"
|
||||
secret = "secret"
|
||||
id = "testID"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
req.Close = true
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
keys := mocks.NewKeyRepository()
|
||||
groups := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
policies := mocks.NewKetoMock(map[string][]mocks.MockSubjectSet{})
|
||||
return auth.New(keys, groups, idProvider, t, policies, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestShareGroupAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.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)
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
type shareGroupAccessReq struct {
|
||||
token string
|
||||
userGroupID string
|
||||
ThingGroupID string `json:"thing_group_id"`
|
||||
}
|
||||
data := shareGroupAccessReq{token: apiToken, userGroupID: "ug", ThingGroupID: "tg"}
|
||||
invalidData := shareGroupAccessReq{token: apiToken, userGroupID: "ug", ThingGroupID: ""}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
userGroupID string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "share a user group with thing group",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with invalid thing group",
|
||||
req: toJSON(invalidData),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share an invalid user group with thing group",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share an invalid user group with invalid thing group",
|
||||
req: toJSON(invalidData),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with thing group with invalid content type",
|
||||
req: toJSON(data),
|
||||
contentType: "",
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with thing group with invalid token",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: "token",
|
||||
userGroupID: "ug",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/groups/%s/share", ts.URL, tc.userGroupID),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type createGroupReq struct {
|
||||
token string
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req createGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if len(req.Name) > maxNameSize || req.Name == "" {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateGroupReq struct {
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listGroupsReq struct {
|
||||
token string
|
||||
id string
|
||||
level uint64
|
||||
// - `true` - result is JSON tree representing groups hierarchy,
|
||||
// - `false` - result is JSON array of groups.
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listGroupsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.level > auth.MaxLevel || req.level < auth.MinLevel {
|
||||
return apiutil.ErrMaxLevelExceeded
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembersReq struct {
|
||||
token string
|
||||
id string
|
||||
groupType string
|
||||
offset uint64
|
||||
limit uint64
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembershipsReq struct {
|
||||
token string
|
||||
id string
|
||||
offset uint64
|
||||
limit uint64
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembershipsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type assignReq struct {
|
||||
token string
|
||||
groupID string
|
||||
Type string `json:"type,omitempty"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Type == "" {
|
||||
return apiutil.ErrMissingMemberType
|
||||
}
|
||||
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.Members) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type shareGroupAccessReq struct {
|
||||
token string
|
||||
userGroupID string
|
||||
ThingGroupID string `json:"thing_group_id"`
|
||||
}
|
||||
|
||||
func (req shareGroupAccessReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ThingGroupID == "" || req.userGroupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type unassignReq struct {
|
||||
assignReq
|
||||
}
|
||||
|
||||
func (req unassignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.Members) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type groupReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req groupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*memberPageRes)(nil)
|
||||
_ mainflux.Response = (*groupRes)(nil)
|
||||
_ mainflux.Response = (*deleteRes)(nil)
|
||||
_ mainflux.Response = (*assignRes)(nil)
|
||||
_ mainflux.Response = (*unassignRes)(nil)
|
||||
)
|
||||
|
||||
type memberPageRes struct {
|
||||
pageRes
|
||||
Type string `json:"type"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (res memberPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res memberPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res memberPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type shareGroupRes struct {
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type viewGroupRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
// Indicates a level in tree hierarchy from first group node - root.
|
||||
Level int `json:"level"`
|
||||
// Path in a tree consisting of group ids
|
||||
// parentID1.parentID2.childID1
|
||||
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
|
||||
Path string `json:"path"`
|
||||
Children []*viewGroupRes `json:"children,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (res viewGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewGroupRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type groupRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res groupRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res groupRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/groups/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type groupPageRes struct {
|
||||
pageRes
|
||||
Groups []viewGroupRes `json:"groups"`
|
||||
}
|
||||
|
||||
type pageRes struct {
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
Level uint64 `json:"level"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (res groupPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res groupPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type deleteRes struct{}
|
||||
|
||||
func (res deleteRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res deleteRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deleteRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type assignRes struct{}
|
||||
|
||||
func (res assignRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res assignRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res assignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type unassignRes struct{}
|
||||
|
||||
func (res unassignRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res unassignRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res unassignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxNameSize = 254
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
levelKey = "level"
|
||||
metadataKey = "metadata"
|
||||
treeKey = "tree"
|
||||
groupType = "type"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
defLevel = 1
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
mux.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_group")(createGroupEndpoint(svc)),
|
||||
decodeGroupCreate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "view_group")(viewGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "update_group")(updateGroupEndpoint(svc)),
|
||||
decodeGroupUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_group")(deleteGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:groupID/share", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "share_group_access")(shareGroupAccessEndpoint(svc)),
|
||||
decodeShareGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(listGroupsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/children", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_children")(listChildrenEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/parents", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_parents_groups")(listParentsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:groupID/members/assign", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:groupID/members/unassign", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "unassign")(unassignEndpoint(svc)),
|
||||
decodeUnassignRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_members")(listMembersEndpoint(svc)),
|
||||
decodeListMembersRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/members/:memberID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_memberships")(listMemberships(svc)),
|
||||
decodeListMembershipsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeShareGroupRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := shareGroupAccessReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
userGroupID: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := apiutil.ReadUintQuery(r, levelKey, defLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := apiutil.ReadBoolQuery(r, treeKey, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listGroupsReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
level: l,
|
||||
metadata: m,
|
||||
tree: t,
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := apiutil.ReadBoolQuery(r, treeKey, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := apiutil.ReadStringQuery(r, groupType, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembersReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
groupType: t,
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
tree: tree,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembershipsReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "memberID"),
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := createGroupReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateGroupReq{
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := groupReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := assignReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUnassignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := unassignReq{
|
||||
assignReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrEmptyList,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrNameSize:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrAuthentication):
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
|
||||
case errors.Contains(err, errors.ErrCreateEntity),
|
||||
errors.Contains(err, errors.ErrUpdateEntity),
|
||||
errors.Contains(err, errors.ErrViewEntity),
|
||||
errors.Contains(err, errors.ErrRemoveEntity):
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
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.(issueKeyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
newKey := auth.Key{
|
||||
IssuedAt: now,
|
||||
Type: req.Type,
|
||||
}
|
||||
|
||||
duration := time.Duration(req.Duration * time.Second)
|
||||
if duration != 0 {
|
||||
exp := now.Add(duration)
|
||||
newKey.ExpiresAt = exp
|
||||
}
|
||||
|
||||
key, secret, err := svc.Issue(ctx, req.token, newKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := issueKeyRes{
|
||||
ID: key.ID,
|
||||
Value: secret,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
res.ExpiresAt = &key.ExpiresAt
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveEndpoint(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
|
||||
}
|
||||
|
||||
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 retrieveKeysEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listKeysReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Subject: req.subject,
|
||||
Type: req.keyType,
|
||||
}
|
||||
kp, err := svc.RetrieveKeys(ctx, req.token, pm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := keyPageRes{
|
||||
pageRes: pageRes{
|
||||
Limit: kp.Limit,
|
||||
Offset: kp.Offset,
|
||||
Total: kp.Total,
|
||||
},
|
||||
Keys: []retrieveKeyRes{},
|
||||
}
|
||||
|
||||
for _, key := range kp.Keys {
|
||||
view := retrieveKeyRes{
|
||||
ID: key.ID,
|
||||
IssuerID: key.IssuerID,
|
||||
Subject: key.Subject,
|
||||
Type: key.Type,
|
||||
IssuedAt: key.IssuedAt,
|
||||
ExpiresAt: &key.ExpiresAt,
|
||||
}
|
||||
res.Keys = append(res.Keys, view)
|
||||
}
|
||||
|
||||
return res, 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
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type issueRequest struct {
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
Type uint32 `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
|
||||
req.Header.Set("Referer", "http://localhost")
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: "authorities", Relation: "member"})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
lk := issueRequest{Type: auth.LoginKey}
|
||||
ak := issueRequest{Type: auth.APIKey, Duration: time.Hour}
|
||||
rk := issueRequest{Type: auth.RecoveryKey}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
ct string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "issue login key with empty token",
|
||||
req: toJSON(lk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: loginSecret,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: loginSecret,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue login key wrong content type",
|
||||
req: toJSON(lk),
|
||||
ct: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key wrong content type",
|
||||
req: toJSON(rk),
|
||||
ct: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key with an invalid token",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key with empty token",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid request",
|
||||
req: "{",
|
||||
ct: contentType,
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON",
|
||||
req: "{invalid}",
|
||||
ct: contentType,
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON content",
|
||||
req: `{"Type":{"key":"value"}}`,
|
||||
ct: contentType,
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/keys", ts.URL),
|
||||
contentType: tc.ct,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
|
||||
|
||||
k, _, err := svc.Issue(context.Background(), loginSecret, key)
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: k.ID,
|
||||
token: loginSecret,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing key",
|
||||
id: "non-existing",
|
||||
token: loginSecret,
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key with an invalid token",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
n := uint64(100)
|
||||
var data []auth.Key
|
||||
for i := uint64(0); i < n; i++ {
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: fmt.Sprintf("user_%d@example.com", i)}
|
||||
|
||||
k, _, err := svc.Issue(context.Background(), loginSecret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
k.ExpiresAt = time.Time{}
|
||||
data = append(data, k)
|
||||
}
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
url string
|
||||
auth string
|
||||
status int
|
||||
res []auth.Key
|
||||
}{
|
||||
{
|
||||
desc: "get a list of keys",
|
||||
auth: loginSecret,
|
||||
status: http.StatusOK,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d", 0, 5),
|
||||
res: data[0:5],
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with invalid token",
|
||||
auth: "wrongValue",
|
||||
status: http.StatusUnauthorized,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d", 0, 1),
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with empty token",
|
||||
auth: "",
|
||||
status: http.StatusUnauthorized,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d", 0, 1),
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with negative offset",
|
||||
auth: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d", -1, 5),
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with negative limit",
|
||||
auth: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d", 1, -5),
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with zero limit and offset 1",
|
||||
auth: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d", 1, 0),
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys without offset",
|
||||
auth: loginSecret,
|
||||
status: http.StatusOK,
|
||||
url: fmt.Sprintf("?limit=%d", 5),
|
||||
res: data[0:5],
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys without limit",
|
||||
auth: loginSecret,
|
||||
status: http.StatusOK,
|
||||
url: fmt.Sprintf("?offset=%d", 1),
|
||||
res: data[1:11],
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with redundant query params",
|
||||
auth: loginSecret,
|
||||
status: http.StatusOK,
|
||||
url: fmt.Sprintf("?offset=%d&limit=%d&value=something", 0, 5),
|
||||
res: data[0:5],
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with default URL",
|
||||
auth: loginSecret,
|
||||
status: http.StatusOK,
|
||||
url: "",
|
||||
res: data[0:10],
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with invalid number of params",
|
||||
auth: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
url: "?offset=4&limit=4&limit=5&offset=5",
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with invalid offset",
|
||||
auth: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
url: "?offset=e&limit=5",
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of keys with invalid limit",
|
||||
auth: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
url: "?offset=5&limit=e",
|
||||
res: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/keys%s", ts.URL, tc.url),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
|
||||
|
||||
k, _, err := svc.Issue(context.Background(), loginSecret, key)
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "revoke an existing key",
|
||||
id: k.ID,
|
||||
token: loginSecret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a non-existing key",
|
||||
id: "non-existing",
|
||||
token: loginSecret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke key with invalid token",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type issueKeyReq struct {
|
||||
token string
|
||||
Type uint32 `json:"type,omitempty"`
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
// It is not possible to issue Reset key using HTTP API.
|
||||
func (req issueKeyReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Type != auth.LoginKey &&
|
||||
req.Type != auth.RecoveryKey &&
|
||||
req.Type != auth.APIKey {
|
||||
return apiutil.ErrInvalidAPIKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type keyReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req keyReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listKeysReq struct {
|
||||
token string
|
||||
subject string
|
||||
keyType uint32
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listKeysReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.limit < 1 {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*issueKeyRes)(nil)
|
||||
_ mainflux.Response = (*revokeKeyRes)(nil)
|
||||
)
|
||||
|
||||
type issueKeyRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (res issueKeyRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res issueKeyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res issueKeyRes) Empty() bool {
|
||||
return res.Value == ""
|
||||
}
|
||||
|
||||
type 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 keyPageRes struct {
|
||||
pageRes
|
||||
Keys []retrieveKeyRes `json:"keys"`
|
||||
}
|
||||
|
||||
type pageRes struct {
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
}
|
||||
|
||||
type revokeKeyRes struct {
|
||||
}
|
||||
|
||||
func (res revokeKeyRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res revokeKeyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res revokeKeyRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
subjectKey = "subject"
|
||||
typeKey = "type"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
defType = 2
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
mux.Post("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssue,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
mux.Get("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(retrieveKeysEndpoint(svc)),
|
||||
decodeListKeysRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/keys/:keyID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "retrieve")(retrieveEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/keys/:keyID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "revoke")(revokeEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := issueKeyReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "keyID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListKeysRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
s, err := apiutil.ReadStringQuery(r, subjectKey, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := apiutil.ReadUintQuery(r, typeKey, defType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listKeysReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
subject: s,
|
||||
keyType: uint32(t),
|
||||
offset: o,
|
||||
limit: l,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrInvalidAPIKey:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, errors.ErrInvalidQueryParams),
|
||||
errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrBearerKey,
|
||||
err == apiutil.ErrLimitSize,
|
||||
err == apiutil.ErrOffsetSize,
|
||||
err == apiutil.ErrInvalidIDFormat:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func createPolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policiesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return createPolicyRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.AddPolicies(ctx, req.token, req.Object, req.SubjectIDs, req.Policies); err != nil {
|
||||
return createPolicyRes{}, err
|
||||
}
|
||||
|
||||
return createPolicyRes{created: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deletePoliciesEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policiesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deletePoliciesRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.DeletePolicies(ctx, req.token, req.Object, req.SubjectIDs, req.Policies); err != nil {
|
||||
return deletePoliciesRes{}, err
|
||||
}
|
||||
|
||||
return deletePoliciesRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package policies_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = uuid.Prefix + "-000000000001"
|
||||
email = "user@example.com"
|
||||
unauthzID = uuid.Prefix + "-000000000002"
|
||||
unauthzEmail = "unauthz@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
|
||||
req.Header.Set("Referer", "http://localhost")
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: "authorities", Relation: "member"})
|
||||
mockAuthzDB[unauthzID] = append(mockAuthzDB[unauthzID], mocks.MockSubjectSet{Object: "users", Relation: "member"})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
type addPolicyRequest struct {
|
||||
SubjectIDs []string `json:"subjects"`
|
||||
Policies []string `json:"policies"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
func TestAddPolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, userLoginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: unauthzID, Subject: unauthzEmail})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing unauthorized user's key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
valid := addPolicyRequest{Object: "obj", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
multipleValid := addPolicyRequest{Object: "obj", Policies: []string{"write", "delete"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidObject := addPolicyRequest{Object: "", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidPolicies := addPolicyRequest{Object: "obj", Policies: []string{"read", "invalid"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidSubjects := addPolicyRequest{Object: "obj", Policies: []string{"read", "access"}, SubjectIDs: []string{"", "user2"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ct string
|
||||
status int
|
||||
req string
|
||||
}{
|
||||
{
|
||||
desc: "Add policies with authorized access",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusCreated,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add multiple policies to multiple user",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusCreated,
|
||||
req: toJSON(multipleValid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with unauthorized access",
|
||||
token: userLoginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusForbidden,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid token",
|
||||
token: "invalid",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty token",
|
||||
token: "",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid content type",
|
||||
token: loginSecret,
|
||||
ct: "text/html",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty content type",
|
||||
token: loginSecret,
|
||||
ct: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid object field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidObject),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid policies field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidPolicies),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid subjects field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidSubjects),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/policies", ts.URL),
|
||||
contentType: tc.ct,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, userLoginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: unauthzID, Subject: unauthzEmail})
|
||||
require.Nil(t, err, fmt.Sprintf("Issuing unauthorized user's key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
policies := addPolicyRequest{Object: "obj", Policies: []string{"read", "write", "delete"}, SubjectIDs: []string{"user1", "user2", "user3"}}
|
||||
err = svc.AddPolicies(context.Background(), loginSecret, policies.Object, policies.SubjectIDs, policies.Policies)
|
||||
require.Nil(t, err, fmt.Sprintf("Adding policies expected to succeed: %s", err))
|
||||
|
||||
validSingleDeleteReq := addPolicyRequest{Object: "obj", Policies: []string{"read"}, SubjectIDs: []string{"user1"}}
|
||||
validMultipleDeleteReq := addPolicyRequest{Object: "obj", Policies: []string{"write", "delete"}, SubjectIDs: []string{"user2", "user3"}}
|
||||
invalidObject := addPolicyRequest{Object: "", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidPolicies := addPolicyRequest{Object: "obj", Policies: []string{"read", "invalid"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidSubjects := addPolicyRequest{Object: "obj", Policies: []string{"read", "access"}, SubjectIDs: []string{"", "user2"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ct string
|
||||
req string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "Delete policies with unauthorized access",
|
||||
token: userLoginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusForbidden,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid token",
|
||||
token: "invalid",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty token",
|
||||
token: "",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with authorized access",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusNoContent,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete multiple policies to multiple user",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusNoContent,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid content type",
|
||||
token: loginSecret,
|
||||
ct: "text/html",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty content type",
|
||||
token: loginSecret,
|
||||
ct: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid object field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidObject),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid policies field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidPolicies),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid subjects field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidSubjects),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/policies", ts.URL),
|
||||
contentType: tc.ct,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package policies
|
||||
|
||||
import "github.com/mainflux/mainflux/internal/apiutil"
|
||||
|
||||
// Action represents an enum for the policies used in the Mainflux.
|
||||
type Action int
|
||||
|
||||
const (
|
||||
Create Action = iota
|
||||
Read
|
||||
Write
|
||||
Delete
|
||||
Access
|
||||
Member
|
||||
Unknown
|
||||
)
|
||||
|
||||
var actions = map[string]Action{
|
||||
"create": Create,
|
||||
"read": Read,
|
||||
"write": Write,
|
||||
"delete": Delete,
|
||||
"access": Access,
|
||||
"member": Member,
|
||||
}
|
||||
|
||||
type policiesReq struct {
|
||||
token string
|
||||
SubjectIDs []string `json:"subjects"`
|
||||
Policies []string `json:"policies"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
func (req policiesReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if len(req.SubjectIDs) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
if len(req.Policies) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
if req.Object == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
for _, policy := range req.Policies {
|
||||
if _, ok := actions[policy]; !ok {
|
||||
return apiutil.ErrMalformedPolicy
|
||||
}
|
||||
}
|
||||
|
||||
for _, subID := range req.SubjectIDs {
|
||||
if subID == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package policies
|
||||
|
||||
import "net/http"
|
||||
|
||||
type createPolicyRes struct {
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type deletePoliciesRes struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Code() int {
|
||||
if res.deleted {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
|
||||
mux.Post("/policies", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_policy_bulk")(createPolicyEndpoint(svc)),
|
||||
decodePoliciesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/policies", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_policies")(deletePoliciesEndpoint(svc)),
|
||||
decodePoliciesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodePoliciesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := policiesReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrEmptyList,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMalformedPolicy:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/api/http/groups"
|
||||
"github.com/mainflux/mainflux/auth/api/http/keys"
|
||||
"github.com/mainflux/mainflux/auth/api/http/policies"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, tracer opentracing.Tracer, logger logger.Logger) http.Handler {
|
||||
mux := bone.New()
|
||||
mux = keys.MakeHandler(svc, mux, tracer, logger)
|
||||
mux = groups.MakeHandler(svc, mux, tracer, logger)
|
||||
mux = policies.MakeHandler(svc, mux, tracer, logger)
|
||||
mux.GetFunc("/health", mainflux.Health("auth"))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
return mux
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc auth.Service, logger log.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListPolicies(ctx context.Context, pr auth.PolicyReq) (p auth.PolicyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_policies took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListPolicies(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Issue(ctx context.Context, token string, newKey auth.Key) (key auth.Key, secret string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
d := "infinite duration"
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
d = fmt.Sprintf("the key with expiration date %v", key.ExpiresAt)
|
||||
}
|
||||
message := fmt.Sprintf("Method issue for %s took %s to complete", d, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Issue(ctx, token, newKey)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Revoke(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke for key %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Revoke(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RetrieveKey(ctx context.Context, token, id string) (key auth.Key, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method retrieve for key %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RetrieveKeys(ctx context.Context, token string, pm auth.PageMetadata) (kp auth.KeyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method retrieve 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.RetrieveKeys(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id auth.Identity, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method identify took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identify(ctx, key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method authorize took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.Authorize(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddPolicy(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add_policy took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_policy_bulk took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AddPolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeletePolicy(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method delete_policy took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method delete_policies took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.DeletePolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (g auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, group.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, group.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_groups for token %s took %s to complete", token, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListGroups(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_children for token %s and parent %s took %s to complete", token, parentID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChildren(ctx, token, parentID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_parents for token %s and child %s took for child %s to complete", token, childID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListParents(ctx, token, childID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_members for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMembers(ctx, token, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_memberships for token %s and member id %s took %s to complete", token, memberID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMemberships(ctx, token, memberID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method share_group_access took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AssignGroupAccessRights(ctx, token, thingGroupID, userGroupID)
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListPolicies(ctx context.Context, pr auth.PolicyReq) (p auth.PolicyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_policies").Add(1)
|
||||
ms.latency.With("method", "list_policies").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListPolicies(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.Key) (auth.Key, string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_key").Add(1)
|
||||
ms.latency.With("method", "issue_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Issue(ctx, token, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Revoke(ctx context.Context, token, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_key").Add(1)
|
||||
ms.latency.With("method", "revoke_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Revoke(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RetrieveKey(ctx context.Context, token, id string) (auth.Key, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve_key").Add(1)
|
||||
ms.latency.With("method", "retrieve_key").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RetrieveKey(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RetrieveKeys(ctx context.Context, token string, pm auth.PageMetadata) (auth.KeyPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve_keys").Add(1)
|
||||
ms.latency.With("method", "retrieve_keys").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RetrieveKeys(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.Identity, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identify").Add(1)
|
||||
ms.latency.With("method", "identify").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "authorize").Add(1)
|
||||
ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.Authorize(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_policy").Add(1)
|
||||
ms.latency.With("method", "add_policy").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_policy_bulk").Add(1)
|
||||
ms.latency.With("method", "create_policy_bulk").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AddPolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "delete_policy").Add(1)
|
||||
ms.latency.With("method", "delete_policy").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "delete_policies").Add(1)
|
||||
ms.latency.With("method", "delete_policies").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.DeletePolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_group").Add(1)
|
||||
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_group").Add(1)
|
||||
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_group").Add(1)
|
||||
ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_group").Add(1)
|
||||
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_groups").Add(1)
|
||||
ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListGroups(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "parents").Add(1)
|
||||
ms.latency.With("method", "parents").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListParents(ctx, token, childID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_children").Add(1)
|
||||
ms.latency.With("method", "list_children").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChildren(ctx, token, parentID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_members").Add(1)
|
||||
ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMembers(ctx, token, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_memberships").Add(1)
|
||||
ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMemberships(ctx, token, memberID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "assign").Add(1)
|
||||
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "unassign").Add(1)
|
||||
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "share_group_access").Add(1)
|
||||
ms.latency.With("method", "share_group_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AssignGroupAccessRights(ctx, token, thingGroupID, userGroupID)
|
||||
}
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLevel represents the maximum group hierarchy level.
|
||||
MaxLevel = uint64(5)
|
||||
// MinLevel represents the minimum group hierarchy level.
|
||||
MinLevel = uint64(1)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAssignToGroup indicates failure to assign member to a group.
|
||||
ErrAssignToGroup = errors.New("failed to assign member to a group")
|
||||
|
||||
// ErrUnassignFromGroup indicates failure to unassign member from a group.
|
||||
ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
|
||||
|
||||
// ErrMissingParent indicates that parent can't be found
|
||||
ErrMissingParent = errors.New("failed to retrieve parent")
|
||||
|
||||
// ErrGroupNotEmpty indicates group is not empty, can't be deleted.
|
||||
ErrGroupNotEmpty = errors.New("group is not empty")
|
||||
|
||||
// ErrMemberAlreadyAssigned indicates that members is already assigned.
|
||||
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
|
||||
)
|
||||
|
||||
// GroupMetadata defines the Metadata type.
|
||||
type GroupMetadata map[string]interface{}
|
||||
|
||||
// Member represents the member information.
|
||||
type Member struct {
|
||||
ID string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Group represents the group information.
|
||||
type Group struct {
|
||||
ID string
|
||||
OwnerID string
|
||||
ParentID string
|
||||
Name string
|
||||
Description string
|
||||
Metadata GroupMetadata
|
||||
// Indicates a level in tree hierarchy.
|
||||
// Root node is level 1.
|
||||
Level int
|
||||
// Path in a tree consisting of group ids
|
||||
// parentID1.parentID2.childID1
|
||||
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
|
||||
Path string
|
||||
Children []*Group
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PageMetadata contains page metadata that helps navigation.
|
||||
type PageMetadata struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Size uint64
|
||||
Level uint64
|
||||
Name string
|
||||
Type uint32
|
||||
Subject string
|
||||
Metadata GroupMetadata
|
||||
}
|
||||
|
||||
// GroupPage contains page related metadata as well as list of groups that
|
||||
// belong to this page.
|
||||
type GroupPage struct {
|
||||
PageMetadata
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
// MemberPage contains page related metadata as well as list of members that
|
||||
// belong to this page.
|
||||
type MemberPage struct {
|
||||
PageMetadata
|
||||
Members []Member
|
||||
}
|
||||
|
||||
// GroupService specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type GroupService interface {
|
||||
// CreateGroup creates new group.
|
||||
CreateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// UpdateGroup updates the group identified by the provided ID.
|
||||
UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// ViewGroup retrieves data about the group identified by ID.
|
||||
ViewGroup(ctx context.Context, token, id string) (Group, error)
|
||||
|
||||
// ListGroups retrieves groups.
|
||||
ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListChildren retrieves groups that are children to group identified by parentID
|
||||
ListChildren(ctx context.Context, token, parentID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListParents retrieves groups that are parent to group identified by childID.
|
||||
ListParents(ctx context.Context, token, childID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListMembers retrieves everything that is assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID, groupType string, pm PageMetadata) (MemberPage, error)
|
||||
|
||||
// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
|
||||
ListMemberships(ctx context.Context, token, memberID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RemoveGroup removes the group identified with the provided ID.
|
||||
RemoveGroup(ctx context.Context, token, id string) error
|
||||
|
||||
// Assign adds a member with memberID into the group identified by groupID.
|
||||
Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) error
|
||||
|
||||
// Unassign removes member with memberID from group identified by groupID.
|
||||
Unassign(ctx context.Context, token, groupID string, memberIDs ...string) error
|
||||
|
||||
// AssignGroupAccessRights adds access rights on thing groups to user group.
|
||||
AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error
|
||||
}
|
||||
|
||||
// GroupRepository specifies a group persistence API.
|
||||
type GroupRepository interface {
|
||||
// Save group
|
||||
Save(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Update a group
|
||||
Update(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Delete a group
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// RetrieveByID retrieves group by its id
|
||||
RetrieveByID(ctx context.Context, id string) (Group, error)
|
||||
|
||||
// RetrieveAll retrieves all groups.
|
||||
RetrieveAll(ctx context.Context, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
|
||||
RetrieveAllParents(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
|
||||
RetrieveAllChildren(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// Retrieves list of groups that member belongs to
|
||||
Memberships(ctx context.Context, memberID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// Members retrieves everything that is assigned to a group identified by groupID.
|
||||
Members(ctx context.Context, groupID, groupType string, pm PageMetadata) (MemberPage, error)
|
||||
|
||||
// Assign adds a member to group.
|
||||
Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error
|
||||
|
||||
// Unassign removes a member from a group
|
||||
Unassign(ctx context.Context, groupID string, memberIDs ...string) error
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const secret = "test"
|
||||
|
||||
func key() auth.Key {
|
||||
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
|
||||
return auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.LoginKey,
|
||||
Subject: "user@email.com",
|
||||
IssuerID: "",
|
||||
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: exp,
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
tokenizer := jwt.New(secret)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue new token",
|
||||
key: key(),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := tokenizer.Issue(tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tokenizer := jwt.New(secret)
|
||||
|
||||
token, err := tokenizer.Issue(key())
|
||||
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
|
||||
|
||||
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()
|
||||
expKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
expToken, err := tokenizer.Issue(expKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing expired key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "parse valid key",
|
||||
key: key(),
|
||||
token: token,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse ivalid key",
|
||||
key: auth.Key{},
|
||||
token: "invalid",
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "parse expired key",
|
||||
key: auth.Key{},
|
||||
token: expToken,
|
||||
err: auth.ErrKeyExpired,
|
||||
},
|
||||
{
|
||||
desc: "parse expired API key",
|
||||
key: apiKey,
|
||||
token: apiToken,
|
||||
err: auth.ErrAPIKeyExpired,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
key, err := tokenizer.Parse(tc.token)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const issuerName = "mainflux.auth"
|
||||
|
||||
type claims struct {
|
||||
jwt.RegisteredClaims
|
||||
IssuerID string `json:"issuer_id,omitempty"`
|
||||
Type *uint32 `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (c claims) Valid() error {
|
||||
if c.Type == nil || *c.Type > auth.APIKey || c.Issuer != issuerName {
|
||||
return errors.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return c.RegisteredClaims.Valid()
|
||||
}
|
||||
|
||||
type tokenizer struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// New returns new JWT Tokenizer.
|
||||
func New(secret string) auth.Tokenizer {
|
||||
return tokenizer{secret: secret}
|
||||
}
|
||||
|
||||
func (svc tokenizer) Issue(key auth.Key) (string, error) {
|
||||
claims := claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: issuerName,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: &jwt.NumericDate{Time: key.IssuedAt.UTC()},
|
||||
},
|
||||
IssuerID: key.IssuerID,
|
||||
Type: &key.Type,
|
||||
}
|
||||
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
claims.ExpiresAt = &jwt.NumericDate{Time: key.ExpiresAt.UTC()}
|
||||
}
|
||||
if key.ID != "" {
|
||||
claims.ID = key.ID
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(svc.secret))
|
||||
}
|
||||
|
||||
func (svc tokenizer) Parse(token string) (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, errors.ErrAuthentication
|
||||
}
|
||||
return []byte(svc.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if e, ok := err.(*jwt.ValidationError); ok && e.Errors == jwt.ValidationErrorExpired {
|
||||
// Expired User key needs to be revoked.
|
||||
|
||||
if c.Type != nil && *c.Type == auth.APIKey {
|
||||
return c.toKey(), auth.ErrAPIKeyExpired
|
||||
}
|
||||
return auth.Key{}, errors.Wrap(auth.ErrKeyExpired, err)
|
||||
}
|
||||
return auth.Key{}, errors.Wrap(errors.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
return c.toKey(), nil
|
||||
}
|
||||
|
||||
func (c claims) toKey() auth.Key {
|
||||
key := auth.Key{
|
||||
ID: c.ID,
|
||||
IssuerID: c.IssuerID,
|
||||
Subject: c.Subject,
|
||||
IssuedAt: c.IssuedAt.Time.UTC(),
|
||||
}
|
||||
|
||||
key.ExpiresAt = time.Time{}
|
||||
if c.ExpiresAt != nil && c.ExpiresAt.Time.UTC().Unix() != 0 {
|
||||
key.ExpiresAt = c.ExpiresAt.Time.UTC()
|
||||
}
|
||||
|
||||
// Default type is 0.
|
||||
if c.Type != nil {
|
||||
key.Type = *(c.Type)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package keto contains PolicyAgent implementation using Keto.
|
||||
package keto
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
subjectSetRegex = "^.{1,}:.{1,}#.{1,}$" // expected subject set structure is <namespace>:<object>#<relation>
|
||||
ketoNamespace = "members"
|
||||
)
|
||||
|
||||
type policyAgent struct {
|
||||
writer acl.WriteServiceClient
|
||||
checker acl.CheckServiceClient
|
||||
reader acl.ReadServiceClient
|
||||
}
|
||||
|
||||
// NewPolicyAgent returns a gRPC communication functionalities
|
||||
// to communicate with ORY Keto.
|
||||
func NewPolicyAgent(checker acl.CheckServiceClient, writer acl.WriteServiceClient, reader acl.ReadServiceClient) auth.PolicyAgent {
|
||||
return policyAgent{checker: checker, writer: writer, reader: reader}
|
||||
}
|
||||
|
||||
func (pa policyAgent) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
res, err := pa.checker.Check(context.Background(), &acl.CheckRequest{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: getSubject(pr),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errors.ErrAuthorization)
|
||||
}
|
||||
if !res.GetAllowed() {
|
||||
return errors.ErrAuthorization
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa policyAgent) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
var ss *acl.Subject
|
||||
switch isSubjectSet(pr.Subject) {
|
||||
case true:
|
||||
namespace, object, relation := parseSubjectSet(pr.Subject)
|
||||
ss = &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{Namespace: namespace, Object: object, Relation: relation}},
|
||||
}
|
||||
default:
|
||||
ss = &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
trt := pa.writer.TransactRelationTuples
|
||||
_, err := trt(context.Background(), &acl.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*acl.RelationTupleDelta{
|
||||
{
|
||||
Action: acl.RelationTupleDelta_INSERT,
|
||||
RelationTuple: &acl.RelationTuple{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: ss,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (pa policyAgent) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
trt := pa.writer.TransactRelationTuples
|
||||
_, err := trt(context.Background(), &acl.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*acl.RelationTupleDelta{
|
||||
{
|
||||
Action: acl.RelationTupleDelta_DELETE,
|
||||
RelationTuple: &acl.RelationTuple{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: &acl.Subject{Ref: &acl.Subject_Id{
|
||||
Id: pr.Subject,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (pa policyAgent) RetrievePolicies(ctx context.Context, pr auth.PolicyReq) ([]*acl.RelationTuple, error) {
|
||||
var ss *acl.Subject
|
||||
switch isSubjectSet(pr.Subject) {
|
||||
case true:
|
||||
namespace, object, relation := parseSubjectSet(pr.Subject)
|
||||
ss = &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{Namespace: namespace, Object: object, Relation: relation}},
|
||||
}
|
||||
default:
|
||||
ss = &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
res, err := pa.reader.ListRelationTuples(ctx, &acl.ListRelationTuplesRequest{
|
||||
Query: &acl.ListRelationTuplesRequest_Query{
|
||||
Namespace: ketoNamespace,
|
||||
Relation: pr.Relation,
|
||||
Subject: ss,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return []*acl.RelationTuple{}, err
|
||||
}
|
||||
|
||||
tuple := res.GetRelationTuples()
|
||||
for res.NextPageToken != "" {
|
||||
tuple = append(tuple, res.GetRelationTuples()...)
|
||||
}
|
||||
|
||||
return tuple, nil
|
||||
}
|
||||
|
||||
// getSubject returns a 'subject' field for ACL(access control lists).
|
||||
// If the given PolicyReq argument contains a subject as subject set,
|
||||
// it returns subject set; otherwise, it returns a subject.
|
||||
func getSubject(pr auth.PolicyReq) *acl.Subject {
|
||||
if isSubjectSet(pr.Subject) {
|
||||
return &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
return &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
// isSubjectSet returns true when given subject is subject set.
|
||||
// Otherwise, it returns false.
|
||||
func isSubjectSet(subject string) bool {
|
||||
r, err := regexp.Compile(subjectSetRegex)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return r.MatchString(subject)
|
||||
}
|
||||
|
||||
func parseSubjectSet(subjectSet string) (namespace, object, relation string) {
|
||||
r := strings.Split(subjectSet, ":")
|
||||
if len(r) != 2 {
|
||||
return
|
||||
}
|
||||
namespace = r[0]
|
||||
|
||||
r = strings.Split(r[1], "#")
|
||||
if len(r) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
object = r[0]
|
||||
relation = r[1]
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package keto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSubjectSet(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
subjectSet string
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
desc: "check valid subject set",
|
||||
subjectSet: "namespace:object#relation",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing namespace field",
|
||||
subjectSet: ":object#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing object field",
|
||||
subjectSet: "namespace:#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing relation field",
|
||||
subjectSet: "namespace:object#",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, empty subject set",
|
||||
subjectSet: ":#",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing subject set identifier",
|
||||
subjectSet: "namespace:#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing object field",
|
||||
subjectSet: "namespace:object",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, unexpected object field",
|
||||
subjectSet: "namespace:object@relation",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
iss := isSubjectSet(tc.subjectSet)
|
||||
assert.Equal(t, iss, tc.result, fmt.Sprintf("%s expected to be %v, got %v\n", tc.desc, tc.result, iss))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetSubject(t *testing.T) {
|
||||
p1 := auth.PolicyReq{Subject: "subject", Object: "object", Relation: "relation"}
|
||||
s1 := getSubject(p1)
|
||||
ref1 := s1.GetRef()
|
||||
_, ok := ref1.(*acl.Subject_Id)
|
||||
assert.True(t, ok, fmt.Errorf("subject reference of %#v is expected to be (*acl.Subject_Id), got %T", p1, ref1))
|
||||
|
||||
p2 := auth.PolicyReq{Subject: "members:group#access", Object: "object", Relation: "relation"}
|
||||
s2 := getSubject(p2)
|
||||
ref2 := s2.GetRef()
|
||||
_, ok = ref2.(*acl.Subject_Set)
|
||||
assert.True(t, ok, fmt.Errorf("subject reference of %#v is expected to be (*acl.Subject_Set), got %T", p2, ref2))
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidKeyIssuedAt indicates that the Key is being used before it's issued.
|
||||
ErrInvalidKeyIssuedAt = errors.New("invalid issue time")
|
||||
|
||||
// ErrKeyExpired indicates that the Key is expired.
|
||||
ErrKeyExpired = errors.New("use of expired key")
|
||||
|
||||
// ErrAPIKeyExpired indicates that the Key is expired
|
||||
// and that the key type is API key.
|
||||
ErrAPIKeyExpired = errors.New("use of expired API key")
|
||||
)
|
||||
|
||||
const (
|
||||
// LoginKey is temporary User key received on successful login.
|
||||
LoginKey uint32 = iota
|
||||
// RecoveryKey represents a key for resseting password.
|
||||
RecoveryKey
|
||||
// APIKey enables the one to act on behalf of the user.
|
||||
APIKey
|
||||
)
|
||||
|
||||
// Key represents API key.
|
||||
type Key struct {
|
||||
ID string
|
||||
Type uint32
|
||||
IssuerID string
|
||||
Subject string
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// KeyPage contains a page of keys.
|
||||
type KeyPage struct {
|
||||
PageMetadata
|
||||
Keys []Key
|
||||
}
|
||||
|
||||
// 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() {
|
||||
return false
|
||||
}
|
||||
return k.ExpiresAt.UTC().Before(time.Now().UTC())
|
||||
}
|
||||
|
||||
// KeyRepository specifies Key persistence API.
|
||||
type KeyRepository interface {
|
||||
// Save persists the Key. A non-nil error is returned to indicate
|
||||
// operation failure
|
||||
Save(context.Context, Key) (string, error)
|
||||
|
||||
// RetrieveByID retrieves Key by its unique identifier.
|
||||
RetrieveByID(context.Context, string, string) (Key, error)
|
||||
|
||||
// RetrieveAll retrieves all keys for given user ID.
|
||||
RetrieveAll(context.Context, string, PageMetadata) (KeyPage, error)
|
||||
|
||||
// Remove removes Key with provided ID.
|
||||
Remove(context.Context, string, string) error
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExpired(t *testing.T) {
|
||||
exp := time.Now().Add(5 * time.Minute)
|
||||
exp1 := time.Now()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
expired bool
|
||||
}{
|
||||
{
|
||||
desc: "not expired key",
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: exp,
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
{
|
||||
desc: "expired key",
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now().UTC().Add(2 * time.Minute),
|
||||
ExpiresAt: exp1,
|
||||
},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "user key with no expiration date",
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "API key with no expiration date",
|
||||
key: auth.Key{
|
||||
IssuedAt: time.Now(),
|
||||
Type: auth.APIKey,
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res := tc.key.Expired()
|
||||
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
|
||||
}
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ auth.GroupRepository = (*groupRepositoryMock)(nil)
|
||||
|
||||
type groupRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
// Map of groups, group id as a key.
|
||||
// groups map[GroupID]auth.Group
|
||||
groups map[string]auth.Group
|
||||
// Map of groups with group id as key that are
|
||||
// children (i.e. has same parent id) is element
|
||||
// in children's map where parent id is key.
|
||||
// children map[ParentID]map[GroupID]auth.Group
|
||||
children map[string]map[string]auth.Group
|
||||
// Map of parents' id with child group id as key.
|
||||
// Each child has one parent.
|
||||
// parents map[ChildID]ParentID
|
||||
parents map[string]string
|
||||
// Map of groups (with group id as key) which
|
||||
// represent memberships is element in
|
||||
// memberships' map where member id is a key.
|
||||
// memberships map[MemberID]map[GroupID]auth.Group
|
||||
memberships map[string]map[string]auth.Group
|
||||
// Map of group members where member id is a key
|
||||
// is an element in the map members where group id is a key.
|
||||
// members map[type][GroupID]map[MemberID]MemberID
|
||||
members map[string]map[string]map[string]string
|
||||
}
|
||||
|
||||
// NewGroupRepository creates in-memory user repository
|
||||
func NewGroupRepository() auth.GroupRepository {
|
||||
return &groupRepositoryMock{
|
||||
groups: make(map[string]auth.Group),
|
||||
children: make(map[string]map[string]auth.Group),
|
||||
parents: make(map[string]string),
|
||||
memberships: make(map[string]map[string]auth.Group),
|
||||
members: make(map[string]map[string]map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Save(ctx context.Context, group auth.Group) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[group.ID]; ok {
|
||||
return auth.Group{}, errors.ErrConflict
|
||||
}
|
||||
path := group.ID
|
||||
|
||||
if group.ParentID != "" {
|
||||
parent, ok := grm.groups[group.ParentID]
|
||||
if !ok {
|
||||
return auth.Group{}, errors.ErrCreateEntity
|
||||
}
|
||||
if _, ok := grm.children[group.ParentID]; !ok {
|
||||
grm.children[group.ParentID] = make(map[string]auth.Group)
|
||||
}
|
||||
grm.children[group.ParentID][group.ID] = group
|
||||
grm.parents[group.ID] = group.ParentID
|
||||
path = fmt.Sprintf("%s.%s", parent.Path, path)
|
||||
}
|
||||
|
||||
group.Path = path
|
||||
group.Level = len(strings.Split(path, "."))
|
||||
|
||||
grm.groups[group.ID] = group
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Update(ctx context.Context, group auth.Group) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
up, ok := grm.groups[group.ID]
|
||||
if !ok {
|
||||
return auth.Group{}, errors.ErrNotFound
|
||||
}
|
||||
up.Name = group.Name
|
||||
up.Description = group.Description
|
||||
up.Metadata = group.Metadata
|
||||
up.UpdatedAt = time.Now()
|
||||
|
||||
grm.groups[group.ID] = up
|
||||
return up, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[id]; !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
if len(grm.members[id]) > 0 {
|
||||
return auth.ErrGroupNotEmpty
|
||||
}
|
||||
|
||||
// This is not quite exact, it should go in depth
|
||||
for _, ch := range grm.children[id] {
|
||||
if len(grm.members[ch.ID]) > 0 {
|
||||
return auth.ErrGroupNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// This is not quite exact, it should go in depth
|
||||
delete(grm.groups, id)
|
||||
for _, ch := range grm.children[id] {
|
||||
delete(grm.members, ch.ID)
|
||||
}
|
||||
|
||||
delete(grm.children, id)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
|
||||
val, ok := grm.groups[id]
|
||||
if !ok {
|
||||
return auth.Group{}, errors.ErrNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []auth.Group
|
||||
for _, g := range grm.groups {
|
||||
items = append(items, g)
|
||||
}
|
||||
return auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
for _, memberID := range memberIDs {
|
||||
for typ, m := range grm.members[groupID] {
|
||||
_, ok := m[memberID]
|
||||
if !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
delete(grm.members[groupID][typ], memberID)
|
||||
delete(grm.memberships[memberID], groupID)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
if _, ok := grm.members[groupID]; !ok {
|
||||
grm.members[groupID] = make(map[string]map[string]string)
|
||||
}
|
||||
|
||||
for _, memberID := range memberIDs {
|
||||
if _, ok := grm.members[groupID][groupType]; !ok {
|
||||
grm.members[groupID][groupType] = make(map[string]string)
|
||||
}
|
||||
if _, ok := grm.memberships[memberID]; !ok {
|
||||
grm.memberships[memberID] = make(map[string]auth.Group)
|
||||
}
|
||||
|
||||
grm.members[groupID][groupType][memberID] = memberID
|
||||
grm.memberships[memberID][groupID] = grm.groups[groupID]
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []auth.Group
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
i := uint64(0)
|
||||
for _, g := range grm.memberships[memberID] {
|
||||
if i >= first && i < last {
|
||||
items = append(items, g)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Limit: pm.Limit,
|
||||
Offset: pm.Offset,
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []auth.Member
|
||||
members, ok := grm.members[groupID][groupType]
|
||||
if !ok {
|
||||
return auth.MemberPage{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
i := uint64(0)
|
||||
for _, g := range members {
|
||||
if i >= first && i < last {
|
||||
items = append(items, auth.Member{ID: g, Type: groupType})
|
||||
}
|
||||
i++
|
||||
}
|
||||
return auth.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if groupID == "" {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return auth.GroupPage{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
groups := make([]auth.Group, 0)
|
||||
groups, err := grm.getParents(groups, group)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: groups,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(groups)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) getParents(groups []auth.Group, group auth.Group) ([]auth.Group, error) {
|
||||
groups = append(groups, group)
|
||||
parentID, ok := grm.parents[group.ID]
|
||||
if !ok && parentID == "" {
|
||||
return groups, nil
|
||||
}
|
||||
parent, ok := grm.groups[parentID]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("parent with id: %s not found", parentID))
|
||||
}
|
||||
return grm.getParents(groups, parent)
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
|
||||
groups := make([]auth.Group, 0)
|
||||
groups = append(groups, group)
|
||||
for ch := range grm.parents {
|
||||
g, ok := grm.groups[ch]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("child with id %s not found", ch))
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: groups,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(groups)),
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ auth.KeyRepository = (*keyRepositoryMock)(nil)
|
||||
|
||||
type keyRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
keys map[string]auth.Key
|
||||
}
|
||||
|
||||
// NewKeyRepository creates in-memory user repository
|
||||
func NewKeyRepository() auth.KeyRepository {
|
||||
return &keyRepositoryMock{
|
||||
keys: make(map[string]auth.Key),
|
||||
}
|
||||
}
|
||||
|
||||
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 "", errors.ErrConflict
|
||||
}
|
||||
|
||||
krm.keys[key.ID] = key
|
||||
return key.ID, nil
|
||||
}
|
||||
func (krm *keyRepositoryMock) RetrieveByID(ctx context.Context, issuerID, id string) (auth.Key, error) {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
if key, ok := krm.keys[id]; ok && key.IssuerID == issuerID {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return auth.Key{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (krm *keyRepositoryMock) RetrieveAll(ctx context.Context, issuerID string, pm auth.PageMetadata) (auth.KeyPage, error) {
|
||||
krm.mu.Lock()
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
kp := auth.KeyPage{}
|
||||
i := uint64(0)
|
||||
|
||||
for _, k := range krm.keys {
|
||||
if i >= pm.Offset && i < (pm.Limit+pm.Offset) {
|
||||
kp.Keys = append(kp.Keys, k)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
kp.Offset = pm.Offset
|
||||
kp.Limit = pm.Limit
|
||||
kp.Total = uint64(i)
|
||||
|
||||
return kp, nil
|
||||
}
|
||||
|
||||
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.IssuerID == issuerID {
|
||||
delete(krm.keys, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
type MockSubjectSet struct {
|
||||
Object string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type policyAgentMock struct {
|
||||
mu sync.Mutex
|
||||
// authzDb stores 'subject' as a key, and subject policies as a value.
|
||||
authzDB map[string][]MockSubjectSet
|
||||
}
|
||||
|
||||
// NewKetoMock returns a mock service for Keto.
|
||||
// This mock is not implemented yet.
|
||||
func NewKetoMock(db map[string][]MockSubjectSet) auth.PolicyAgent {
|
||||
return &policyAgentMock{authzDB: db}
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
for _, ss := range ssList {
|
||||
if ss.Object == pr.Object && ss.Relation == pr.Relation {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.ErrAuthorization
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
pa.authzDB[pr.Subject] = append(pa.authzDB[pr.Subject], MockSubjectSet{Object: pr.Object, Relation: pr.Relation})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
for k, ss := range ssList {
|
||||
if ss.Object == pr.Object && ss.Relation == pr.Relation {
|
||||
ssList[k] = MockSubjectSet{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) RetrievePolicies(ctx context.Context, pr auth.PolicyReq) ([]*acl.RelationTuple, error) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
tuple := []*acl.RelationTuple{}
|
||||
for _, ss := range ssList {
|
||||
if ss.Relation == pr.Relation {
|
||||
tuple = append(tuple, &acl.RelationTuple{Object: ss.Object, Relation: ss.Relation})
|
||||
}
|
||||
}
|
||||
return tuple, nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
// PolicyReq represents an argument struct for making a policy related
|
||||
// function calls.
|
||||
type PolicyReq struct {
|
||||
Subject string
|
||||
Object string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type PolicyPage struct {
|
||||
Policies []string
|
||||
}
|
||||
|
||||
// Authz represents a authorization service. It exposes
|
||||
// functionalities through `auth` to perform authorization.
|
||||
type Authz interface {
|
||||
// Authorize checks authorization of the given `subject`. Basically,
|
||||
// Authorize verifies that Is `subject` allowed to `relation` on
|
||||
// `object`. Authorize returns a non-nil error if the subject has
|
||||
// no relation on the object (which simply means the operation is
|
||||
// denied).
|
||||
Authorize(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicy creates a policy for the given subject, so that, after
|
||||
// AddPolicy, `subject` has a `relation` on `object`. Returns a non-nil
|
||||
// error in case of failures.
|
||||
AddPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicies adds new policies for given subjects. This method is
|
||||
// only allowed to use as an admin.
|
||||
AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error
|
||||
|
||||
// DeletePolicy removes a policy.
|
||||
DeletePolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// DeletePolicies deletes policies for given subjects. This method is
|
||||
// only allowed to use as an admin.
|
||||
DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error
|
||||
|
||||
// ListPolicies lists policies based on the given PolicyReq structure.
|
||||
ListPolicies(ctx context.Context, pr PolicyReq) (PolicyPage, error)
|
||||
}
|
||||
|
||||
// PolicyAgent facilitates the communication to authorization
|
||||
// services and implements Authz functionalities for certain
|
||||
// authorization services (e.g. ORY Keto).
|
||||
type PolicyAgent interface {
|
||||
// CheckPolicy checks if the subject has a relation on the object.
|
||||
// It returns a non-nil error if the subject has no relation on
|
||||
// the object (which simply means the operation is denied).
|
||||
CheckPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicy creates a policy for the given subject, so that, after
|
||||
// AddPolicy, `subject` has a `relation` on `object`. Returns a non-nil
|
||||
// error in case of failures.
|
||||
AddPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// DeletePolicy removes a policy.
|
||||
DeletePolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
RetrievePolicies(ctx context.Context, pr PolicyReq) ([]*acl.RelationTuple, error)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains Key repository implementations using
|
||||
// PostgreSQL as the underlying database.
|
||||
package postgres
|
||||
@@ -1,752 +0,0 @@
|
||||
// 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/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errStringToUUID = errors.New("error converting string to uuid")
|
||||
errGetTotal = errors.New("failed to get total number of groups")
|
||||
errCreateMetadataQuery = errors.New("failed to create query for metadata")
|
||||
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 {
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pgErr.Code {
|
||||
case pgerrcode.InvalidTextRepresentation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case pgerrcode.ForeignKeyViolation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, err)
|
||||
case pgerrcode.UniqueViolation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
|
||||
case pgerrcode.StringDataRightTruncationDataException:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
}
|
||||
|
||||
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbg = dbGroup{}
|
||||
if err := row.StructScan(&dbg); err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
return toGroup(dbg)
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
|
||||
dbu, err := toDBGroup(g)
|
||||
if err != nil {
|
||||
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
|
||||
if err != nil {
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pgErr.Code {
|
||||
case pgerrcode.InvalidTextRepresentation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case pgerrcode.UniqueViolation:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
|
||||
case pgerrcode.StringDataRightTruncationDataException:
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbu = dbGroup{}
|
||||
if err := row.StructScan(&dbu); err != nil {
|
||||
return g, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
qd := `DELETE FROM groups WHERE id = :id`
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
}
|
||||
dbg, err := toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pqErr.Code {
|
||||
case pgerrcode.InvalidTextRepresentation:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case pgerrcode.ForeignKeyViolation:
|
||||
switch pqErr.ConstraintName {
|
||||
case groupIDFkeyy:
|
||||
return errors.Wrap(auth.ErrGroupNotEmpty, err)
|
||||
}
|
||||
return errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
dbu := dbGroup{
|
||||
ID: id,
|
||||
}
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups WHERE id = $1`
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return auth.Group{}, errors.Wrap(errors.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
_, metaQuery, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
var mq string
|
||||
if metaQuery != "" {
|
||||
mq = fmt.Sprintf(" AND %s", metaQuery)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
|
||||
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := gr.processRows(rows)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
cq := "SELECT COUNT(*) FROM groups"
|
||||
if metaQuery != "" {
|
||||
cq = fmt.Sprintf(" %s WHERE %s", cq, metaQuery)
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level`
|
||||
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path @> parent.path`
|
||||
|
||||
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveParents, err)
|
||||
}
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) < :level`
|
||||
|
||||
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path <@ parent.path `
|
||||
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveChildren, err)
|
||||
}
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) retrieve(ctx context.Context, groupID, retQuery, cntQuery string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
_, mq, err := getGroupsMetadataQuery("g", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
|
||||
retQuery = fmt.Sprintf(`%s %s`, retQuery, mq)
|
||||
cntQuery = fmt.Sprintf(`%s %s`, cntQuery, mq)
|
||||
|
||||
dbPage, err := toDBGroupPage(groupID, "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, retQuery, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := gr.processRows(rows)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cntQuery, dbPage)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Level: pm.Level,
|
||||
Total: total,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
|
||||
}
|
||||
|
||||
func (gr groupRepository) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
|
||||
WHERE gr.group_id = :group_id AND gr.type = :type %s`, mq)
|
||||
|
||||
if groupType == "" {
|
||||
q = fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
|
||||
WHERE gr.group_id = :group_id %s`, mq)
|
||||
}
|
||||
|
||||
params, err := toDBMemberPage("", groupID, groupType, pm)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []auth.Member
|
||||
for rows.Next() {
|
||||
member := dbMember{}
|
||||
if err := rows.StructScan(&member); err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, auth.Member{ID: member.MemberID, Type: member.Type})
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups g, group_relations gr
|
||||
WHERE gr.group_id = :group_id AND gr.group_id = g.id AND gr.type = :type %s;`, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
page := auth.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params, err := toDBMemberPage(memberID, "", "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []auth.Group
|
||||
for rows.Next() {
|
||||
dbg := dbGroup{}
|
||||
if err := rows.StructScan(&dbg); err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
gr, err := toGroup(dbg)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id %s `, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string, ids ...string) error {
|
||||
tx, err := gr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
|
||||
VALUES(:group_id, :member_id, :type, :created_at, :updated_at)`
|
||||
|
||||
for _, id := range ids {
|
||||
dbg, err := toDBGroupRelation(id, groupID, groupType)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
created := time.Now()
|
||||
dbg.CreatedAt = created
|
||||
dbg.UpdatedAt = created
|
||||
|
||||
if _, err := tx.NamedExecContext(ctx, qIns, dbg); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
err = errors.Wrap(err, rollbackErr)
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pgErr.Code {
|
||||
case pgerrcode.InvalidTextRepresentation:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case pgerrcode.ForeignKeyViolation:
|
||||
return errors.Wrap(errors.ErrConflict, errors.New(pgErr.Detail))
|
||||
case pgerrcode.UniqueViolation:
|
||||
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pgErr.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 {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
err = errors.Wrap(rollbackErr, err)
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pgErr.Code {
|
||||
case pgerrcode.InvalidTextRepresentation:
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case pgerrcode.UniqueViolation:
|
||||
return errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbMember struct {
|
||||
MemberID string `db:"member_id"`
|
||||
GroupID string `db:"group_id"`
|
||||
Type string `db:"type"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type dbGroup struct {
|
||||
ID string `db:"id"`
|
||||
ParentID sql.NullString `db:"parent_id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Level int `db:"level"`
|
||||
Path string `db:"path"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type dbGroupPage struct {
|
||||
ID string `db:"id"`
|
||||
ParentID string `db:"parent_id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Path string `db:"path"`
|
||||
Level uint64 `db:"level"`
|
||||
Total uint64 `db:"total"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
}
|
||||
|
||||
type dbMemberPage struct {
|
||||
GroupID string `db:"group_id"`
|
||||
MemberID string `db:"member_id"`
|
||||
Type string `db:"type"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func toUUID(id string) (uuid.NullUUID, error) {
|
||||
var uid uuid.NullUUID
|
||||
if id == "" {
|
||||
return uuid.NullUUID{UUID: uuid.Nil, Valid: false}, nil
|
||||
}
|
||||
err := uid.Scan(id)
|
||||
return uid, err
|
||||
}
|
||||
|
||||
func toString(id uuid.NullUUID) (string, error) {
|
||||
if id.Valid {
|
||||
return id.UUID.String(), nil
|
||||
}
|
||||
if id.UUID == uuid.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return "", errStringToUUID
|
||||
}
|
||||
|
||||
func toDBGroup(g auth.Group) (dbGroup, error) {
|
||||
ownerID, err := toUUID(g.OwnerID)
|
||||
if err != nil {
|
||||
return dbGroup{}, err
|
||||
}
|
||||
|
||||
var parentID sql.NullString
|
||||
if g.ParentID != "" {
|
||||
parentID = sql.NullString{String: g.ParentID, Valid: true}
|
||||
}
|
||||
|
||||
meta := dbMetadata(g.Metadata)
|
||||
|
||||
return dbGroup{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
ParentID: parentID,
|
||||
OwnerID: ownerID,
|
||||
Description: g.Description,
|
||||
Metadata: meta,
|
||||
Path: g.Path,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBGroupPage(id, path string, pm auth.PageMetadata) (dbGroupPage, error) {
|
||||
level := auth.MaxLevel
|
||||
if pm.Level < auth.MaxLevel {
|
||||
level = pm.Level
|
||||
}
|
||||
return dbGroupPage{
|
||||
Metadata: dbMetadata(pm.Metadata),
|
||||
ID: id,
|
||||
Path: path,
|
||||
Level: level,
|
||||
Total: pm.Total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) {
|
||||
return dbMemberPage{
|
||||
GroupID: groupID,
|
||||
MemberID: memberID,
|
||||
Type: groupType,
|
||||
Metadata: dbMetadata(pm.Metadata),
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGroup(dbu dbGroup) (auth.Group, error) {
|
||||
ownerID, err := toString(dbu.OwnerID)
|
||||
if err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
return auth.Group{
|
||||
ID: dbu.ID,
|
||||
Name: dbu.Name,
|
||||
ParentID: dbu.ParentID.String,
|
||||
OwnerID: ownerID,
|
||||
Description: dbu.Description,
|
||||
Metadata: auth.GroupMetadata(dbu.Metadata),
|
||||
Level: dbu.Level,
|
||||
Path: dbu.Path,
|
||||
UpdatedAt: dbu.UpdatedAt,
|
||||
CreatedAt: dbu.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dbGroupRelation struct {
|
||||
GroupID sql.NullString `db:"group_id"`
|
||||
MemberID sql.NullString `db:"member_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Type string `db:"type"`
|
||||
}
|
||||
|
||||
func toDBGroupRelation(memberID, groupID, groupType string) (dbGroupRelation, error) {
|
||||
var grID sql.NullString
|
||||
if groupID != "" {
|
||||
grID = sql.NullString{String: groupID, Valid: true}
|
||||
}
|
||||
|
||||
var mID sql.NullString
|
||||
if memberID != "" {
|
||||
mID = sql.NullString{String: memberID, Valid: true}
|
||||
}
|
||||
|
||||
return dbGroupRelation{
|
||||
GroupID: grID,
|
||||
MemberID: mID,
|
||||
Type: groupType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getGroupsMetadataQuery(db string, m auth.GroupMetadata) (mb []byte, mq string, err error) {
|
||||
if len(m) > 0 {
|
||||
mq = `metadata @> :metadata`
|
||||
if db != "" {
|
||||
mq = db + "." + mq
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, errCreateMetadataQuery)
|
||||
}
|
||||
mb = b
|
||||
}
|
||||
return mb, mq, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) processRows(rows *sqlx.Rows) ([]auth.Group, error) {
|
||||
var items []auth.Group
|
||||
for rows.Next() {
|
||||
dbg := dbGroup{}
|
||||
if err := rows.StructScan(&dbg); err != nil {
|
||||
return items, err
|
||||
}
|
||||
group, err := toGroup(dbg)
|
||||
if err != nil {
|
||||
return items, err
|
||||
}
|
||||
items = append(items, group)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
|
||||
rows, err := db.NamedQueryContext(ctx, query, params)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errGetTotal, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
total := uint64(0)
|
||||
if rows.Next() {
|
||||
if err := rows.Scan(&total); err != nil {
|
||||
return 0, errors.Wrap(errGetTotal, err)
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// dbMetadata type for handling metadata properly in database/sql
|
||||
type dbMetadata map[string]interface{}
|
||||
|
||||
// Scan - Implement the database/sql scanner interface
|
||||
func (m *dbMetadata) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.ErrScanMetadata
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value Implements valuer
|
||||
func (m dbMetadata) Value() (driver.Value, error) {
|
||||
if len(m) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
@@ -1,777 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNameSize = 254
|
||||
maxDescSize = 1024
|
||||
groupName = "Mainflux"
|
||||
description = "description"
|
||||
)
|
||||
|
||||
var (
|
||||
invalidName = strings.Repeat("m", maxNameSize+1)
|
||||
invalidDesc = strings.Repeat("m", maxDescSize+1)
|
||||
metadata = auth.GroupMetadata{
|
||||
"admin": "true",
|
||||
}
|
||||
)
|
||||
|
||||
func generateGroupID(t *testing.T) string {
|
||||
grpID, err := ulidProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
return grpID
|
||||
}
|
||||
|
||||
func TestGroupSave(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
usrID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
wrongID, err := ulidProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
grpID := generateGroupID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "create new group",
|
||||
group: auth.Group{
|
||||
ID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create new group with existing name",
|
||||
group: auth.Group{
|
||||
ID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid name",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
OwnerID: usrID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid description",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: "withParent",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent and existing name",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with wrong parent",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: wrongID,
|
||||
OwnerID: usrID,
|
||||
Name: "wrongParent",
|
||||
},
|
||||
err: errors.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := groupRepo.Save(context.Background(), tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGroupRetrieveByID(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "TestGroupRetrieveByID1",
|
||||
OwnerID: uid,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
retrieved, err := groupRepo.RetrieveByID(context.Background(), group1.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.True(t, retrieved.ID == group1.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group1.ID, retrieved.ID))
|
||||
|
||||
// Round to milliseconds as otherwise saving and retrieving from DB
|
||||
// adds rounding error.
|
||||
creationTime := time.Now().UTC().Round(time.Millisecond)
|
||||
group2 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "TestGroupRetrieveByID",
|
||||
OwnerID: uid,
|
||||
ParentID: group1.ID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group2)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), group2.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.True(t, retrieved.ID == group2.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group2.ID, retrieved.ID))
|
||||
assert.True(t, retrieved.CreatedAt.Equal(creationTime), fmt.Sprintf("Save group, CreatedAt: expected %s got %s\n", creationTime, retrieved.CreatedAt))
|
||||
assert.True(t, retrieved.UpdatedAt.Equal(creationTime), fmt.Sprintf("Save group, UpdatedAt: expected %s got %s\n", creationTime, retrieved.UpdatedAt))
|
||||
assert.True(t, retrieved.Level == 2, fmt.Sprintf("Save group, Level: expected %d got %d\n", retrieved.Level, 2))
|
||||
assert.True(t, retrieved.ParentID == group1.ID, fmt.Sprintf("Save group, Level: expected %s got %s\n", group1.ID, retrieved.ParentID))
|
||||
assert.True(t, retrieved.Description == description, fmt.Sprintf("Save group, Description: expected %v got %v\n", retrieved.Description, description))
|
||||
assert.True(t, retrieved.Path == fmt.Sprintf("%s.%s", group1.ID, group2.ID), fmt.Sprintf("Save group, Path: expected %s got %s\n", fmt.Sprintf("%s.%s", group1.ID, group2.ID), retrieved.Path))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), generateGroupID(t))
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", errors.ErrNotFound, err))
|
||||
}
|
||||
|
||||
func TestGroupUpdate(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
updateTime := time.Now().UTC()
|
||||
groupID := generateGroupID(t)
|
||||
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
Name: groupName + "TestGroupUpdate",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
retrieved, err := groupRepo.RetrieveByID(context.Background(), group.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
groupUpdate auth.Group
|
||||
groupExpected auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update group for existing id",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Name: groupName + "Updated",
|
||||
UpdatedAt: updateTime,
|
||||
Metadata: auth.GroupMetadata{"admin": "false"},
|
||||
},
|
||||
groupExpected: auth.Group{
|
||||
Name: groupName + "Updated",
|
||||
UpdatedAt: updateTime,
|
||||
Metadata: auth.GroupMetadata{"admin": "false"},
|
||||
CreatedAt: retrieved.CreatedAt,
|
||||
Path: retrieved.Path,
|
||||
ParentID: retrieved.ParentID,
|
||||
ID: retrieved.ID,
|
||||
Level: retrieved.Level,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update group for non-existing id",
|
||||
groupUpdate: auth.Group{
|
||||
ID: "wrong",
|
||||
Name: groupName + "-2",
|
||||
},
|
||||
err: errors.ErrUpdateEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid name",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid description",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
updated, err := groupRepo.Update(context.Background(), tc.groupUpdate)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if tc.desc == "update group for existing id" {
|
||||
assert.True(t, updated.Level == tc.groupExpected.Level, fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Level, updated.Level))
|
||||
assert.True(t, updated.Name == tc.groupExpected.Name, fmt.Sprintf("%s:Name: expected %s got %s\n", tc.desc, tc.groupExpected.Name, updated.Name))
|
||||
assert.True(t, updated.Metadata["admin"] == tc.groupExpected.Metadata["admin"], fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Metadata["admin"], updated.Metadata["admin"]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupDelete(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
groupParent := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
groupParent, err = groupRepo.Save(context.Background(), groupParent)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
creationTime = time.Now().UTC()
|
||||
groupChild1 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: groupParent.ID,
|
||||
Name: groupName + "child1",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
creationTime = time.Now().UTC()
|
||||
groupChild2 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: groupParent.ID,
|
||||
Name: groupName + "child2",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
meta := auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
|
||||
groupChild1, err = groupRepo.Save(context.Background(), groupChild1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
groupChild2, err = groupRepo.Save(context.Background(), groupChild2)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
gp, err := groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Retrieve children for parent: expected %v got %v\n", nil, err))
|
||||
assert.True(t, gp.Total == 3, fmt.Sprintf("Number of children + parent: expected %d got %d\n", 3, gp.Total))
|
||||
|
||||
thingID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("thing id create unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), groupChild1.ID, "things", thingID)
|
||||
require.Nil(t, err, fmt.Sprintf("thing assign got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete non empty group: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupChild2.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete empty group: expected %v got %v\n", nil, err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupParent.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete parent with children with members: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
|
||||
|
||||
gp, err = groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("retrieve children after one child removed: expected %v got %v\n", nil, err))
|
||||
assert.True(t, gp.Total == 2, fmt.Sprintf("number of children + parent: expected %d got %d\n", 2, gp.Total))
|
||||
|
||||
err = groupRepo.Unassign(context.Background(), groupChild1.ID, thingID)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to remove thing from a group error: %s", err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupParent.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete parent with children with no members: expected %v got %v\n", nil, err))
|
||||
|
||||
_, err = groupRepo.RetrieveByID(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.PageMetadata{
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
wrongMeta := auth.PageMetadata{
|
||||
Metadata: auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
},
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(auth.MaxLevel)
|
||||
parentID := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if i < metaNum {
|
||||
group.Metadata = metadata.Metadata
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
Size uint64
|
||||
Metadata auth.PageMetadata
|
||||
}{
|
||||
"retrieve all groups": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: n,
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
Size: n,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: metaNum,
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: metaNum,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(0),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: wrongMeta.Metadata,
|
||||
},
|
||||
Size: uint64(0),
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(metaNum),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: uint64(metaNum),
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(metaNum),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: uint64(metaNum),
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAll(context.Background(), tc.Metadata)
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
|
||||
assert.Equal(t, tc.Metadata.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Metadata.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllParents(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.GroupMetadata{
|
||||
"field": "value",
|
||||
}
|
||||
wrongMeta := auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
}
|
||||
|
||||
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.Equal(t, uint64(0), p.Total, fmt.Sprintf("expected total %d got %d\n", 0, p.Total))
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(10)
|
||||
parentID := ""
|
||||
parentMiddle := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if n-i <= metaNum {
|
||||
group.Metadata = metadata
|
||||
}
|
||||
if i == n/2 {
|
||||
parentMiddle = group.ID
|
||||
}
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
level uint64
|
||||
parentID string
|
||||
Size uint64
|
||||
Total uint64
|
||||
Metadata auth.GroupMetadata
|
||||
}{
|
||||
"retrieve all parents": {
|
||||
Total: n,
|
||||
Size: auth.MaxLevel + 1,
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
Total: metaNum,
|
||||
Size: metaNum,
|
||||
Metadata: metadata,
|
||||
parentID: parentID,
|
||||
level: auth.MaxLevel,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
Total: uint64(0),
|
||||
Metadata: wrongMeta,
|
||||
Size: uint64(0),
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
Total: n,
|
||||
Size: 2 + 1,
|
||||
level: uint64(2),
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
Total: metaNum,
|
||||
Size: metaNum,
|
||||
level: 3,
|
||||
Metadata: metadata,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve parent groups from children in the middle": {
|
||||
Total: n/2 + 1,
|
||||
Size: n/2 + 1,
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentMiddle,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAllParents(context.Background(), tc.parentID, auth.PageMetadata{Level: tc.level, Metadata: tc.Metadata})
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
|
||||
assert.Equal(t, tc.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllChildren(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.GroupMetadata{
|
||||
"field": "value",
|
||||
}
|
||||
wrongMeta := auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
}
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(10)
|
||||
groupID := generateGroupID(t)
|
||||
firstParentID := groupID
|
||||
parentID := ""
|
||||
parentMiddle := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if i < metaNum {
|
||||
group.Metadata = metadata
|
||||
}
|
||||
if i == n/2 {
|
||||
parentMiddle = group.ID
|
||||
}
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
groupID = generateGroupID(t)
|
||||
}
|
||||
|
||||
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.Equal(t, n, p.Total, fmt.Sprintf("expected total %d got %d\n", n, p.Total))
|
||||
|
||||
cases := map[string]struct {
|
||||
parentID string
|
||||
size uint64
|
||||
total uint64
|
||||
metadata auth.PageMetadata
|
||||
}{
|
||||
"retrieve all children": {
|
||||
size: auth.MaxLevel,
|
||||
total: n,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
size: metaNum,
|
||||
total: metaNum,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
total: 0,
|
||||
size: 0,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: wrongMeta,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
total: n,
|
||||
size: 2,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: 2,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
total: metaNum,
|
||||
size: metaNum,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: 3,
|
||||
Metadata: metadata,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve parent groups from children in the middle": {
|
||||
total: n / 2,
|
||||
size: n / 2,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
parentID: parentMiddle,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAllChildren(context.Background(), tc.parentID, tc.metadata)
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssign(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
group, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
|
||||
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrMemberAlreadyAssigned), fmt.Sprintf("assign member again: expected %v got %v\n", auth.ErrMemberAlreadyAssigned, err))
|
||||
}
|
||||
|
||||
func TestUnassign(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
group, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
|
||||
|
||||
mid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
|
||||
|
||||
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 2, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 2, mp.Total))
|
||||
|
||||
err = groupRepo.Unassign(context.Background(), group.ID, mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
|
||||
|
||||
mp, err = groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("members retrieve unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
}
|
||||
|
||||
func cleanUp(t *testing.T) {
|
||||
_, err := db.Exec("delete from group_relations")
|
||||
require.Nil(t, err, fmt.Sprintf("clean relations unexpected error: %s", err))
|
||||
_, err = db.Exec("delete from groups")
|
||||
require.Nil(t, err, fmt.Sprintf("clean groups unexpected error: %s", err))
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of Auth service
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &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`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errSave = errors.New("failed to save key in database")
|
||||
errRetrieve = errors.New("failed to retrieve key from database")
|
||||
errDelete = errors.New("failed to delete key from database")
|
||||
)
|
||||
var _ auth.KeyRepository = (*repo)(nil)
|
||||
|
||||
type repo struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// New instantiates a PostgreSQL implementation of key repository.
|
||||
func New(db Database) auth.KeyRepository {
|
||||
return &repo{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == pgerrcode.UniqueViolation {
|
||||
return "", errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
|
||||
return "", errors.Wrap(errSave, err)
|
||||
}
|
||||
|
||||
return dbKey.ID, nil
|
||||
}
|
||||
|
||||
func (kr repo) RetrieveByID(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, issuerID, id).StructScan(&key); err != nil {
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if err == sql.ErrNoRows || ok && pgerrcode.InvalidTextRepresentation == pgErr.Code {
|
||||
return auth.Key{}, errors.Wrap(errors.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return auth.Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return toKey(key), nil
|
||||
}
|
||||
|
||||
func (kr repo) RetrieveAll(ctx context.Context, issuerID string, pm auth.PageMetadata) (auth.KeyPage, error) {
|
||||
var query []string
|
||||
var emq string
|
||||
query = append(query, fmt.Sprintf("issuer_id = '%s'", issuerID))
|
||||
if pm.Type != 0 {
|
||||
query = append(query, fmt.Sprintf("type = '%d'", pm.Type))
|
||||
}
|
||||
if pm.Subject != "" {
|
||||
query = append(query, fmt.Sprintf("subject = '%s'", pm.Subject))
|
||||
}
|
||||
if len(query) > 0 {
|
||||
emq = fmt.Sprintf(" WHERE %s", strings.Join(query, " AND "))
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, type, issuer_id, subject, issued_at, expires_at FROM keys %s ORDER BY issued_at LIMIT :limit OFFSET :offset;`, emq)
|
||||
params := map[string]interface{}{
|
||||
"limit": pm.Limit,
|
||||
"offset": pm.Offset,
|
||||
}
|
||||
|
||||
rows, err := kr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return auth.KeyPage{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []auth.Key
|
||||
for rows.Next() {
|
||||
dbkey := dbKey{}
|
||||
if err := rows.StructScan(&dbkey); err != nil {
|
||||
return auth.KeyPage{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
key := toKey(dbkey)
|
||||
items = append(items, key)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM keys %s;`, emq)
|
||||
|
||||
total, err := total(ctx, kr.db, cq, params)
|
||||
if err != nil {
|
||||
return auth.KeyPage{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
page := auth.KeyPage{
|
||||
Keys: items,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
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,
|
||||
IssuerID: issuerID,
|
||||
}
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, key); err != nil {
|
||||
return errors.Wrap(errDelete, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbKey struct {
|
||||
ID string `db:"id"`
|
||||
Type uint32 `db:"type"`
|
||||
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 auth.Key) dbKey {
|
||||
ret := dbKey{
|
||||
ID: key.ID,
|
||||
Type: key.Type,
|
||||
IssuerID: key.IssuerID,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
ret.ExpiresAt = sql.NullTime{Time: key.ExpiresAt, Valid: true}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func toKey(key dbKey) auth.Key {
|
||||
ret := auth.Key{
|
||||
ID: key.ID,
|
||||
Type: key.Type,
|
||||
IssuerID: key.IssuerID,
|
||||
Subject: key.Subject,
|
||||
IssuedAt: key.IssuedAt,
|
||||
}
|
||||
if key.ExpiresAt.Valid {
|
||||
ret.ExpiresAt = key.ExpiresAt.Time
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
// 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()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a new key",
|
||||
key: auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: id,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save with duplicate id",
|
||||
key: auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: id,
|
||||
},
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(context.Background(), tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyRetrieveByID(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
id, err := idProvider.ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: id,
|
||||
}
|
||||
_, err = repo.Save(context.Background(), key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
owner string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: key.ID,
|
||||
owner: key.IssuerID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve key with empty issuer id",
|
||||
id: key.ID,
|
||||
owner: "",
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existent key",
|
||||
id: "",
|
||||
owner: key.IssuerID,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(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 TestKeyRetrieveAll(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
issuerID1, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
issuerID2, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
n := uint64(100)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
id, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
key := auth.Key{
|
||||
Subject: fmt.Sprintf("key-%d@email.com", i),
|
||||
IssuedAt: time.Now(),
|
||||
ExpiresAt: expTime,
|
||||
ID: id,
|
||||
IssuerID: issuerID1,
|
||||
Type: auth.LoginKey,
|
||||
}
|
||||
if i%10 == 0 {
|
||||
key.Type = auth.APIKey
|
||||
}
|
||||
if i == n-1 {
|
||||
key.IssuerID = issuerID2
|
||||
}
|
||||
_, err = repo.Save(context.Background(), key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Storing Key expected to succeed: %s", err))
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
pageMetadata auth.PageMetadata
|
||||
size uint64
|
||||
}{
|
||||
"retrieve all keys": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Total: n,
|
||||
},
|
||||
size: n - 1,
|
||||
},
|
||||
"retrieve all keys with different issuer ID": {
|
||||
owner: issuerID2,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Total: n,
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
"retrieve subset of keys with existing owner": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: n/2 - 1,
|
||||
Limit: n,
|
||||
Total: n,
|
||||
},
|
||||
size: n / 2,
|
||||
},
|
||||
"retrieve keys with existing subject": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Subject: "key-10@email.com",
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
"retrieve keys with non-existing subject": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Subject: "wrong",
|
||||
Total: 0,
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
"retrieve keys with existing type": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Type: auth.APIKey,
|
||||
},
|
||||
size: 10,
|
||||
},
|
||||
"retrieve keys with non-existing type": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Total: 0,
|
||||
Type: uint32(9),
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
"retrieve all keys with existing subject and type": {
|
||||
owner: issuerID1,
|
||||
pageMetadata: auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Subject: "key-10@email.com",
|
||||
Type: auth.APIKey,
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := repo.RetrieveAll(context.Background(), tc.owner, tc.pageMetadata)
|
||||
size := uint64(len(page.Keys))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
|
||||
// assert.Equal(t, tc.pageMetadata.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.pageMetadata.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyRemove(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.New(dbMiddleware)
|
||||
|
||||
id, err := idProvider.ID()
|
||||
assert.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))
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
var _ Database = (*database)(nil)
|
||||
|
||||
type database struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// Database provides a database interface
|
||||
type Database interface {
|
||||
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
|
||||
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
|
||||
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
|
||||
func NewDatabase(db *sqlx.DB) Database {
|
||||
return &database{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (d database) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row {
|
||||
addSpanTags(ctx, query)
|
||||
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 {
|
||||
span.SetTag("sql.statement", query)
|
||||
span.SetTag("span.kind", "client")
|
||||
span.SetTag("peer.service", "postgres")
|
||||
span.SetTag("db.type", "sql")
|
||||
}
|
||||
}
|
||||
-453
@@ -1,453 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/ulid"
|
||||
)
|
||||
|
||||
const (
|
||||
recoveryDuration = 5 * time.Minute
|
||||
thingsGroupType = "things"
|
||||
|
||||
authoritiesObject = "authorities"
|
||||
memberRelation = "member"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFailedToRetrieveMembers failed to retrieve group members.
|
||||
ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members")
|
||||
|
||||
// ErrFailedToRetrieveMembership failed to retrieve memberships
|
||||
ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships")
|
||||
|
||||
// ErrFailedToRetrieveAll failed to retrieve groups.
|
||||
ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups")
|
||||
|
||||
// ErrFailedToRetrieveParents failed to retrieve groups.
|
||||
ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups")
|
||||
|
||||
// ErrFailedToRetrieveChildren failed to retrieve groups.
|
||||
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new login key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
)
|
||||
|
||||
// Authn specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
type Authn interface {
|
||||
// Issue issues a new Key, returning its token value alongside.
|
||||
Issue(ctx context.Context, token string, key Key) (Key, string, error)
|
||||
|
||||
// Revoke removes the Key with the provided id that is
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(ctx context.Context, token, id string) error
|
||||
|
||||
// RetrieveKey retrieves data for the Key identified by the provided
|
||||
// ID, that is issued by the user identified by the provided key.
|
||||
RetrieveKey(ctx context.Context, token, id string) (Key, error)
|
||||
|
||||
// RetrieveKeys retrieves data for the Keys that are
|
||||
// issued by the user identified by the provided key.
|
||||
RetrieveKeys(ctx context.Context, token string, pm PageMetadata) (KeyPage, error)
|
||||
|
||||
// Identify validates token token. If token is valid, content
|
||||
// is returned. If token is invalid, or invocation failed for some
|
||||
// other reason, non-nil error value is returned in response.
|
||||
Identify(ctx context.Context, token string) (Identity, error)
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
type Service interface {
|
||||
Authn
|
||||
Authz
|
||||
|
||||
// GroupService implements groups API, creating groups, assigning members
|
||||
GroupService
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
groups GroupRepository
|
||||
idProvider mainflux.IDProvider
|
||||
ulidProvider mainflux.IDProvider
|
||||
agent PolicyAgent
|
||||
tokenizer Tokenizer
|
||||
loginDuration time.Duration
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer, policyAgent PolicyAgent, duration time.Duration) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
groups: groups,
|
||||
idProvider: idp,
|
||||
ulidProvider: ulid.New(),
|
||||
agent: policyAgent,
|
||||
loginDuration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Issue(ctx context.Context, token string, key Key) (Key, string, error) {
|
||||
if key.IssuedAt.IsZero() {
|
||||
return Key{}, "", ErrInvalidKeyIssuedAt
|
||||
}
|
||||
switch key.Type {
|
||||
case APIKey:
|
||||
return svc.userKey(ctx, token, key)
|
||||
case RecoveryKey:
|
||||
return svc.tmpKey(recoveryDuration, key)
|
||||
default:
|
||||
return svc.tmpKey(svc.loginDuration, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, token, id string) error {
|
||||
issuerID, _, err := svc.login(token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
if err := svc.keys.Remove(ctx, issuerID, id); err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
|
||||
issuerID, _, err := svc.login(token)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return svc.keys.RetrieveByID(ctx, issuerID, id)
|
||||
}
|
||||
|
||||
func (svc service) RetrieveKeys(ctx context.Context, token string, pm PageMetadata) (KeyPage, error) {
|
||||
issuerID, _, err := svc.login(token)
|
||||
if err != nil {
|
||||
return KeyPage{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return svc.keys.RetrieveAll(ctx, issuerID, pm)
|
||||
}
|
||||
|
||||
func (svc service) Identify(ctx context.Context, token string) (Identity, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
if err == ErrAPIKeyExpired {
|
||||
err = svc.keys.Remove(ctx, key.IssuerID, key.ID)
|
||||
return Identity{}, errors.Wrap(ErrAPIKeyExpired, err)
|
||||
}
|
||||
if err != nil {
|
||||
return Identity{}, errors.Wrap(errIdentify, err)
|
||||
}
|
||||
|
||||
switch key.Type {
|
||||
case RecoveryKey, LoginKey:
|
||||
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
||||
case APIKey:
|
||||
_, err := svc.keys.RetrieveByID(context.TODO(), key.IssuerID, key.ID)
|
||||
if err != nil {
|
||||
return Identity{}, errors.ErrAuthentication
|
||||
}
|
||||
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
||||
default:
|
||||
return Identity{}, errors.ErrAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Authorize(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.CheckPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) AddPolicy(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Authorize(ctx, PolicyReq{Object: authoritiesObject, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, subjectID := range subjectIDs {
|
||||
for _, relation := range relations {
|
||||
if err := svc.AddPolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add '%s' policy on object '%s' for subject '%s': %w", relation, object, subjectID, err), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) DeletePolicy(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user identified by token is the admin.
|
||||
if err := svc.Authorize(ctx, PolicyReq{Object: authoritiesObject, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, subjectID := range subjectIDs {
|
||||
for _, relation := range relations {
|
||||
if err := svc.DeletePolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy on object '%s' for subject '%s': %w", relation, object, subjectID, err), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.agent.AddPolicy(ctx, PolicyReq{Object: thingGroupID, Relation: memberRelation, Subject: fmt.Sprintf("%s:%s#%s", "members", userGroupID, memberRelation)})
|
||||
}
|
||||
|
||||
func (svc service) ListPolicies(ctx context.Context, pr PolicyReq) (PolicyPage, error) {
|
||||
res, err := svc.agent.RetrievePolicies(ctx, pr)
|
||||
if err != nil {
|
||||
return PolicyPage{}, err
|
||||
}
|
||||
var page PolicyPage
|
||||
for _, tuple := range res {
|
||||
page.Policies = append(page.Policies, tuple.GetObject())
|
||||
}
|
||||
return page, err
|
||||
}
|
||||
|
||||
func (svc service) tmpKey(duration time.Duration, key Key) (Key, string, error) {
|
||||
key.ExpiresAt = key.IssuedAt.Add(duration)
|
||||
secret, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
return key, secret, nil
|
||||
}
|
||||
|
||||
func (svc service) userKey(ctx context.Context, token string, key Key) (Key, string, error) {
|
||||
id, sub, err := svc.login(token)
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
key.IssuerID = id
|
||||
if key.Subject == "" {
|
||||
key.Subject = sub
|
||||
}
|
||||
|
||||
keyID, err := svc.idProvider.ID()
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.ID = keyID
|
||||
|
||||
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
secret, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, "", errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
return key, secret, nil
|
||||
}
|
||||
|
||||
func (svc service) login(token string) (string, string, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Only login key token is valid for login.
|
||||
if key.Type != LoginKey || key.IssuerID == "" {
|
||||
return "", "", errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return key.IssuerID, key.Subject, nil
|
||||
}
|
||||
|
||||
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
ulid, err := svc.ulidProvider.ID()
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
timestamp := getTimestmap()
|
||||
group.UpdatedAt = timestamp
|
||||
group.CreatedAt = timestamp
|
||||
|
||||
group.ID = ulid
|
||||
group.OwnerID = user.ID
|
||||
|
||||
group, err = svc.groups.Save(ctx, group)
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: group.ID, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (svc service) ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAll(ctx, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListParents(ctx context.Context, token string, childID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAllParents(ctx, childID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListChildren(ctx context.Context, token string, parentID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAllChildren(ctx, parentID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListMembers(ctx context.Context, token string, groupID, groupType string, pm PageMetadata) (MemberPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return MemberPage{}, err
|
||||
}
|
||||
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
|
||||
if err != nil {
|
||||
return MemberPage{}, errors.Wrap(ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.groups.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
group.UpdatedAt = getTimestmap()
|
||||
return svc.groups.Update(ctx, group)
|
||||
}
|
||||
|
||||
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
return svc.groups.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) Assign(ctx context.Context, token string, groupID, groupType string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.groups.Assign(ctx, groupID, groupType, memberIDs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if groupType == thingsGroupType {
|
||||
ss := fmt.Sprintf("%s:%s#%s", "members", groupID, memberRelation)
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
for _, action := range []string{"read", "write", "delete"} {
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: memberID, Relation: action, Subject: ss}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add thing: '%s' to thing group: '%s'", memberID, groupID), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: groupID, Relation: memberRelation, Subject: memberID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add user: '%s' to user group: '%s'", memberID, groupID), errs)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ss := fmt.Sprintf("%s:%s#%s", "members", groupID, memberRelation)
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
// If the member is a user, <groupID>#member@memberID must be deleted.
|
||||
if err := svc.agent.DeletePolicy(ctx, PolicyReq{Object: groupID, Relation: memberRelation, Subject: memberID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete a membership of member '%s' from group '%s'", memberID, groupID), errs)
|
||||
}
|
||||
|
||||
// If the member is a Thing, memberID#read|write|delete@(members:groupID#member) must be deleted.
|
||||
for _, action := range []string{"read", "write", "delete"} {
|
||||
if err := svc.agent.DeletePolicy(ctx, PolicyReq{Object: memberID, Relation: action, Subject: ss}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy from member '%s'", action, memberID), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := svc.groups.Unassign(ctx, groupID, memberIDs...)
|
||||
return errors.Wrap(err, errs)
|
||||
}
|
||||
|
||||
func (svc service) ListMemberships(ctx context.Context, token string, memberID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.Memberships(ctx, memberID, pm)
|
||||
}
|
||||
|
||||
func getTimestmap() time.Time {
|
||||
return time.Now().UTC().Round(time.Millisecond)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
// Tokenizer specifies API for encoding and decoding between string and Key.
|
||||
type Tokenizer interface {
|
||||
// Issue converts API Key to its string representation.
|
||||
Issue(Key) (string, error)
|
||||
|
||||
// Parse extracts API Key data from string token.
|
||||
Parse(string) (Key, error)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing contains middlewares that will add spans to existing traces.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/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...)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing contains middlewares that will add spans
|
||||
// to existing traces.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
saveOp = "save"
|
||||
retrieveOp = "retrieve_by_id"
|
||||
retrieveAllOp = "retrieve_all"
|
||||
revokeOp = "remove"
|
||||
)
|
||||
|
||||
var _ auth.KeyRepository = (*keyRepositoryMiddleware)(nil)
|
||||
|
||||
// keyRepositoryMiddleware tracks request and their latency, and adds spans
|
||||
// to context.
|
||||
type keyRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo auth.KeyRepository
|
||||
}
|
||||
|
||||
// New tracks request and their latency, and adds spans
|
||||
// to context.
|
||||
func New(tracer opentracing.Tracer, repo auth.KeyRepository) auth.KeyRepository {
|
||||
return keyRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return krm.repo.Save(ctx, key)
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) RetrieveByID(ctx context.Context, owner, id string) (auth.Key, error) {
|
||||
span := createSpan(ctx, krm.tracer, retrieveOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return krm.repo.RetrieveByID(ctx, owner, id)
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) RetrieveAll(ctx context.Context, owner string, pm auth.PageMetadata) (auth.KeyPage, error) {
|
||||
span := createSpan(ctx, krm.tracer, retrieveAllOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return krm.repo.RetrieveAll(ctx, owner, pm)
|
||||
}
|
||||
|
||||
func (krm keyRepositoryMiddleware) Remove(ctx context.Context, owner, id string) error {
|
||||
span := createSpan(ctx, krm.tracer, revokeOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return krm.repo.Remove(ctx, owner, id)
|
||||
}
|
||||
|
||||
func createSpan(ctx context.Context, tracer opentracing.Tracer, opName string) opentracing.Span {
|
||||
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
|
||||
return tracer.StartSpan(
|
||||
opName,
|
||||
opentracing.ChildOf(parentSpan.Context()),
|
||||
)
|
||||
}
|
||||
|
||||
return tracer.StartSpan(opName)
|
||||
}
|
||||
-552
@@ -1,552 +0,0 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.12
|
||||
// source: auth.proto
|
||||
|
||||
package mainflux
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// ThingsServiceClient is the client API for ThingsService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ThingsServiceClient interface {
|
||||
CanAccessByKey(ctx context.Context, in *AccessByKeyReq, opts ...grpc.CallOption) (*ThingID, error)
|
||||
IsChannelOwner(ctx context.Context, in *ChannelOwnerReq, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
CanAccessByID(ctx context.Context, in *AccessByIDReq, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Identify(ctx context.Context, in *Token, opts ...grpc.CallOption) (*ThingID, error)
|
||||
}
|
||||
|
||||
type thingsServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewThingsServiceClient(cc grpc.ClientConnInterface) ThingsServiceClient {
|
||||
return &thingsServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *thingsServiceClient) CanAccessByKey(ctx context.Context, in *AccessByKeyReq, opts ...grpc.CallOption) (*ThingID, error) {
|
||||
out := new(ThingID)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.ThingsService/CanAccessByKey", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *thingsServiceClient) IsChannelOwner(ctx context.Context, in *ChannelOwnerReq, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.ThingsService/IsChannelOwner", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *thingsServiceClient) CanAccessByID(ctx context.Context, in *AccessByIDReq, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.ThingsService/CanAccessByID", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *thingsServiceClient) Identify(ctx context.Context, in *Token, opts ...grpc.CallOption) (*ThingID, error) {
|
||||
out := new(ThingID)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.ThingsService/Identify", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ThingsServiceServer is the server API for ThingsService service.
|
||||
// All implementations must embed UnimplementedThingsServiceServer
|
||||
// for forward compatibility
|
||||
type ThingsServiceServer interface {
|
||||
CanAccessByKey(context.Context, *AccessByKeyReq) (*ThingID, error)
|
||||
IsChannelOwner(context.Context, *ChannelOwnerReq) (*emptypb.Empty, error)
|
||||
CanAccessByID(context.Context, *AccessByIDReq) (*emptypb.Empty, error)
|
||||
Identify(context.Context, *Token) (*ThingID, error)
|
||||
mustEmbedUnimplementedThingsServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedThingsServiceServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedThingsServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedThingsServiceServer) CanAccessByKey(context.Context, *AccessByKeyReq) (*ThingID, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CanAccessByKey not implemented")
|
||||
}
|
||||
func (UnimplementedThingsServiceServer) IsChannelOwner(context.Context, *ChannelOwnerReq) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method IsChannelOwner not implemented")
|
||||
}
|
||||
func (UnimplementedThingsServiceServer) CanAccessByID(context.Context, *AccessByIDReq) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CanAccessByID not implemented")
|
||||
}
|
||||
func (UnimplementedThingsServiceServer) Identify(context.Context, *Token) (*ThingID, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Identify not implemented")
|
||||
}
|
||||
func (UnimplementedThingsServiceServer) mustEmbedUnimplementedThingsServiceServer() {}
|
||||
|
||||
// UnsafeThingsServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ThingsServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeThingsServiceServer interface {
|
||||
mustEmbedUnimplementedThingsServiceServer()
|
||||
}
|
||||
|
||||
func RegisterThingsServiceServer(s grpc.ServiceRegistrar, srv ThingsServiceServer) {
|
||||
s.RegisterService(&ThingsService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ThingsService_CanAccessByKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AccessByKeyReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ThingsServiceServer).CanAccessByKey(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.ThingsService/CanAccessByKey",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ThingsServiceServer).CanAccessByKey(ctx, req.(*AccessByKeyReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ThingsService_IsChannelOwner_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ChannelOwnerReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ThingsServiceServer).IsChannelOwner(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.ThingsService/IsChannelOwner",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ThingsServiceServer).IsChannelOwner(ctx, req.(*ChannelOwnerReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ThingsService_CanAccessByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AccessByIDReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ThingsServiceServer).CanAccessByID(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.ThingsService/CanAccessByID",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ThingsServiceServer).CanAccessByID(ctx, req.(*AccessByIDReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ThingsService_Identify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Token)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ThingsServiceServer).Identify(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.ThingsService/Identify",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ThingsServiceServer).Identify(ctx, req.(*Token))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ThingsService_ServiceDesc is the grpc.ServiceDesc for ThingsService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ThingsService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "mainflux.ThingsService",
|
||||
HandlerType: (*ThingsServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "CanAccessByKey",
|
||||
Handler: _ThingsService_CanAccessByKey_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "IsChannelOwner",
|
||||
Handler: _ThingsService_IsChannelOwner_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CanAccessByID",
|
||||
Handler: _ThingsService_CanAccessByID_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Identify",
|
||||
Handler: _ThingsService_Identify_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "auth.proto",
|
||||
}
|
||||
|
||||
// AuthServiceClient is the client API for AuthService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AuthServiceClient interface {
|
||||
Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error)
|
||||
Identify(ctx context.Context, in *Token, opts ...grpc.CallOption) (*UserIdentity, error)
|
||||
Authorize(ctx context.Context, in *AuthorizeReq, opts ...grpc.CallOption) (*AuthorizeRes, error)
|
||||
AddPolicy(ctx context.Context, in *AddPolicyReq, opts ...grpc.CallOption) (*AddPolicyRes, error)
|
||||
DeletePolicy(ctx context.Context, in *DeletePolicyReq, opts ...grpc.CallOption) (*DeletePolicyRes, error)
|
||||
ListPolicies(ctx context.Context, in *ListPoliciesReq, opts ...grpc.CallOption) (*ListPoliciesRes, error)
|
||||
Assign(ctx context.Context, in *Assignment, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Members(ctx context.Context, in *MembersReq, opts ...grpc.CallOption) (*MembersRes, error)
|
||||
}
|
||||
|
||||
type authServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient {
|
||||
return &authServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *authServiceClient) Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error) {
|
||||
out := new(Token)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/Issue", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) Identify(ctx context.Context, in *Token, opts ...grpc.CallOption) (*UserIdentity, error) {
|
||||
out := new(UserIdentity)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/Identify", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) Authorize(ctx context.Context, in *AuthorizeReq, opts ...grpc.CallOption) (*AuthorizeRes, error) {
|
||||
out := new(AuthorizeRes)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/Authorize", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) AddPolicy(ctx context.Context, in *AddPolicyReq, opts ...grpc.CallOption) (*AddPolicyRes, error) {
|
||||
out := new(AddPolicyRes)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/AddPolicy", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) DeletePolicy(ctx context.Context, in *DeletePolicyReq, opts ...grpc.CallOption) (*DeletePolicyRes, error) {
|
||||
out := new(DeletePolicyRes)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/DeletePolicy", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) ListPolicies(ctx context.Context, in *ListPoliciesReq, opts ...grpc.CallOption) (*ListPoliciesRes, error) {
|
||||
out := new(ListPoliciesRes)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/ListPolicies", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) Assign(ctx context.Context, in *Assignment, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/Assign", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) Members(ctx context.Context, in *MembersReq, opts ...grpc.CallOption) (*MembersRes, error) {
|
||||
out := new(MembersRes)
|
||||
err := c.cc.Invoke(ctx, "/mainflux.AuthService/Members", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AuthServiceServer is the server API for AuthService service.
|
||||
// All implementations must embed UnimplementedAuthServiceServer
|
||||
// for forward compatibility
|
||||
type AuthServiceServer interface {
|
||||
Issue(context.Context, *IssueReq) (*Token, error)
|
||||
Identify(context.Context, *Token) (*UserIdentity, error)
|
||||
Authorize(context.Context, *AuthorizeReq) (*AuthorizeRes, error)
|
||||
AddPolicy(context.Context, *AddPolicyReq) (*AddPolicyRes, error)
|
||||
DeletePolicy(context.Context, *DeletePolicyReq) (*DeletePolicyRes, error)
|
||||
ListPolicies(context.Context, *ListPoliciesReq) (*ListPoliciesRes, error)
|
||||
Assign(context.Context, *Assignment) (*emptypb.Empty, error)
|
||||
Members(context.Context, *MembersReq) (*MembersRes, error)
|
||||
mustEmbedUnimplementedAuthServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAuthServiceServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedAuthServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedAuthServiceServer) Issue(context.Context, *IssueReq) (*Token, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Issue not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) Identify(context.Context, *Token) (*UserIdentity, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Identify not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) Authorize(context.Context, *AuthorizeReq) (*AuthorizeRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) AddPolicy(context.Context, *AddPolicyReq) (*AddPolicyRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) DeletePolicy(context.Context, *DeletePolicyReq) (*DeletePolicyRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeletePolicy not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) ListPolicies(context.Context, *ListPoliciesReq) (*ListPoliciesRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPolicies not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) Assign(context.Context, *Assignment) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Assign not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) Members(context.Context, *MembersReq) (*MembersRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Members not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
|
||||
|
||||
// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AuthServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAuthServiceServer interface {
|
||||
mustEmbedUnimplementedAuthServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
|
||||
s.RegisterService(&AuthService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AuthService_Issue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(IssueReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).Issue(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/Issue",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).Issue(ctx, req.(*IssueReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_Identify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Token)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).Identify(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/Identify",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).Identify(ctx, req.(*Token))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuthorizeReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).Authorize(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/Authorize",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).Authorize(ctx, req.(*AuthorizeReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_AddPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AddPolicyReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).AddPolicy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/AddPolicy",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).AddPolicy(ctx, req.(*AddPolicyReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_DeletePolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeletePolicyReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).DeletePolicy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/DeletePolicy",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).DeletePolicy(ctx, req.(*DeletePolicyReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_ListPolicies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListPoliciesReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).ListPolicies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/ListPolicies",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).ListPolicies(ctx, req.(*ListPoliciesReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_Assign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Assignment)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).Assign(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/Assign",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).Assign(ctx, req.(*Assignment))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_Members_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MembersReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).Members(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mainflux.AuthService/Members",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).Members(ctx, req.(*MembersReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AuthService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "mainflux.AuthService",
|
||||
HandlerType: (*AuthServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Issue",
|
||||
Handler: _AuthService_Issue_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Identify",
|
||||
Handler: _AuthService_Identify_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Authorize",
|
||||
Handler: _AuthService_Authorize_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddPolicy",
|
||||
Handler: _AuthService_AddPolicy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeletePolicy",
|
||||
Handler: _AuthService_DeletePolicy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListPolicies",
|
||||
Handler: _AuthService_ListPolicies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Assign",
|
||||
Handler: _AuthService_Assign_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Members",
|
||||
Handler: _AuthService_Members_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "auth.proto",
|
||||
}
|
||||
+5
-4
@@ -62,8 +62,9 @@ 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_AUTH_GRPC_URL | Auth service gRPC URL | localhost:7001 |
|
||||
| MF_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
|
||||
| MF_AUTH_GRPC_URL | Users service gRPC URL | localhost:7001 |
|
||||
| MF_AUTH_GRPC_TIMEOUT | Users service gRPC request timeout in seconds | 1s |
|
||||
| MF_SEND_TELEMETRY | Send telemetry to mainflux call home server | true |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -104,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_AUTH_GRPC_URL=[Auth service gRPC URL] \
|
||||
MF_AUTH_GRPC_TIMEOUT=[Auth service gRPC request timeout in seconds] \
|
||||
MF_AUTH_GRPC_URL=[Users service gRPC URL] \
|
||||
MF_AUTH_GRPC_TIMEOUT=[Users service gRPC request timeout in seconds] \
|
||||
$GOBIN/mainflux-bootstrap
|
||||
```
|
||||
|
||||
|
||||
@@ -12,24 +12,29 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
bsapi "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
mfclients "github.com/mainflux/mainflux/pkg/clients"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfgroups "github.com/mainflux/mainflux/pkg/groups"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/mainflux/mainflux/things/clients"
|
||||
capi "github.com/mainflux/mainflux/things/clients/api"
|
||||
"github.com/mainflux/mainflux/things/groups"
|
||||
gapi "github.com/mainflux/mainflux/things/groups/api"
|
||||
tpolicies "github.com/mainflux/mainflux/things/policies"
|
||||
papi "github.com/mainflux/mainflux/things/policies/api/http"
|
||||
upolicies "github.com/mainflux/mainflux/users/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -163,7 +168,7 @@ func dec(in []byte) ([]byte, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
func newService(auth upolicies.AuthServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
ThingsURL: url,
|
||||
@@ -173,11 +178,11 @@ func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
return bootstrap.New(auth, things, sdk, encKey)
|
||||
}
|
||||
|
||||
func generateChannels() map[string]things.Channel {
|
||||
channels := make(map[string]things.Channel, channelsNum)
|
||||
func generateChannels() map[string]mfgroups.Group {
|
||||
channels := make(map[string]mfgroups.Group, channelsNum)
|
||||
for i := 0; i < channelsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
channels[id] = things.Channel{
|
||||
channels[id] = mfgroups.Group{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Metadata: metadata,
|
||||
@@ -186,18 +191,24 @@ func generateChannels() map[string]things.Channel {
|
||||
return channels
|
||||
}
|
||||
|
||||
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, generateChannels(), auth)
|
||||
func newThingsService(auth upolicies.AuthServiceClient) (clients.Service, groups.Service, tpolicies.Service) {
|
||||
csvc := mocks.NewThingsService(map[string]mfclients.Client{}, auth)
|
||||
gsvc := mocks.NewChannelsService(generateChannels(), auth)
|
||||
psvc := mocks.NewPoliciesService(auth)
|
||||
return csvc, gsvc, psvc
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := thingsapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
func newThingsServer(csvc clients.Service, gsvc groups.Service, psvc tpolicies.Service) *httptest.Server {
|
||||
logger := mflog.NewMock()
|
||||
mux := bone.New()
|
||||
capi.MakeHandler(csvc, mux, logger)
|
||||
gapi.MakeHandler(gsvc, mux, logger)
|
||||
papi.MakeHandler(csvc, psvc, mux, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newBootstrapServer(svc bootstrap.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
logger := mflog.NewMock()
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader(encKey), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
@@ -1155,7 +1166,7 @@ func TestBootstrap(t *testing.T) {
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
if tc.secure && tc.status == http.StatusOK {
|
||||
body, err = dec(body)
|
||||
|
||||
+25
-11
@@ -11,24 +11,26 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
logger mflog.Logger
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Service {
|
||||
// LoggingMiddleware adds logging facilities to the bootstrap service.
|
||||
func LoggingMiddleware(svc bootstrap.Service, logger mflog.Logger) bootstrap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
// Add logs the add request. It logs the thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Add(ctx context.Context, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
message := fmt.Sprintf("Method add using token %s with thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -39,9 +41,11 @@ func (lm *loggingMiddleware) Add(ctx context.Context, token string, cfg bootstra
|
||||
return lm.svc.Add(ctx, token, cfg)
|
||||
}
|
||||
|
||||
// View logs the view request. It logs the thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
message := fmt.Sprintf("Method view using token %s with thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -52,9 +56,11 @@ func (lm *loggingMiddleware) View(ctx context.Context, token, id string) (saved
|
||||
return lm.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
// Update logs the update request. It logs token, bootstrap thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update for token %s and thing %s took %s to complete", token, cfg.MFThing, time.Since(begin))
|
||||
message := fmt.Sprintf("Method update using token %s with thing %s took %s to complete", token, cfg.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -65,9 +71,11 @@ func (lm *loggingMiddleware) Update(ctx context.Context, token string, cfg boots
|
||||
return lm.svc.Update(ctx, token, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert logs the update_cert request. It logs token, thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, token, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_cert for thing with id %s took %s to complete", thingID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method update_cert using token %s with 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
|
||||
@@ -78,9 +86,11 @@ func (lm *loggingMiddleware) UpdateCert(ctx context.Context, token, thingID, cli
|
||||
return lm.svc.UpdateCert(ctx, token, thingID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections logs the update_connections request. It logs token, bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_connections for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
message := fmt.Sprintf("Method update_connections using token %s with thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -91,9 +101,11 @@ func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, token, id st
|
||||
return lm.svc.UpdateConnections(ctx, token, id, connections)
|
||||
}
|
||||
|
||||
// List logs the list request. It logs token, offset, limit and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list for token %s and offset %d and limit %d took %s to complete", token, offset, limit, time.Since(begin))
|
||||
message := fmt.Sprintf("Method list using token %s with offset %d and limit %d took %s to complete", token, offset, limit, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -104,9 +116,11 @@ func (lm *loggingMiddleware) List(ctx context.Context, token string, filter boot
|
||||
return lm.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove logs the remove request. It logs token, bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
message := fmt.Sprintf("Method remove using token %s with thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
|
||||
@@ -30,6 +30,7 @@ func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency m
|
||||
}
|
||||
}
|
||||
|
||||
// Add instruments Add method with metrics.
|
||||
func (mm *metricsMiddleware) Add(ctx context.Context, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
@@ -39,6 +40,7 @@ func (mm *metricsMiddleware) Add(ctx context.Context, token string, cfg bootstra
|
||||
return mm.svc.Add(ctx, token, cfg)
|
||||
}
|
||||
|
||||
// View instruments View method with metrics.
|
||||
func (mm *metricsMiddleware) View(ctx context.Context, token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
@@ -48,6 +50,7 @@ func (mm *metricsMiddleware) View(ctx context.Context, token, id string) (saved
|
||||
return mm.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
// Update instruments Update method with metrics.
|
||||
func (mm *metricsMiddleware) Update(ctx context.Context, token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
@@ -57,6 +60,7 @@ func (mm *metricsMiddleware) Update(ctx context.Context, token string, cfg boots
|
||||
return mm.svc.Update(ctx, token, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert instruments UpdateCert method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, token, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
@@ -66,6 +70,7 @@ func (mm *metricsMiddleware) UpdateCert(ctx context.Context, token, thingKey, cl
|
||||
return mm.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections instruments UpdateConnections method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
@@ -75,6 +80,7 @@ func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, token, id st
|
||||
return mm.svc.UpdateConnections(ctx, token, id, connections)
|
||||
}
|
||||
|
||||
// List instruments List method with metrics.
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
@@ -84,6 +90,7 @@ func (mm *metricsMiddleware) List(ctx context.Context, token string, filter boot
|
||||
return mm.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove instruments Remove method with metrics.
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
@@ -93,6 +100,7 @@ func (mm *metricsMiddleware) Remove(ctx context.Context, token, id string) (err
|
||||
return mm.svc.Remove(ctx, token, id)
|
||||
}
|
||||
|
||||
// Bootstrap instruments Bootstrap method with metrics.
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
@@ -102,6 +110,7 @@ func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externa
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState instruments ChangeState method with metrics.
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
@@ -111,6 +120,7 @@ func (mm *metricsMiddleware) ChangeState(ctx context.Context, token, id string,
|
||||
return mm.svc.ChangeState(ctx, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
@@ -120,6 +130,7 @@ func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel b
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
@@ -129,6 +140,7 @@ func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string)
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
@@ -138,6 +150,7 @@ func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// DisconnectThingHandler instruments DisconnectThingHandler method with metrics.
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(ctx context.Context, channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_thing_handler").Add(1)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -34,7 +34,7 @@ var (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger logger.Logger) http.Handler {
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger mflog.Logger) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
|
||||
+32
-19
@@ -3,31 +3,44 @@
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/pkg/clients"
|
||||
)
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding Mainflux entities.
|
||||
// MFThing represents corresponding Mainflux Thing ID.
|
||||
// MFKey is key of corresponding Mainflux Thing.
|
||||
// MFChannels is a list of Mainflux Channels corresponding Mainflux Thing connects to.
|
||||
type Config struct {
|
||||
MFThing string
|
||||
Owner string
|
||||
Name string
|
||||
ClientCert string
|
||||
ClientKey string
|
||||
CACert string
|
||||
MFKey string
|
||||
MFChannels []Channel
|
||||
ExternalID string
|
||||
ExternalKey string
|
||||
Content string
|
||||
State State
|
||||
MFThing string `json:"mainflux_thing"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
MFKey string `json:"mainflux_key"`
|
||||
MFChannels []Channel `json:"mainflux_channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Content string `json:"content,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// Channel represents Mainflux channel corresponding Mainflux Thing is connected to.
|
||||
type Channel struct {
|
||||
ID string
|
||||
Name string
|
||||
Metadata map[string]interface{}
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
Owner string `json:"owner_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
@@ -39,10 +52,10 @@ type Filter struct {
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Configs []Config
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []Config `json:"configs"`
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
mfclients "github.com/mainflux/mainflux/pkg/clients"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfgroups "github.com/mainflux/mainflux/pkg/groups"
|
||||
"github.com/mainflux/mainflux/things/groups"
|
||||
upolicies "github.com/mainflux/mainflux/users/policies"
|
||||
)
|
||||
|
||||
var _ groups.Service = (*mainfluxChannels)(nil)
|
||||
|
||||
type mainfluxChannels struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
channels map[string]mfgroups.Group
|
||||
auth upolicies.AuthServiceClient
|
||||
}
|
||||
|
||||
// NewChannelsService returns Mainflux Channels service mock.
|
||||
// Only methods used by SDK are mocked.
|
||||
func NewChannelsService(channels map[string]mfgroups.Group, auth upolicies.AuthServiceClient) groups.Service {
|
||||
return &mainfluxChannels{
|
||||
channels: channels,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) CreateGroups(ctx context.Context, token string, chs ...mfgroups.Group) ([]mfgroups.Group, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(ctx, &upolicies.Token{Value: token})
|
||||
if err != nil {
|
||||
return []mfgroups.Group{}, errors.ErrAuthentication
|
||||
}
|
||||
for i := range chs {
|
||||
svc.counter++
|
||||
chs[i].Owner = userID.GetId()
|
||||
chs[i].ID = strconv.FormatUint(svc.counter, 10)
|
||||
svc.channels[chs[i].ID] = chs[i]
|
||||
}
|
||||
|
||||
return chs, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) ViewGroup(_ context.Context, owner, id string) (mfgroups.Group, error) {
|
||||
if c, ok := svc.channels[id]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return mfgroups.Group{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) ListGroups(context.Context, string, mfgroups.GroupsPage) (mfgroups.GroupsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) ListMemberships(context.Context, string, string, mfgroups.GroupsPage) (mfgroups.MembershipsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) UpdateGroup(context.Context, string, mfgroups.Group) (mfgroups.Group, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) EnableGroup(ctx context.Context, token, id string) (mfgroups.Group, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(ctx, &upolicies.Token{Value: token})
|
||||
if err != nil {
|
||||
return mfgroups.Group{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
if t, ok := svc.channels[id]; !ok || t.Owner != userID.GetId() {
|
||||
return mfgroups.Group{}, errors.ErrNotFound
|
||||
}
|
||||
if t, ok := svc.channels[id]; ok && t.Owner == userID.GetId() {
|
||||
t.Status = mfclients.EnabledStatus
|
||||
return t, nil
|
||||
}
|
||||
return mfgroups.Group{}, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxChannels) DisableGroup(ctx context.Context, token, id string) (mfgroups.Group, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(ctx, &upolicies.Token{Value: token})
|
||||
if err != nil {
|
||||
return mfgroups.Group{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
if t, ok := svc.channels[id]; !ok || t.Owner != userID.GetId() {
|
||||
return mfgroups.Group{}, errors.ErrNotFound
|
||||
}
|
||||
if t, ok := svc.channels[id]; ok && t.Owner == userID.GetId() {
|
||||
t.Status = mfclients.DisabledStatus
|
||||
return t, nil
|
||||
}
|
||||
return mfgroups.Group{}, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package mocks contains mocks for testing purposes.
|
||||
package mocks
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
tpolicies "github.com/mainflux/mainflux/things/policies"
|
||||
upolicies "github.com/mainflux/mainflux/users/policies"
|
||||
)
|
||||
|
||||
var _ tpolicies.Service = (*mainfluxPolicies)(nil)
|
||||
|
||||
type mainfluxPolicies struct {
|
||||
mu sync.Mutex
|
||||
auth upolicies.AuthServiceClient
|
||||
connections map[string]tpolicies.Policy
|
||||
}
|
||||
|
||||
// NewPoliciesService returns Mainflux Things Policies service mock.
|
||||
// Only methods used by SDK are mocked.
|
||||
func NewPoliciesService(auth upolicies.AuthServiceClient) tpolicies.Service {
|
||||
return &mainfluxPolicies{
|
||||
auth: auth,
|
||||
connections: make(map[string]tpolicies.Policy),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *mainfluxPolicies) AddPolicy(ctx context.Context, token string, p tpolicies.Policy) (tpolicies.Policy, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
if _, err := svc.auth.Identify(ctx, &upolicies.Token{Value: token}); err != nil {
|
||||
return tpolicies.Policy{}, errors.ErrAuthentication
|
||||
}
|
||||
svc.connections[fmt.Sprintf("%s:%s", p.Subject, p.Object)] = p
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxPolicies) DeletePolicy(ctx context.Context, token string, p tpolicies.Policy) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
if _, err := svc.auth.Identify(context.Background(), &upolicies.Token{Value: token}); err != nil {
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
|
||||
for _, pol := range svc.connections {
|
||||
if pol.Subject == p.Subject && pol.Object == p.Object {
|
||||
delete(svc.connections, fmt.Sprintf("%s:%s", p.Subject, p.Object))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxPolicies) UpdatePolicy(context.Context, string, tpolicies.Policy) (tpolicies.Policy, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxPolicies) Authorize(context.Context, tpolicies.AccessRequest) (tpolicies.Policy, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxPolicies) ListPolicies(context.Context, string, tpolicies.Page) (tpolicies.PolicyPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
+48
-155
@@ -8,218 +8,125 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
mfclients "github.com/mainflux/mainflux/pkg/clients"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
"github.com/mainflux/mainflux/things/clients"
|
||||
upolicies "github.com/mainflux/mainflux/users/policies"
|
||||
)
|
||||
|
||||
var _ things.Service = (*mainfluxThings)(nil)
|
||||
var _ clients.Service = (*mainfluxThings)(nil)
|
||||
|
||||
type mainfluxThings struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
things map[string]things.Thing
|
||||
channels map[string]things.Channel
|
||||
auth mainflux.AuthServiceClient
|
||||
connections map[string][]string
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
things map[string]mfclients.Client
|
||||
auth upolicies.AuthServiceClient
|
||||
}
|
||||
|
||||
// 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, auth mainflux.AuthServiceClient) things.Service {
|
||||
func NewThingsService(things map[string]mfclients.Client, auth upolicies.AuthServiceClient) clients.Service {
|
||||
return &mainfluxThings{
|
||||
things: things,
|
||||
channels: channels,
|
||||
auth: auth,
|
||||
connections: make(map[string][]string),
|
||||
things: things,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...things.Thing) ([]things.Thing, error) {
|
||||
func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...mfclients.Client) ([]mfclients.Client, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &upolicies.Token{Value: owner})
|
||||
if err != nil {
|
||||
return []things.Thing{}, errors.ErrAuthentication
|
||||
return []mfclients.Client{}, errors.ErrAuthentication
|
||||
}
|
||||
for i := range ths {
|
||||
svc.counter++
|
||||
ths[i].Owner = userID.Email
|
||||
ths[i].Owner = userID.GetId()
|
||||
ths[i].ID = strconv.FormatUint(svc.counter, 10)
|
||||
ths[i].Key = ths[i].ID
|
||||
ths[i].Credentials.Secret = ths[i].ID
|
||||
svc.things[ths[i].ID] = ths[i]
|
||||
}
|
||||
|
||||
return ths, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (things.Thing, error) {
|
||||
func (svc *mainfluxThings) ViewClient(_ context.Context, owner, id string) (mfclients.Client, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &upolicies.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, errors.ErrAuthentication
|
||||
return mfclients.Client{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.Email {
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.GetId() {
|
||||
return t, nil
|
||||
|
||||
}
|
||||
|
||||
return things.Thing{}, errors.ErrNotFound
|
||||
return mfclients.Client{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs []string) error {
|
||||
func (svc *mainfluxThings) EnableClient(ctx context.Context, token, id string) (mfclients.Client, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &upolicies.Token{Value: token})
|
||||
if err != nil {
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
for _, chID := range chIDs {
|
||||
if svc.channels[chID].Owner != userID.Email {
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
svc.connections[chID] = append(svc.connections[chID], thIDs...)
|
||||
return mfclients.Client{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return nil
|
||||
if t, ok := svc.things[id]; !ok || t.Owner != userID.GetId() {
|
||||
return mfclients.Client{}, errors.ErrNotFound
|
||||
}
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.GetId() {
|
||||
t.Status = mfclients.EnabledStatus
|
||||
return t, nil
|
||||
}
|
||||
return mfclients.Client{}, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Disconnect(_ context.Context, owner string, chIDs, thIDs []string) error {
|
||||
func (svc *mainfluxThings) DisableClient(ctx context.Context, token, id string) (mfclients.Client, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
userID, err := svc.auth.Identify(context.Background(), &upolicies.Token{Value: token})
|
||||
if err != nil {
|
||||
return errors.ErrAuthentication
|
||||
return mfclients.Client{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
for _, chID := range chIDs {
|
||||
if svc.channels[chID].Owner != userID.Email {
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
|
||||
ids := svc.connections[chID]
|
||||
var count int
|
||||
var newConns []string
|
||||
for _, thID := range thIDs {
|
||||
for _, id := range ids {
|
||||
if id == thID {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
newConns = append(newConns, id)
|
||||
}
|
||||
|
||||
if len(newConns)-len(ids) != count {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
svc.connections[chID] = newConns
|
||||
}
|
||||
if t, ok := svc.things[id]; !ok || t.Owner != userID.GetId() {
|
||||
return mfclients.Client{}, errors.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return errors.ErrAuthentication
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.GetId() {
|
||||
t.Status = mfclients.DisabledStatus
|
||||
return t, nil
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; !ok || t.Owner != userID.Email {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
delete(svc.things, id)
|
||||
conns := make(map[string][]string)
|
||||
for k, v := range svc.connections {
|
||||
i := findIndex(v, id)
|
||||
if i != -1 {
|
||||
var tmp []string
|
||||
if i != len(v)-2 {
|
||||
tmp = v[i+1:]
|
||||
}
|
||||
conns[k] = append(v[:i], tmp...)
|
||||
}
|
||||
}
|
||||
|
||||
svc.connections = conns
|
||||
return nil
|
||||
return mfclients.Client{}, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewChannel(_ context.Context, owner, id string) (things.Channel, error) {
|
||||
if c, ok := svc.channels[id]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return things.Channel{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateThing(context.Context, string, things.Thing) error {
|
||||
func (svc *mainfluxThings) UpdateClient(context.Context, string, mfclients.Client) (mfclients.Client, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) error {
|
||||
func (svc *mainfluxThings) UpdateClientSecret(context.Context, string, string, string) (mfclients.Client, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThings(context.Context, string, things.PageMetadata) (things.Page, error) {
|
||||
func (svc *mainfluxThings) UpdateClientOwner(context.Context, string, mfclients.Client) (mfclients.Client, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, things.PageMetadata) (things.ChannelsPage, error) {
|
||||
func (svc *mainfluxThings) UpdateClientTags(context.Context, string, mfclients.Client) (mfclients.Client, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, things.PageMetadata) (things.Page, error) {
|
||||
func (svc *mainfluxThings) ListClients(context.Context, string, mfclients.Page) (mfclients.ClientsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateChannels(_ context.Context, owner string, chs ...things.Channel) ([]things.Channel, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return []things.Channel{}, errors.ErrAuthentication
|
||||
}
|
||||
for i := range chs {
|
||||
svc.counter++
|
||||
chs[i].Owner = userID.Email
|
||||
chs[i].ID = strconv.FormatUint(svc.counter, 10)
|
||||
svc.channels[chs[i].ID] = chs[i]
|
||||
}
|
||||
|
||||
return chs, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateChannel(context.Context, string, things.Channel) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannels(context.Context, string, things.PageMetadata) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveChannel(context.Context, string, string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CanAccessByKey(context.Context, string, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CanAccessByID(context.Context, string, string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) IsChannelOwner(context.Context, string, string) error {
|
||||
func (svc *mainfluxThings) ListClientsByGroup(context.Context, string, string, mfclients.Page) (mfclients.MembersPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -227,20 +134,6 @@ func (svc *mainfluxThings) Identify(context.Context, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ShareThing(ctx context.Context, token, thingID string, actions, userIDs []string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func findIndex(list []string, val string) int {
|
||||
for i, v := range list {
|
||||
if v == val {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListMembers(ctx context.Context, token, groupID string, pm things.PageMetadata) (things.Page, error) {
|
||||
func (svc *mainfluxThings) ShareClient(ctx context.Context, token, userID, groupID, thingID string, actions []string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
+12
-26
@@ -6,60 +6,46 @@ package mocks
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users/policies"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.AuthServiceClient = (*serviceMock)(nil)
|
||||
var _ policies.AuthServiceClient = (*serviceMock)(nil)
|
||||
|
||||
type serviceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewAuthClient creates mock of users service.
|
||||
func NewAuthClient(users map[string]string) mainflux.AuthServiceClient {
|
||||
func NewAuthClient(users map[string]string) policies.AuthServiceClient {
|
||||
return &serviceMock{users}
|
||||
}
|
||||
|
||||
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.UserIdentity{Email: id, Id: id}, nil
|
||||
func (svc serviceMock) Identify(ctx context.Context, in *policies.Token, opts ...grpc.CallOption) (*policies.UserIdentity, error) {
|
||||
if id, ok := svc.users[in.GetValue()]; ok {
|
||||
return &policies.UserIdentity{Id: id}, nil
|
||||
}
|
||||
return nil, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
func (svc serviceMock) Issue(ctx context.Context, in *policies.IssueReq, opts ...grpc.CallOption) (*policies.Token, error) {
|
||||
if id, ok := svc.users[in.GetEmail()]; ok {
|
||||
switch in.Type {
|
||||
default:
|
||||
return &mainflux.Token{Value: id}, nil
|
||||
}
|
||||
return &policies.Token{Value: id}, nil
|
||||
}
|
||||
return nil, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
func (svc serviceMock) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
|
||||
func (svc serviceMock) Authorize(ctx context.Context, req *policies.AuthorizeReq, _ ...grpc.CallOption) (r *policies.AuthorizeRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) AddPolicy(ctx context.Context, in *mainflux.AddPolicyReq, opts ...grpc.CallOption) (*mainflux.AddPolicyRes, error) {
|
||||
func (svc serviceMock) AddPolicy(ctx context.Context, req *policies.AddPolicyReq, _ ...grpc.CallOption) (r *policies.AddPolicyRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) DeletePolicy(ctx context.Context, in *mainflux.DeletePolicyReq, opts ...grpc.CallOption) (*mainflux.DeletePolicyRes, error) {
|
||||
func (svc serviceMock) DeletePolicy(ctx context.Context, req *policies.DeletePolicyReq, _ ...grpc.CallOption) (r *policies.DeletePolicyRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) ListPolicies(ctx context.Context, in *mainflux.ListPoliciesReq, opts ...grpc.CallOption) (*mainflux.ListPoliciesRes, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) Assign(ctx context.Context, req *mainflux.Assignment, _ ...grpc.CallOption) (r *empty.Empty, err error) {
|
||||
func (svc serviceMock) ListPolicies(ctx context.Context, req *policies.ListPoliciesReq, _ ...grpc.CallOption) (r *policies.ListPoliciesRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/clients"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -33,12 +35,12 @@ var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
db *sqlx.DB
|
||||
log logger.Logger
|
||||
log mflog.Logger
|
||||
}
|
||||
|
||||
// NewConfigRepository instantiates a PostgreSQL implementation of config
|
||||
// repository.
|
||||
func NewConfigRepository(db *sqlx.DB, log logger.Logger) bootstrap.ConfigRepository {
|
||||
func NewConfigRepository(db *sqlx.DB, log mflog.Logger) bootstrap.ConfigRepository {
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
@@ -391,7 +393,8 @@ func (cr configRepository) UpdateChannel(c bootstrap.Channel) error {
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata WHERE mainflux_channel = :mainflux_channel`
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE mainflux_channel = :mainflux_channel`
|
||||
if _, err = cr.db.NamedExec(q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
}
|
||||
@@ -457,9 +460,8 @@ func insertChannels(owner string, channels []bootstrap.Channel, tx *sqlx.Tx) err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata)
|
||||
VALUES (:mainflux_channel, :owner, :name, :metadata)`
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata, parent_id, description, created_at, updated_at, updated_by, status)
|
||||
VALUES (:mainflux_channel, :owner, :name, :metadata, :parent_id, :description, :created_at, :updated_at, :updated_by, :status)`
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
|
||||
@@ -555,6 +557,17 @@ func nullString(s string) sql.NullString {
|
||||
}
|
||||
}
|
||||
|
||||
func nullTime(t time.Time) sql.NullTime {
|
||||
if t.IsZero() {
|
||||
return sql.NullTime{}
|
||||
}
|
||||
|
||||
return sql.NullTime{
|
||||
Time: t,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
MFThing string `db:"mainflux_thing"`
|
||||
Owner string `db:"owner"`
|
||||
@@ -618,17 +631,29 @@ func toConfig(dbcfg dbConfig) bootstrap.Config {
|
||||
}
|
||||
|
||||
type dbChannel struct {
|
||||
ID string `db:"mainflux_channel"`
|
||||
Name sql.NullString `db:"name"`
|
||||
Owner sql.NullString `db:"owner"`
|
||||
Metadata string `db:"metadata"`
|
||||
ID string `db:"mainflux_channel"`
|
||||
Name sql.NullString `db:"name"`
|
||||
Owner sql.NullString `db:"owner"`
|
||||
Metadata string `db:"metadata"`
|
||||
Parent sql.NullString `db:"parent_id,omitempty"`
|
||||
Description string `db:"description,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
UpdatedBy sql.NullString `db:"updated_by,omitempty"`
|
||||
Status clients.Status `db:"status"`
|
||||
}
|
||||
|
||||
func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
dbch := dbChannel{
|
||||
ID: ch.ID,
|
||||
Name: nullString(ch.Name),
|
||||
Owner: nullString(owner),
|
||||
ID: ch.ID,
|
||||
Name: nullString(ch.Name),
|
||||
Owner: nullString(owner),
|
||||
Parent: nullString(ch.Parent),
|
||||
Description: ch.Description,
|
||||
CreatedAt: ch.CreatedAt,
|
||||
UpdatedAt: nullTime(ch.UpdatedAt),
|
||||
UpdatedBy: nullString(ch.UpdatedBy),
|
||||
Status: ch.Status,
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
@@ -642,12 +667,27 @@ func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
|
||||
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
ch := bootstrap.Channel{
|
||||
ID: dbch.ID,
|
||||
ID: dbch.ID,
|
||||
Description: dbch.Description,
|
||||
CreatedAt: dbch.CreatedAt,
|
||||
Status: dbch.Status,
|
||||
}
|
||||
|
||||
if dbch.Name.Valid {
|
||||
ch.Name = dbch.Name.String
|
||||
}
|
||||
if dbch.Owner.Valid {
|
||||
ch.Owner = dbch.Owner.String
|
||||
}
|
||||
if dbch.Parent.Valid {
|
||||
ch.Parent = dbch.Parent.String
|
||||
}
|
||||
if dbch.UpdatedBy.Valid {
|
||||
ch.UpdatedBy = dbch.UpdatedBy.String
|
||||
}
|
||||
if dbch.UpdatedAt.Valid {
|
||||
ch.UpdatedAt = dbch.UpdatedAt.Time
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
|
||||
@@ -636,7 +636,7 @@ func TestUpdateChannel(t *testing.T) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
update.Owner = retreved.Owner
|
||||
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of bootstrap service
|
||||
// Migration of bootstrap service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
@@ -64,6 +64,17 @@ func Migration() *migrate.MemoryMigrationSource {
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_3",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS parent_id VARCHAR(36)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS description VARCHAR(1024)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS created_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_by VARCHAR(254)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
bootstrapRepo "github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
pgClient "github.com/mainflux/mainflux/internal/clients/postgres"
|
||||
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
|
||||
package consumer
|
||||
|
||||
import "time"
|
||||
|
||||
type removeEvent struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata map[string]interface{}
|
||||
id string
|
||||
name string
|
||||
metadata map[string]interface{}
|
||||
updatedAt time.Time
|
||||
updatedBy string
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
|
||||
@@ -7,19 +7,20 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/clients"
|
||||
)
|
||||
|
||||
const (
|
||||
stream = "mainflux.things"
|
||||
group = "mainflux.bootstrap"
|
||||
|
||||
thingPrefix = "thing."
|
||||
thingRemove = thingPrefix + "remove"
|
||||
thingDisconnect = thingPrefix + "disconnect"
|
||||
thingRemove = "thing.remove"
|
||||
thingDisconnect = "policy.delete"
|
||||
|
||||
channelPrefix = "channel."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
@@ -31,7 +32,7 @@ const (
|
||||
// Subscriber represents event source for things and channels provisioning.
|
||||
type Subscriber interface {
|
||||
// Subscribes to given subject and receives events.
|
||||
Subscribe(context.Context, string) error
|
||||
Subscribe(ctx context.Context, subject string) error
|
||||
}
|
||||
|
||||
type eventStore struct {
|
||||
@@ -96,8 +97,20 @@ func (es eventStore) Subscribe(ctx context.Context, subject string) error {
|
||||
}
|
||||
|
||||
func decodeRemoveThing(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
status := read(event, "status", "")
|
||||
st, err := clients.ToStatus(status)
|
||||
if err != nil {
|
||||
return removeEvent{}
|
||||
}
|
||||
switch st {
|
||||
case clients.EnabledStatus:
|
||||
return removeEvent{}
|
||||
case clients.DisabledStatus:
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
}
|
||||
default:
|
||||
return removeEvent{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,15 +122,29 @@ func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
|
||||
}
|
||||
|
||||
return updateChannelEvent{
|
||||
id: read(event, "id", ""),
|
||||
name: read(event, "name", ""),
|
||||
metadata: metadata,
|
||||
id: read(event, "id", ""),
|
||||
name: read(event, "name", ""),
|
||||
metadata: metadata,
|
||||
updatedAt: readTime(event, "updated_at", time.Now()),
|
||||
updatedBy: read(event, "updated_by", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
status := read(event, "status", "")
|
||||
st, err := clients.ToStatus(status)
|
||||
if err != nil {
|
||||
return removeEvent{}
|
||||
}
|
||||
switch st {
|
||||
case clients.EnabledStatus:
|
||||
return removeEvent{}
|
||||
case clients.DisabledStatus:
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
}
|
||||
default:
|
||||
return removeEvent{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +157,11 @@ func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
|
||||
|
||||
func (es eventStore) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
UpdatedAt: uce.updatedAt,
|
||||
UpdatedBy: uce.updatedBy,
|
||||
}
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
@@ -145,3 +174,12 @@ func read(event map[string]interface{}, key, def string) string {
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func readTime(event map[string]interface{}, key string, def time.Time) time.Time {
|
||||
val, ok := event[key].(time.Time)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package redis contains cache implementations using Redis as
|
||||
// the underlying cache.
|
||||
package redis
|
||||
@@ -4,131 +4,278 @@
|
||||
package producer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
const (
|
||||
configPrefix = "config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configPrefix = "config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configList = configPrefix + "list"
|
||||
configHandlerRemove = configPrefix + "remove_handler"
|
||||
|
||||
thingPrefix = "thing."
|
||||
thingBootstrap = thingPrefix + "bootstrap"
|
||||
thingStateChange = thingPrefix + "state_change"
|
||||
thingStateChange = thingPrefix + "change_state"
|
||||
thingUpdateConnections = thingPrefix + "update_connections"
|
||||
thingDisconnect = thingPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "channel."
|
||||
channelHandlerRemove = channelPrefix + "remove_handler"
|
||||
channelUpdateHandler = channelPrefix + "update_handler"
|
||||
|
||||
certUpdate = "cert.update"
|
||||
)
|
||||
|
||||
type event interface {
|
||||
encode() map[string]interface{}
|
||||
encode() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
var (
|
||||
_ event = (*createConfigEvent)(nil)
|
||||
_ event = (*updateConfigEvent)(nil)
|
||||
_ event = (*configEvent)(nil)
|
||||
_ event = (*removeConfigEvent)(nil)
|
||||
_ event = (*bootstrapEvent)(nil)
|
||||
_ event = (*changeStateEvent)(nil)
|
||||
_ event = (*updateConnectionsEvent)(nil)
|
||||
_ event = (*updateCertEvent)(nil)
|
||||
_ event = (*listConfigsEvent)(nil)
|
||||
_ event = (*removeHandlerEvent)(nil)
|
||||
)
|
||||
|
||||
type createConfigEvent struct {
|
||||
mfThing string
|
||||
owner string
|
||||
name string
|
||||
mfChannels []string
|
||||
externalID string
|
||||
content string
|
||||
timestamp time.Time
|
||||
type configEvent struct {
|
||||
bootstrap.Config
|
||||
operation string
|
||||
}
|
||||
|
||||
func (cce createConfigEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": cce.mfThing,
|
||||
"owner": cce.owner,
|
||||
"name": cce.name,
|
||||
"channels": strings.Join(cce.mfChannels, ", "),
|
||||
"external_id": cce.externalID,
|
||||
"content": cce.content,
|
||||
"timestamp": cce.timestamp.Unix(),
|
||||
"operation": configCreate,
|
||||
func (ce configEvent) encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"state": ce.State.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
}
|
||||
|
||||
type updateConfigEvent struct {
|
||||
mfThing string
|
||||
name string
|
||||
content string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (uce updateConfigEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"thing_id": uce.mfThing,
|
||||
"name": uce.name,
|
||||
"content": uce.content,
|
||||
"timestamp": uce.timestamp.Unix(),
|
||||
"operation": configUpdate,
|
||||
if ce.MFThing != "" {
|
||||
val["mainflux_thing"] = ce.MFThing
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
if ce.Owner != "" {
|
||||
val["owner"] = ce.Owner
|
||||
}
|
||||
if ce.Name != "" {
|
||||
val["name"] = ce.Name
|
||||
}
|
||||
if ce.ExternalID != "" {
|
||||
val["external_id"] = ce.ExternalID
|
||||
}
|
||||
if len(ce.MFChannels) > 0 {
|
||||
channels := make([]string, len(ce.MFChannels))
|
||||
for i, ch := range ce.MFChannels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = fmt.Sprintf("[%s]", strings.Join(channels, ", "))
|
||||
}
|
||||
if ce.ClientCert != "" {
|
||||
val["client_cert"] = ce.ClientCert
|
||||
}
|
||||
if ce.ClientKey != "" {
|
||||
val["client_key"] = ce.ClientKey
|
||||
}
|
||||
if ce.CACert != "" {
|
||||
val["ca_cert"] = ce.CACert
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type removeConfigEvent struct {
|
||||
mfThing string
|
||||
timestamp time.Time
|
||||
mfThing string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) encode() map[string]interface{} {
|
||||
func (rce removeConfigEvent) encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"thing_id": rce.mfThing,
|
||||
"timestamp": rce.timestamp.Unix(),
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listConfigsEvent struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
fullMatch map[string]string
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"offset": rce.offset,
|
||||
"limit": rce.limit,
|
||||
"operation": configList,
|
||||
}
|
||||
if len(rce.fullMatch) > 0 {
|
||||
data, err := json.Marshal(rce.fullMatch)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
val["full_match"] = data
|
||||
}
|
||||
|
||||
if len(rce.partialMatch) > 0 {
|
||||
data, err := json.Marshal(rce.partialMatch)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
val["full_match"] = data
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type bootstrapEvent struct {
|
||||
bootstrap.Config
|
||||
externalID string
|
||||
success bool
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (be bootstrapEvent) encode() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (be bootstrapEvent) encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"timestamp": be.timestamp.Unix(),
|
||||
"operation": thingBootstrap,
|
||||
}
|
||||
|
||||
if be.MFThing != "" {
|
||||
val["mainflux_thing"] = be.MFThing
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
if be.Owner != "" {
|
||||
val["owner"] = be.Owner
|
||||
}
|
||||
if be.Name != "" {
|
||||
val["name"] = be.Name
|
||||
}
|
||||
if be.ExternalID != "" {
|
||||
val["external_id"] = be.ExternalID
|
||||
}
|
||||
if len(be.MFChannels) > 0 {
|
||||
channels := make([]string, len(be.MFChannels))
|
||||
for i, ch := range be.MFChannels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = fmt.Sprintf("[%s]", strings.Join(channels, ", "))
|
||||
}
|
||||
if be.ClientCert != "" {
|
||||
val["client_cert"] = be.ClientCert
|
||||
}
|
||||
if be.ClientKey != "" {
|
||||
val["client_key"] = be.ClientKey
|
||||
}
|
||||
if be.CACert != "" {
|
||||
val["ca_cert"] = be.CACert
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type changeStateEvent struct {
|
||||
mfThing string
|
||||
state bootstrap.State
|
||||
timestamp time.Time
|
||||
mfThing string
|
||||
state bootstrap.State
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) encode() map[string]interface{} {
|
||||
func (cse changeStateEvent) encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"thing_id": cse.mfThing,
|
||||
"state": cse.state.String(),
|
||||
"timestamp": cse.timestamp.Unix(),
|
||||
"operation": thingStateChange,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateConnectionsEvent struct {
|
||||
mfThing string
|
||||
mfChannels []string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) encode() map[string]interface{} {
|
||||
func (uce updateConnectionsEvent) encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"thing_id": uce.mfThing,
|
||||
"channels": strings.Join(uce.mfChannels, ", "),
|
||||
"timestamp": uce.timestamp.Unix(),
|
||||
"channels": fmt.Sprintf("[%s]", strings.Join(uce.mfChannels, ", ")),
|
||||
"operation": thingUpdateConnections,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateCertEvent struct {
|
||||
thingKey, clientCert, clientKey, caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"thing_key": uce.thingKey,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
"ca_cert": uce.caCert,
|
||||
"operation": certUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type removeHandlerEvent struct {
|
||||
id string
|
||||
operation string
|
||||
}
|
||||
|
||||
func (rhe removeHandlerEvent) encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"config_id": rhe.id,
|
||||
"operation": rhe.operation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateChannelHandlerEvent struct {
|
||||
bootstrap.Channel
|
||||
}
|
||||
|
||||
func (uche updateChannelHandlerEvent) encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"operation": channelUpdateHandler,
|
||||
}
|
||||
|
||||
if uche.ID != "" {
|
||||
val["channel_id"] = uche.ID
|
||||
}
|
||||
if uche.Name != "" {
|
||||
val["name"] = uche.Name
|
||||
}
|
||||
if uche.Metadata != nil {
|
||||
metadata, err := json.Marshal(uche.Metadata)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
val["metadata"] = metadata
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type disconnectThingEvent struct {
|
||||
thingID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (dte disconnectThingEvent) encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"thing_id": dte.thingID,
|
||||
"channel_id": dte.channelID,
|
||||
"operation": thingDisconnect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ package producer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,28 +38,31 @@ func (es eventStore) Add(ctx context.Context, token string, cfg bootstrap.Config
|
||||
return saved, err
|
||||
}
|
||||
|
||||
var channels []string
|
||||
for _, ch := range saved.MFChannels {
|
||||
channels = append(channels, ch.ID)
|
||||
ev := configEvent{
|
||||
saved, configCreate,
|
||||
}
|
||||
|
||||
ev := createConfigEvent{
|
||||
mfThing: saved.MFThing,
|
||||
owner: saved.Owner,
|
||||
name: saved.Name,
|
||||
mfChannels: channels,
|
||||
externalID: saved.ExternalID,
|
||||
content: saved.Content,
|
||||
timestamp: time.Now(),
|
||||
if err1 := es.add(ctx, ev); err1 != nil {
|
||||
return saved, errors.Wrap(err, err1)
|
||||
}
|
||||
|
||||
err = es.add(ctx, ev)
|
||||
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es eventStore) View(ctx context.Context, token, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(ctx, token, id)
|
||||
cfg, err := es.svc.View(ctx, token, id)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
ev := configEvent{
|
||||
cfg, configList,
|
||||
}
|
||||
|
||||
if err1 := es.add(ctx, ev); err1 != nil {
|
||||
return cfg, errors.Wrap(err, err1)
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es eventStore) Update(ctx context.Context, token string, cfg bootstrap.Config) error {
|
||||
@@ -67,18 +70,26 @@ func (es eventStore) Update(ctx context.Context, token string, cfg bootstrap.Con
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConfigEvent{
|
||||
mfThing: cfg.MFThing,
|
||||
name: cfg.Name,
|
||||
content: cfg.Content,
|
||||
timestamp: time.Now(),
|
||||
ev := configEvent{
|
||||
cfg, configUpdate,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(ctx context.Context, token, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert)
|
||||
if err := es.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateCertEvent{
|
||||
thingKey: thingKey,
|
||||
clientCert: clientCert,
|
||||
clientKey: clientKey,
|
||||
caCert: caCert,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateConnections(ctx context.Context, token, id string, connections []string) error {
|
||||
@@ -89,14 +100,29 @@ func (es eventStore) UpdateConnections(ctx context.Context, token, id string, co
|
||||
ev := updateConnectionsEvent{
|
||||
mfThing: id,
|
||||
mfChannels: connections,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(ctx, token, filter, offset, limit)
|
||||
bp, err := es.svc.List(ctx, token, filter, offset, limit)
|
||||
if err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
ev := listConfigsEvent{
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
fullMatch: filter.FullMatch,
|
||||
partialMatch: filter.PartialMatch,
|
||||
}
|
||||
|
||||
if err1 := es.add(ctx, ev); err1 != nil {
|
||||
return bp, errors.Wrap(err, err1)
|
||||
}
|
||||
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (es eventStore) Remove(ctx context.Context, token, id string) error {
|
||||
@@ -105,8 +131,7 @@ func (es eventStore) Remove(ctx context.Context, token, id string) error {
|
||||
}
|
||||
|
||||
ev := removeConfigEvent{
|
||||
mfThing: id,
|
||||
timestamp: time.Now(),
|
||||
mfThing: id,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
@@ -116,15 +141,18 @@ func (es eventStore) Bootstrap(ctx context.Context, externalKey, externalID stri
|
||||
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
externalID: externalID,
|
||||
timestamp: time.Now(),
|
||||
success: true,
|
||||
cfg,
|
||||
externalID,
|
||||
true,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ev.success = false
|
||||
}
|
||||
_ = es.add(ctx, ev)
|
||||
|
||||
if err1 := es.add(ctx, ev); err1 != nil {
|
||||
return cfg, err1
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
@@ -135,35 +163,74 @@ func (es eventStore) ChangeState(ctx context.Context, token, id string, state bo
|
||||
}
|
||||
|
||||
ev := changeStateEvent{
|
||||
mfThing: id,
|
||||
state: state,
|
||||
timestamp: time.Now(),
|
||||
mfThing: id,
|
||||
state: state,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
return es.svc.RemoveConfigHandler(ctx, id)
|
||||
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: configHandlerRemove,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
return es.svc.RemoveChannelHandler(ctx, id)
|
||||
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: channelHandlerRemove,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateChannelHandlerEvent{
|
||||
channel,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) DisconnectThingHandler(ctx context.Context, channelID, thingID string) error {
|
||||
return es.svc.DisconnectThingHandler(ctx, channelID, thingID)
|
||||
if err := es.svc.DisconnectThingHandler(ctx, channelID, thingID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := disconnectThingEvent{
|
||||
channelID,
|
||||
thingID,
|
||||
}
|
||||
|
||||
return es.add(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) add(ctx context.Context, ev event) error {
|
||||
values, err := ev.encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := &redis.XAddArgs{
|
||||
Stream: streamID,
|
||||
MaxLenApprox: streamLen,
|
||||
Values: ev.encode(),
|
||||
Values: values,
|
||||
}
|
||||
|
||||
return es.client.XAdd(ctx, record).Err()
|
||||
|
||||
@@ -5,6 +5,7 @@ package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
@@ -13,17 +14,23 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/bootstrap/redis/producer"
|
||||
mfclients "github.com/mainflux/mainflux/pkg/clients"
|
||||
mfgroups "github.com/mainflux/mainflux/pkg/groups"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/mainflux/mainflux/things/clients"
|
||||
capi "github.com/mainflux/mainflux/things/clients/api"
|
||||
"github.com/mainflux/mainflux/things/groups"
|
||||
gapi "github.com/mainflux/mainflux/things/groups/api"
|
||||
tpolicies "github.com/mainflux/mainflux/things/policies"
|
||||
papi "github.com/mainflux/mainflux/things/policies/api/http"
|
||||
upolicies "github.com/mainflux/mainflux/users/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -63,7 +70,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
func newService(auth upolicies.AuthServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
ThingsURL: url,
|
||||
@@ -73,25 +80,33 @@ func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
return bootstrap.New(auth, configs, sdk, encKey)
|
||||
}
|
||||
|
||||
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
channels := make(map[string]things.Channel, channelsNum)
|
||||
func newThingsService(auth upolicies.AuthServiceClient) (clients.Service, groups.Service, tpolicies.Service) {
|
||||
channels := make(map[string]mfgroups.Group, channelsNum)
|
||||
for i := 0; i < channelsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
channels[id] = things.Channel{
|
||||
channels[id] = mfgroups.Group{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Metadata: map[string]interface{}{"meta": "data"},
|
||||
Status: mfclients.EnabledStatus,
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
|
||||
csvc := mocks.NewThingsService(map[string]mfclients.Client{}, auth)
|
||||
gsvc := mocks.NewChannelsService(channels, auth)
|
||||
psvc := mocks.NewPoliciesService(auth)
|
||||
return csvc, gsvc, psvc
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
func newThingsServer(csvc clients.Service, gsvc groups.Service, psvc tpolicies.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
mux := bone.New()
|
||||
capi.MakeHandler(csvc, mux, logger)
|
||||
gapi.MakeHandler(gsvc, mux, logger)
|
||||
papi.MakeHandler(csvc, psvc, mux, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
err := redisClient.FlushAll(context.Background()).Err()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
@@ -156,9 +171,8 @@ func TestAdd(t *testing.T) {
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
@@ -222,11 +236,15 @@ func TestUpdate(t *testing.T) {
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": modified.MFThing,
|
||||
"name": modified.Name,
|
||||
"content": modified.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configUpdate,
|
||||
"name": modified.Name,
|
||||
"content": modified.Content,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configUpdate,
|
||||
"channels": "[1, 2]",
|
||||
"external_id": "external_id",
|
||||
"mainflux_thing": "1",
|
||||
"owner": email,
|
||||
"state": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -253,6 +271,7 @@ func TestUpdate(t *testing.T) {
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
event["timestamp"] = msg.ID
|
||||
lastID = msg.ID
|
||||
}
|
||||
|
||||
@@ -318,9 +337,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
@@ -400,9 +418,8 @@ func TestRemove(t *testing.T) {
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
@@ -447,7 +464,7 @@ func TestBootstrap(t *testing.T) {
|
||||
{
|
||||
desc: "bootstrap with an error",
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "external_id",
|
||||
externalKey: "external_id1",
|
||||
err: bootstrap.ErrExternalKey,
|
||||
event: map[string]interface{}{
|
||||
"external_id": "external_id",
|
||||
@@ -471,9 +488,8 @@ func TestBootstrap(t *testing.T) {
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
test(t, tc.event, event, tc.desc)
|
||||
}
|
||||
@@ -539,9 +555,8 @@ func TestChangeState(t *testing.T) {
|
||||
|
||||
var event map[string]interface{}
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
lastID = msg.ID
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
}
|
||||
|
||||
test(t, tc.event, event, tc.desc)
|
||||
@@ -551,15 +566,34 @@ func TestChangeState(t *testing.T) {
|
||||
func test(t *testing.T, expected, actual map[string]interface{}, description string) {
|
||||
if expected != nil && actual != nil {
|
||||
ts1 := expected["timestamp"].(int64)
|
||||
ats := actual["timestamp"].(string)
|
||||
|
||||
ts2, err := strconv.ParseInt(actual["timestamp"].(string), 10, 64)
|
||||
ts2, err := strconv.ParseInt(strings.Split(ats, "-")[0], 10, 64)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid timestamp, got %s", description, err))
|
||||
ts2 = time.UnixMilli(ts2).Unix()
|
||||
|
||||
val := ts1 == ts2 || ts2 <= ts1+defaultTimout
|
||||
assert.True(t, val, fmt.Sprintf("%s: timestamp is not in valid range", description))
|
||||
|
||||
delete(expected, "timestamp")
|
||||
delete(actual, "timestamp")
|
||||
|
||||
ech := expected["channels"]
|
||||
ach := actual["channels"]
|
||||
|
||||
che := []int{}
|
||||
err = json.Unmarshal([]byte(ech.(string)), &che)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid channels, got %s", description, err))
|
||||
|
||||
cha := []int{}
|
||||
err = json.Unmarshal([]byte(ach.(string)), &cha)
|
||||
require.Nil(t, err, fmt.Sprintf("%s: expected to get a valid channels, got %s", description, err))
|
||||
|
||||
if assert.ElementsMatchf(t, che, cha, "%s: got incorrect channels\n", description) {
|
||||
delete(expected, "channels")
|
||||
delete(actual, "channels")
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual, fmt.Sprintf("%s: got incorrect event\n", description))
|
||||
}
|
||||
}
|
||||
|
||||
+17
-16
@@ -10,9 +10,9 @@ import (
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/users/policies"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,10 +20,10 @@ var (
|
||||
// It can be due to networking error or invalid/unauthenticated request.
|
||||
ErrThings = errors.New("failed to receive response from Things service")
|
||||
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
|
||||
ErrExternalKey = errors.New("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
|
||||
ErrExternalKeySecure = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
@@ -103,14 +103,14 @@ type ConfigReader interface {
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
auth mainflux.AuthServiceClient
|
||||
auth policies.AuthServiceClient
|
||||
configs ConfigRepository
|
||||
sdk mfsdk.SDK
|
||||
encKey []byte
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(auth mainflux.AuthServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
|
||||
func New(auth policies.AuthServiceClient, configs ConfigRepository, sdk mfsdk.SDK, encKey []byte) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
@@ -140,7 +140,7 @@ func (bs bootstrapService) Add(ctx context.Context, token string, cfg Config) (C
|
||||
}
|
||||
|
||||
id := cfg.MFThing
|
||||
mfThing, err := bs.thing(token, id)
|
||||
mfThing, err := bs.thing(id, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errThingNotFound, err)
|
||||
}
|
||||
@@ -148,12 +148,12 @@ func (bs bootstrapService) Add(ctx context.Context, token string, cfg Config) (C
|
||||
cfg.MFThing = mfThing.ID
|
||||
cfg.Owner = owner
|
||||
cfg.State = Inactive
|
||||
cfg.MFKey = mfThing.Key
|
||||
cfg.MFKey = mfThing.Credentials.Secret
|
||||
|
||||
saved, err := bs.configs.Save(cfg, toConnect)
|
||||
if err != nil {
|
||||
if id == "" {
|
||||
if errT := bs.sdk.DeleteThing(cfg.MFThing, token); errT != nil {
|
||||
if _, errT := bs.sdk.DisableThing(cfg.MFThing, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
@@ -364,30 +364,31 @@ func (bs bootstrapService) identify(token string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := bs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
res, err := bs.auth.Identify(ctx, &policies.Token{Value: token})
|
||||
if err != nil {
|
||||
return "", errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return res.GetEmail(), nil
|
||||
return res.GetId(), nil
|
||||
}
|
||||
|
||||
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
|
||||
thingID := id
|
||||
func (bs bootstrapService) thing(id, token string) (mfsdk.Thing, error) {
|
||||
var thing mfsdk.Thing
|
||||
var err error
|
||||
|
||||
thing.ID = id
|
||||
if id == "" {
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, token)
|
||||
thing, err = bs.sdk.CreateThing(mfsdk.Thing{}, token)
|
||||
if err != nil {
|
||||
return mfsdk.Thing{}, errors.Wrap(errCreateThing, err)
|
||||
}
|
||||
}
|
||||
|
||||
thing, err := bs.sdk.Thing(thingID, token)
|
||||
thing, err = bs.sdk.Thing(thing.ID, token)
|
||||
if err != nil {
|
||||
if id != "" {
|
||||
if errT := bs.sdk.DeleteThing(thingID, token); errT != nil {
|
||||
if _, errT := bs.sdk.DisableThing(thing.ID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
@@ -430,7 +431,7 @@ func (bs bootstrapService) connectionChannels(channels, existing []string, token
|
||||
// Method updateList accepts config and channel IDs and returns three lists:
|
||||
// 1) IDs of Channels to be added
|
||||
// 2) IDs of Channels to be removed
|
||||
// 3) IDs of common Channels for these two configs
|
||||
// 3) IDs of common Channels for these two configs.
|
||||
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
|
||||
disconnect := make(map[string]bool, len(cfg.MFChannels))
|
||||
for _, c := range cfg.MFChannels {
|
||||
|
||||
+27
-14
@@ -15,17 +15,23 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/go-zoo/bone"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
mfclients "github.com/mainflux/mainflux/pkg/clients"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfgroups "github.com/mainflux/mainflux/pkg/groups"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/mainflux/mainflux/things/clients"
|
||||
capi "github.com/mainflux/mainflux/things/clients/api"
|
||||
"github.com/mainflux/mainflux/things/groups"
|
||||
gapi "github.com/mainflux/mainflux/things/groups/api"
|
||||
tpolicies "github.com/mainflux/mainflux/things/policies"
|
||||
papi "github.com/mainflux/mainflux/things/policies/api/http"
|
||||
upolicies "github.com/mainflux/mainflux/users/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -55,7 +61,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
func newService(auth upolicies.AuthServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
ThingsURL: url,
|
||||
@@ -65,23 +71,30 @@ func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
return bootstrap.New(auth, things, sdk, encKey)
|
||||
}
|
||||
|
||||
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
channels := make(map[string]things.Channel, channelsNum)
|
||||
func newThingsService(auth upolicies.AuthServiceClient) (clients.Service, groups.Service, tpolicies.Service) {
|
||||
channels := make(map[string]mfgroups.Group, channelsNum)
|
||||
for i := 0; i < channelsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
channels[id] = things.Channel{
|
||||
channels[id] = mfgroups.Group{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Metadata: map[string]interface{}{"meta": "data"},
|
||||
Status: mfclients.EnabledStatus,
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, auth)
|
||||
csvc := mocks.NewThingsService(map[string]mfclients.Client{}, auth)
|
||||
gsvc := mocks.NewChannelsService(channels, auth)
|
||||
psvc := mocks.NewPoliciesService(auth)
|
||||
return csvc, gsvc, psvc
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
func newThingsServer(csvc clients.Service, gsvc groups.Service, psvc tpolicies.Service) *httptest.Server {
|
||||
logger := mflog.NewMock()
|
||||
mux := bone.New()
|
||||
capi.MakeHandler(csvc, mux, logger)
|
||||
gapi.MakeHandler(gsvc, mux, logger)
|
||||
papi.MakeHandler(csvc, psvc, mux, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -648,7 +661,7 @@ func TestChangeState(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(context.Background(), tc.token, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ const (
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
|
||||
// | Active | Thing is able to communicate using Mainflux |
|
||||
// | Active | Thing is able to communicate using Mainflux |.
|
||||
type State int
|
||||
|
||||
// String returns string representation of State.
|
||||
|
||||
+18
-9
@@ -11,24 +11,26 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
logger mflog.Logger
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc certs.Service, logger log.Logger) certs.Service {
|
||||
// LoggingMiddleware adds logging facilities to the bootstrap service.
|
||||
func LoggingMiddleware(svc certs.Service, logger mflog.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
// IssueCert logs the issue_cert request. It logs the token, thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, ttl string) (c certs.Cert, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method issue_cert for token: %s and thing: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method issue_cert using token %s and thing %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
|
||||
@@ -39,9 +41,10 @@ func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, ttl
|
||||
return lm.svc.IssueCert(ctx, token, thingID, ttl)
|
||||
}
|
||||
|
||||
// ListCerts logs the list_certs request. It logs the token, thing ID and the time it took to complete the request.
|
||||
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 and thing id: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method list_certs using 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
|
||||
@@ -52,9 +55,11 @@ func (lm *loggingMiddleware) ListCerts(ctx context.Context, token, thingID strin
|
||||
return lm.svc.ListCerts(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
// ListSerials logs the list_serials request. It logs the token, thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (cp certs.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_serials for token: %s and thing id: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method list_serials using 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
|
||||
@@ -65,9 +70,11 @@ func (lm *loggingMiddleware) ListSerials(ctx context.Context, token, thingID str
|
||||
return lm.svc.ListSerials(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
// ViewCert logs the view_cert request. It logs the token, serial ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) ViewCert(ctx context.Context, token, serialID string) (c certs.Cert, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_cert for token: %s and serial id %s took %s to complete", token, serialID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method view_cert using token %s and serial id %s took %s to complete", token, serialID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -78,9 +85,11 @@ func (lm *loggingMiddleware) ViewCert(ctx context.Context, token, serialID strin
|
||||
return lm.svc.ViewCert(ctx, token, serialID)
|
||||
}
|
||||
|
||||
// RevokeCert logs the revoke_cert request. It logs the token, thing ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) RevokeCert(ctx context.Context, token, thingID string) (c certs.Revoke, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke_cert for token: %s and thing: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method revoke_cert using token %s and thing %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
|
||||
|
||||
@@ -30,6 +30,7 @@ func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metri
|
||||
}
|
||||
}
|
||||
|
||||
// IssueCert instruments IssueCert method with metrics.
|
||||
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID, ttl string) (certs.Cert, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_cert").Add(1)
|
||||
@@ -39,6 +40,7 @@ func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID, ttl
|
||||
return ms.svc.IssueCert(ctx, token, thingID, ttl)
|
||||
}
|
||||
|
||||
// ListCerts instruments ListCerts method with metrics.
|
||||
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)
|
||||
@@ -48,6 +50,7 @@ func (ms *metricsMiddleware) ListCerts(ctx context.Context, token, thingID strin
|
||||
return ms.svc.ListCerts(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
// ListSerials instruments ListSerials method with metrics.
|
||||
func (ms *metricsMiddleware) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_serials").Add(1)
|
||||
@@ -57,6 +60,7 @@ func (ms *metricsMiddleware) ListSerials(ctx context.Context, token, thingID str
|
||||
return ms.svc.ListSerials(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
// ViewCert instruments ViewCert method with metrics.
|
||||
func (ms *metricsMiddleware) ViewCert(ctx context.Context, token, serialID string) (certs.Cert, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_cert").Add(1)
|
||||
@@ -66,6 +70,7 @@ func (ms *metricsMiddleware) ViewCert(ctx context.Context, token, serialID strin
|
||||
return ms.svc.ViewCert(ctx, token, serialID)
|
||||
}
|
||||
|
||||
// RevokeCert instruments RevokeCert method with metrics.
|
||||
func (ms *metricsMiddleware) RevokeCert(ctx context.Context, token, thingID string) (certs.Revoke, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_cert").Add(1)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list
|
||||
// ConfigsPage contains page related metadata as well as list.
|
||||
type Page struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs_test
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package mocks contains mocks for testing purposes.
|
||||
package mocks
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki contains the domain concept definitions needed to
|
||||
// support Mainflux Certs service functionality.
|
||||
// It provides the abstraction of the PKI (Public Key Infrastructure)
|
||||
// Valut service, which is used to issue and revoke certificates.
|
||||
package pki
|
||||
+3
-3
@@ -20,13 +20,13 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingCACertificate indicates missing CA certificate
|
||||
// ErrMissingCACertificate indicates missing CA certificate.
|
||||
ErrMissingCACertificate = errors.New("missing CA certificate for certificate signing")
|
||||
|
||||
// ErrFailedCertCreation indicates failed to certificate creation
|
||||
// ErrFailedCertCreation indicates failed to certificate creation.
|
||||
ErrFailedCertCreation = errors.New("failed to create client certificate")
|
||||
|
||||
// ErrFailedCertRevocation indicates failed certificate revocation
|
||||
// ErrFailedCertRevocation indicates failed certificate revocation.
|
||||
ErrFailedCertRevocation = errors.New("failed to revoke certificate")
|
||||
|
||||
errFailedCertDecoding = errors.New("failed to decode response from vault service")
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of Certs service
|
||||
// Migration of Certs service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
|
||||
+14
-14
@@ -7,17 +7,17 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/certs/pki"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/users/policies"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFailedCertCreation failed to create certificate
|
||||
// ErrFailedCertCreation failed to create certificate.
|
||||
ErrFailedCertCreation = errors.New("failed to create client certificate")
|
||||
|
||||
// ErrFailedCertRevocation failed to revoke certificate
|
||||
// ErrFailedCertRevocation failed to revoke certificate.
|
||||
ErrFailedCertRevocation = errors.New("failed to revoke certificate")
|
||||
|
||||
ErrFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
|
||||
@@ -45,14 +45,14 @@ type Service interface {
|
||||
}
|
||||
|
||||
type certsService struct {
|
||||
auth mainflux.AuthServiceClient
|
||||
auth policies.AuthServiceClient
|
||||
certsRepo Repository
|
||||
sdk mfsdk.SDK
|
||||
pki pki.Agent
|
||||
}
|
||||
|
||||
// New returns new Certs service
|
||||
func New(auth mainflux.AuthServiceClient, certs Repository, sdk mfsdk.SDK, pki pki.Agent) Service {
|
||||
// New returns new Certs service.
|
||||
func New(auth policies.AuthServiceClient, certs Repository, sdk mfsdk.SDK, pki pki.Agent) Service {
|
||||
return &certsService{
|
||||
certsRepo: certs,
|
||||
sdk: sdk,
|
||||
@@ -61,12 +61,12 @@ func New(auth mainflux.AuthServiceClient, certs Repository, sdk mfsdk.SDK, pki p
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke defines the conditions to revoke a certificate
|
||||
// Revoke defines the conditions to revoke a certificate.
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
// Cert defines the certificate paremeters
|
||||
// Cert defines the certificate paremeters.
|
||||
type Cert struct {
|
||||
OwnerID string `json:"owner_id" mapstructure:"owner_id"`
|
||||
ThingID string `json:"thing_id" mapstructure:"thing_id"`
|
||||
@@ -80,7 +80,7 @@ type Cert struct {
|
||||
}
|
||||
|
||||
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, ttl string) (Cert, error) {
|
||||
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
owner, err := cs.auth.Identify(ctx, &policies.Token{Value: token})
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, tt
|
||||
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
|
||||
cert, err := cs.pki.IssueCert(thing.Key, ttl)
|
||||
cert, err := cs.pki.IssueCert(thing.Credentials.Secret, ttl)
|
||||
if err != nil {
|
||||
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, tt
|
||||
|
||||
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
|
||||
var revoke Revoke
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
u, err := cs.auth.Identify(ctx, &policies.Token{Value: token})
|
||||
if err != nil {
|
||||
return revoke, err
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (
|
||||
}
|
||||
|
||||
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})
|
||||
u, err := cs.auth.Identify(ctx, &policies.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, err
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func (cs *certsService) ListCerts(ctx context.Context, token, thingID string, of
|
||||
}
|
||||
|
||||
func (cs *certsService) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
u, err := cs.auth.Identify(ctx, &policies.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, err
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func (cs *certsService) ListSerials(ctx context.Context, token, thingID string,
|
||||
}
|
||||
|
||||
func (cs *certsService) ViewCert(ctx context.Context, token, serialID string) (Cert, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
u, err := cs.auth.Identify(ctx, &policies.Token{Value: token})
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
+20
-15
@@ -12,17 +12,18 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/go-zoo/bone"
|
||||
bsmocks "github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/certs/mocks"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
mfclients "github.com/mainflux/mainflux/pkg/clients"
|
||||
"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/mainflux/mainflux/things/clients"
|
||||
httpapi "github.com/mainflux/mainflux/things/clients/api"
|
||||
thmocks "github.com/mainflux/mainflux/things/clients/mocks"
|
||||
"github.com/mainflux/mainflux/users/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -48,8 +49,9 @@ func newService(tokens map[string]string) (certs.Service, error) {
|
||||
ac := bsmocks.NewAuthClient(map[string]string{token: email})
|
||||
server := newThingsServer(newThingsService(ac))
|
||||
|
||||
policies := []thmocks.MockSubjectSet{{Object: "users", Relation: "member"}}
|
||||
auth := thmocks.NewAuthService(tokens, map[string][]thmocks.MockSubjectSet{email: policies})
|
||||
policies := []thmocks.MockSubjectSet{{Object: "token", Relation: clients.AdminRelationKey}}
|
||||
auth := thmocks.NewAuthService(tokens, map[string][]thmocks.MockSubjectSet{token: policies})
|
||||
|
||||
config := mfsdk.Config{
|
||||
ThingsURL: server.URL,
|
||||
}
|
||||
@@ -72,18 +74,20 @@ func newService(tokens map[string]string) (certs.Service, error) {
|
||||
return certs.New(auth, repo, sdk, pki), nil
|
||||
}
|
||||
|
||||
func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
ths := make(map[string]things.Thing, thingsNum)
|
||||
func newThingsService(auth policies.AuthServiceClient) clients.Service {
|
||||
ths := make(map[string]mfclients.Client, thingsNum)
|
||||
for i := 0; i < thingsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
ths[id] = things.Thing{
|
||||
ID: id,
|
||||
Key: thingKey,
|
||||
ths[id] = mfclients.Client{
|
||||
ID: id,
|
||||
Credentials: mfclients.Credentials{
|
||||
Secret: thingKey,
|
||||
},
|
||||
Owner: email,
|
||||
}
|
||||
}
|
||||
|
||||
return bsmocks.NewThingsService(ths, map[string]things.Channel{}, auth)
|
||||
return bsmocks.NewThingsService(ths, auth)
|
||||
}
|
||||
|
||||
func TestIssueCert(t *testing.T) {
|
||||
@@ -359,8 +363,9 @@ func TestViewCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
func newThingsServer(svc clients.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
mux := bone.New()
|
||||
httpapi.MakeHandler(svc, mux, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user