mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92d8fb99bf | |||
| ad80cf868b | |||
| f0dac5c6ac | |||
| 48d6a95a21 | |||
| c6f7c69798 | |||
| ce46723a8f | |||
| 29f9cbb10a | |||
| 127044efad | |||
| f3ed852b36 | |||
| 0a6b2f135a | |||
| e5278c463f | |||
| cc5d5195ab | |||
| 5ca8495f35 | |||
| 9972d1d1a4 | |||
| 8b94199785 | |||
| d51a79c538 | |||
| 09cbc3f14b | |||
| 655ac838be | |||
| f8ce94e9bb | |||
| 1f8a221c22 | |||
| 9e0947a355 | |||
| e8a51842aa | |||
| 6ad654d7cb | |||
| 4f56986c8e | |||
| 81b2a1a2d3 | |||
| 89061d33e5 | |||
| 2abf9da27e | |||
| 42dd813521 | |||
| bcc8cf7119 | |||
| d86a4dee92 | |||
| 864ad144e6 | |||
| bea09d97dd | |||
| 6483969927 | |||
| aa014c2191 | |||
| dd7d52ef10 | |||
| ad8b7ddf5a | |||
| b18c9e79dc | |||
| 7e9ab453d4 | |||
| 309ef512cb | |||
| db6fab961e | |||
| f99f5d228d | |||
| 5112ef681a | |||
| 31d30b204d | |||
| be3e98f677 | |||
| 87510288c2 | |||
| b78928c998 | |||
| 39133b06a4 | |||
| 3042d6b40b | |||
| c70fb576b6 | |||
| 9f5a319519 | |||
| b570c38ed0 | |||
| dc935858bd | |||
| 90e4561491 | |||
| 30912e5a45 | |||
| 27d4646db4 | |||
| 042ff98509 | |||
| 5e9a91bd03 | |||
| 39ae7bdfa1 | |||
| 94cba4aab2 | |||
| 66d3da0531 | |||
| f4312aef8a | |||
| caa4bda1a1 | |||
| e1a66b3268 | |||
| 7b3c26f60f | |||
| d73a5d53fe | |||
| d6a3830ef4 | |||
| 5ac1203b55 | |||
| 19f0437f57 | |||
| 2cfff01979 | |||
| bb072b8ad2 | |||
| af0162f0df | |||
| 68af0e32b5 | |||
| cddfdf4038 | |||
| 0a89f1dae1 | |||
| d3e34b1662 | |||
| 9e5e50b347 | |||
| 516c02bebe | |||
| 38ca7f761b | |||
| 9ce31b65a6 | |||
| 79af1ba9a8 | |||
| e04d94ecc7 | |||
| e02e9c2387 | |||
| 063f73076d | |||
| 84978c034b | |||
| 0fe9f55bae | |||
| 00dc197fa6 | |||
| cd11728073 | |||
| 6770c19279 |
@@ -0,0 +1,26 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Swagger ui action
|
||||
id: swagger-ui-action
|
||||
uses: blokovi/swagger-ui-action@main
|
||||
with:
|
||||
dir: './api/openapi'
|
||||
pattern: '*.yml'
|
||||
debug: 'true'
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: api.mainflux.io
|
||||
@@ -11,6 +11,98 @@ Otherwise, whole log in a similar format can be observed via:
|
||||
git log --pretty=oneline --abbrev-commit
|
||||
```
|
||||
|
||||
## 0.13.0 - 15. APR 2022.
|
||||
### Features and Bugfixes
|
||||
- NOISSUE - Update changelog for release 0.13.0
|
||||
- Update VerneMQ release (#1593)
|
||||
- NOISSUE - Update changelog and readme for release 0.13.0
|
||||
- MF-1582 - Fix lora-adapter MQTT client (#1583)
|
||||
- NOISSUE - Fix CoAP adapter (#1572)
|
||||
- NOISSUE - Unify MF_INFLUX_READER_DB_HOST and MF_INFLUX_WRITER_DB_HOST envars (#1585)
|
||||
- MF-1580 - Influxdb Writer changes format of update-time to string (#1581)
|
||||
- MF-1575 Add 'Name' field to ListMembers response in things svc (#1576)
|
||||
- MF-1565 - Document Bearer, Thing and Basic Authorization header (#1566)
|
||||
- MF-1567 - Use Bearer, Thing or Basic scheme in Authorization header (#1568)
|
||||
- MF-1348 - Add transport errors logging (#1544)
|
||||
- NOISSUE - Add nats wrapper for COAP (#1569)
|
||||
- MF-1469 - Indicate proper authentication scheme in Authorization header (#1523)
|
||||
- MF-1240 - Return to service transport layer only service errors (#1559)
|
||||
- Update dependencies (#1564)
|
||||
- NOISSUE - Separate Keto hosts for read and write (#1563)
|
||||
- MF-1551 - Fix Cobra usage commands and clean unnecessary struct types (#1558)
|
||||
- MF-1257 - Access messages from readers endpoint with user access token (#1470)
|
||||
- NOISSUE - Refactor MQTT subscriber (#1561)
|
||||
- MF-1059 - Add TLS support for email (#1560)
|
||||
- MF-1261 - Use StatusUnauthorized for authn and StatusForbidden for authz (#1538)
|
||||
- NOISSUE - Fix auth members list response (#1555)
|
||||
- MF-1263 - Move repeating errors to the separate package (#1540)
|
||||
- NOISSUE - Add API keys functions to CLI (#1537)
|
||||
- Fix SDK for group members (#1553)
|
||||
- NOISSUE - Fix Swagger UI (#1552)
|
||||
- MF-1008 - Make token duration configurable (#1550)
|
||||
- MF-1308 - Use IETF Health Check standard (#1541)
|
||||
- Fix user listing access control (#1546)
|
||||
- Update dependencies (#1545)
|
||||
- MF-1478 - TimescaleDB writer and reader add-on (#1542)
|
||||
- MF-1149 - Add AsyncAPI MQTT API doc (#1539)
|
||||
- MF-1535 - Add API keys functions to SDK (#1536)
|
||||
- NOISSUE - Add view and list serials endpoints in certs service (#1483)
|
||||
- MF-1516 - Fix API key issuing (#1530)
|
||||
- NOISSUE - Add disconnect endpoint in nginx conf (#1528)
|
||||
- NOISSUE - Add timestamp transformation rules for specifc JSON fields (#1514)
|
||||
- MF-1425 - Support external UUIDs for Things and Channels (#1518)
|
||||
- MF-1521 - Fix email headers (#1522)
|
||||
- Fix SenML lib dependency version (#1519)
|
||||
- Bump vernemq to 1.12.3 (#1520)
|
||||
- NOISSUE - Remove auth URL from SDK (#1511)
|
||||
- NOISSUE - Apply policies to Channels (#1505)
|
||||
- remove dead code (#1503)
|
||||
- NOISSUE - Fix listing (#1502)
|
||||
- NOISSUE - Listing Policies (#1498)
|
||||
- Fix standalone mode (#1497)
|
||||
- MF-1489 - Add API for deleting policies (#1491)
|
||||
- NOISSUE - Update group sharing policies (#1494)
|
||||
- NOISSUE - Refactor InfluxDB Reader: explicit check event + add safe conversion (#1460)
|
||||
- NOISSUE - Update users create command for CLI (#1495)
|
||||
- NOISSUE - Update self register environment variable name (#1493)
|
||||
- Bring back the job add
|
||||
- NOISSUE - Fix assigning invalid group policy (#1487)
|
||||
- MF-1443 - Add policies (#1482)
|
||||
- NOISSUE - Fix retrieving all users (#1477)
|
||||
- MF-1468 - Fix ThingsURL in Certs Service (#1474)
|
||||
- NOISSUE - Refactor single-user mode (#1471)
|
||||
- Fix UpdateChannelHandler for Redis producer (#1473)
|
||||
- NOISSUE - Add SMPP notifier (#1464)
|
||||
- NOISSUE - Update dependencies (#1453)
|
||||
- NOISSUE - Fix security warnings for dependencies (#1452)
|
||||
- Bump docker-compose version in prereq (#1449)
|
||||
- NOISSUE - Fix bootstraping (#1448)
|
||||
- MF 1413 - Use per-service URL in SDK (#1444)
|
||||
- MF-1439 - Add support for Basic Authentication in HTTP Adapter (#1441)
|
||||
- MF-1421 - Make flattening of JSON transformer only available on InfluxDB (#1432)
|
||||
- NOISSUE - Update the /disconnect endpoint HTTP method as PUT (#1438)
|
||||
- MF-1389 - Add /disconnect endpoint in Things service (#1433)
|
||||
- NOISSUE - Fix httputil implementation in users service (#1434)
|
||||
- Fix fetching user members of an empty group (#1436)
|
||||
- Change to user friendly docs urls (#1430)
|
||||
- NOISSUE - Use github action for showing OpenAPI spec with Swagger UI (#1427)
|
||||
- Fix JSON Transformer empty format handling (#1429)
|
||||
- Update README
|
||||
- NOISSUE - Update docker-compose images to latest release (#1419)
|
||||
- MF-1378 - Update dependencies (#1379)
|
||||
|
||||
## 0.12.1 - 05. MAY 2021.
|
||||
### Features and Bugfixes
|
||||
- NOISSUE - Refactor SDK memberships and fix openapi for memberships.
|
||||
- NOISSUE - Fix incorrect influxdb credentials
|
||||
- MF-1408 - Fix error handling for Thing update SQL(#1408)
|
||||
- MF-1288 - Add tests for JSON messages in message writers and readers
|
||||
- NOISSUE - Fix Postgres Reader order
|
||||
- NOISSUE - Fix nginx configuration for groups
|
||||
- NOISSUE - Add tests and connection route-map to lora-adapter
|
||||
- MF-1403 - Change vernemq building source revision
|
||||
- NOISSUE - Rm content-type check from list endpoint
|
||||
|
||||
## 0.12.0 - 29. MAR 2021.
|
||||
### Features and Bugfixes
|
||||
- MF-1394 - SDK groups (#1396)
|
||||
|
||||
@@ -4,15 +4,23 @@
|
||||
MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
|
||||
bootstrap opcua auth twins mqtt provision certs smtp-notifier
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader timescale-writer timescale-reader cli \
|
||||
bootstrap opcua auth twins mqtt provision certs smtp-notifier smpp-notifier
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
VERSION ?= $(shell git describe --abbrev=0 --tags)
|
||||
COMMIT ?= $(shell git rev-parse HEAD)
|
||||
TIME ?= $(shell date +%F_%T)
|
||||
|
||||
define compile_service
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -mod=vendor -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||
go build -mod=vendor -ldflags "-s -w \
|
||||
-X 'github.com/mainflux/mainflux.BuildTime=$(TIME)' \
|
||||
-X 'github.com/mainflux/mainflux.Version=$(VERSION)' \
|
||||
-X 'github.com/mainflux/mainflux.Commit=$(COMMIT)'" \
|
||||
-o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
@@ -23,6 +31,9 @@ define make_docker
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg COMMIT=$(COMMIT) \
|
||||
--build-arg TIME=$(TIME) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
@@ -101,13 +112,3 @@ rundev:
|
||||
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
runlora:
|
||||
docker-compose \
|
||||
-f docker/docker-compose.yml \
|
||||
-f docker/addons/influxdb-writer/docker-compose.yml \
|
||||
-f docker/addons/lora-adapter/docker-compose.yml up \
|
||||
|
||||
# Run all Mainflux core services except distributed tracing system - Jaeger. Recommended on gateways:
|
||||
rungw:
|
||||
MF_JAEGER_URL= docker-compose -f docker/docker-compose.yml up --scale jaeger=0
|
||||
|
||||
@@ -16,10 +16,6 @@ for building complex IoT solutions.
|
||||
|
||||
For more details, check out the [official documentation][docs].
|
||||
|
||||
Mainflux is member of the [Linux Foundation][lf] and an active contributor
|
||||
to the [EdgeX Foundry][edgex] project. It has been made with :heart: by [Mainflux Labs][company],
|
||||
which maintains the project and offers professional services around it.
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-protocol connectivity and bridging (HTTP, MQTT, WebSocket and CoAP)
|
||||
@@ -43,7 +39,7 @@ which maintains the project and offers professional services around it.
|
||||
The following are needed to run Mainflux:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/) (version 20.10)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.28)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/) (version 1.29)
|
||||
|
||||
Developing Mainflux will also require:
|
||||
|
||||
@@ -69,7 +65,7 @@ If you want to run services from specific release checkout code from github and
|
||||
|
||||
```bash
|
||||
git checkout tags/<release_number> -b <release_number>
|
||||
# e.g. `git checkout tags/0.12.0 -b 0.12.0`
|
||||
# e.g. `git checkout tags/0.13.0 -b 0.13.0`
|
||||
```
|
||||
|
||||
Check that `.env` file contains:
|
||||
@@ -78,7 +74,7 @@ Check that `.env` file contains:
|
||||
MF_RELEASE_TAG=<release_number>
|
||||
```
|
||||
|
||||
>`docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://mainflux.readthedocs.io/en/latest/kubernetes/).
|
||||
>`docker-compose` should be used for development and testing deployments. For production we suggest using [Kubernetes](https://docs.mainflux.io/kubernetes).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -91,16 +87,14 @@ make cli
|
||||
./build/mainflux-cli version
|
||||
```
|
||||
|
||||
Additional details on using the CLI can be found in the [CLI documentation](https://mainflux.readthedocs.io/en/latest/cli/).
|
||||
Additional details on using the CLI can be found in the [CLI documentation](https://docs.mainflux.io/cli).
|
||||
|
||||
## Documentation
|
||||
|
||||
Official documentation is hosted at [Mainflux Read The Docs page][docs]. Documentation is auto-generated, checkout the instructions on [official docs repository](https://github.com/mainflux/docs):
|
||||
Official documentation is hosted at [Mainflux official docs page][docs]. Documentation is auto-generated, checkout the instructions on [official docs repository](https://github.com/mainflux/docs):
|
||||
|
||||
If you spot an error or a need for corrections, please let us know - or even better: send us a PR.
|
||||
|
||||
Additional practical information, news and tutorials can be found on the [Mainflux blog][blog].
|
||||
|
||||
## Authors
|
||||
|
||||
Main architect and BDFL of Mainflux project is [@drasko][drasko].
|
||||
@@ -119,6 +113,12 @@ The Mainflux team would like to give special thanks to [@mijicd][dejan] for his
|
||||
on designing and implementing a highly improved and optimized version of the platform,
|
||||
and [@malidukica][dusanm] for his effort on implementing the initial user interface.
|
||||
|
||||
## Professional Support
|
||||
|
||||
There are many companies offering professional support for the Mainflux system.
|
||||
|
||||
If you need this kind of support, best is to reach out to [@drasko][drasko] directly, and he will point you out to the best-matching support team.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for your interest in Mainflux and the desire to contribute!
|
||||
@@ -129,10 +129,9 @@ Thank you for your interest in Mainflux and the desire to contribute!
|
||||
|
||||
### We're Hiring
|
||||
|
||||
If you are interested in working professionally on Mainflux,
|
||||
please head to company's [careers page][careers] or shoot us an e-mail at <careers@mainflux.com>.
|
||||
You like Mainflux and you would like to make it your day job? We're always looking for talented engineers interested in open-source, IoT and distributed systems. If you recognize yourself, reach out to [@drasko][drasko] - he will contact you back.
|
||||
|
||||
>The best way to grab our attention is by sending PRs :sunglasses:.
|
||||
>The best way to grab our attention is, of course, by sending PRs :sunglasses:.
|
||||
|
||||
## Community
|
||||
|
||||
@@ -149,7 +148,7 @@ please head to company's [careers page][careers] or shoot us an e-mail at <caree
|
||||
[banner]: https://github.com/mainflux/docs/blob/master/docs/img/gopherBanner.jpg
|
||||
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
|
||||
[ci-url]: https://semaphoreci.com/mainflux/mainflux
|
||||
[docs]: http://mainflux.readthedocs.io
|
||||
[docs]: https://docs.mainflux.io
|
||||
[docker]: https://www.docker.com
|
||||
[forum]: https://groups.google.com/forum/#!forum/mainflux
|
||||
[gitter]: https://gitter.im/mainflux/mainflux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
asyncapi: '2.2.0'
|
||||
info:
|
||||
title: MQTT Adapter
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0
|
||||
version: '1.0.0'
|
||||
description: |
|
||||
MQTT adapter provides an MQTT API for sending messages through the platform. MQTT adapter uses [mProxy](https://github.com/mainflux/mproxy) for proxying traffic between client and MQTT broker.
|
||||
Additionally, the MQTT adapter and the NATS message broker are replicating the traffic between brokers.
|
||||
|
||||
defaultContentType: application/json
|
||||
|
||||
servers:
|
||||
dev:
|
||||
url: localhost:{port}
|
||||
protocol: mqtt
|
||||
description: Test broker
|
||||
variables:
|
||||
port:
|
||||
description: Secure connection (TLS) is available through port 8883.
|
||||
default: '1883'
|
||||
enum:
|
||||
- '1883'
|
||||
- '8883'
|
||||
username:
|
||||
description: Thing ID connected to the channel defined in the MQTT topic.
|
||||
password:
|
||||
description: Thing Key corresponding to the Thing ID.
|
||||
|
||||
channels:
|
||||
channels/{channelId}/messages/{subtopic}:
|
||||
parameters:
|
||||
channelId:
|
||||
$ref: '#/components/parameters/channelId'
|
||||
subtopic:
|
||||
$ref: '#/components/parameters/subtopic'
|
||||
publish:
|
||||
traits:
|
||||
- $ref: '#/components/operationTraits/mqtt'
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
subscribe:
|
||||
traits:
|
||||
- $ref: '#/components/operationTraits/mqtt'
|
||||
message:
|
||||
$ref: '#/components/messages/jsonMsg'
|
||||
|
||||
components:
|
||||
messages:
|
||||
jsonMsg:
|
||||
title: JSON Message
|
||||
summary: Arbitrary JSON array or object.
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: "#/components/schemas/jsonMsg"
|
||||
|
||||
schemas:
|
||||
jsonMsg:
|
||||
type: object
|
||||
description: Arbitrary JSON object or array. SenML format is recommended.
|
||||
example: |
|
||||
### SenML
|
||||
```json
|
||||
[{"bn":"some-base-name:","bt":1641646520, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]
|
||||
```
|
||||
### JSON
|
||||
```json
|
||||
{"field_1":"val_1", "t": 1641646525}
|
||||
```
|
||||
### JSON Array
|
||||
```json
|
||||
[{"field_1":"val_1", "t": 1641646520},{"field_2":"val_2", "t": 1641646522}]
|
||||
```
|
||||
|
||||
parameters:
|
||||
channelId:
|
||||
description: Channel ID connected to the Thing ID defined in the username.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
subtopic:
|
||||
description: Arbitrary message subtopic.
|
||||
schema:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
operationTraits:
|
||||
mqtt:
|
||||
bindings:
|
||||
mqtt:
|
||||
qos: 2
|
||||
@@ -0,0 +1,5 @@
|
||||
# Mainflux OpenAPI Specification
|
||||
|
||||
This folder contains an OpenAPI specifications for Mainflux API.
|
||||
|
||||
View specification in Swagger UI at [api.mainflux.io](https://api.mainflux.io)
|
||||
@@ -33,14 +33,13 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -51,12 +50,11 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
responses:
|
||||
'204':
|
||||
description: Key revoked.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -68,7 +66,6 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/requestBodies/GroupCreateReq"
|
||||
responses:
|
||||
'201':
|
||||
@@ -85,12 +82,11 @@ paths:
|
||||
summary: Gets all groups.
|
||||
description: |
|
||||
Gets all groups up to a max level of hierarchy that can be fetched in one
|
||||
request ( max level = 5). Result can be filtered by metadata. Groups will
|
||||
request ( max level = 5). Result can be filtered by metadata. Groups will
|
||||
be returned as JSON array or JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
@@ -99,7 +95,7 @@ paths:
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
@@ -113,14 +109,13 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
@@ -133,7 +128,6 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/GroupUpdateReq"
|
||||
@@ -142,7 +136,7 @@ paths:
|
||||
description: Group updated.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
@@ -152,12 +146,11 @@ paths:
|
||||
summary: Deletes group.
|
||||
description: |
|
||||
Deletes group. If group is parent and descendant groups do not have any members
|
||||
child groups will be deleted. Group cannot be deleted if has members or if
|
||||
child groups will be deleted. Group cannot be deleted if has members or if
|
||||
any descendant group has members.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
@@ -167,7 +160,7 @@ paths:
|
||||
description: Group removed.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
@@ -183,7 +176,6 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
@@ -193,7 +185,7 @@ paths:
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
@@ -209,7 +201,6 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
@@ -219,7 +210,7 @@ paths:
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
@@ -233,7 +224,6 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MembersReq"
|
||||
@@ -242,7 +232,7 @@ paths:
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
@@ -255,18 +245,111 @@ paths:
|
||||
description: |
|
||||
Array of member ids that are in the group specified with groupID.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/MemberType"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/MembersRes"
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{userGroupID}/share:
|
||||
post:
|
||||
summary: Adds access rights on thing groups to user group with userGroupID.
|
||||
description: |
|
||||
Takes user group id through parameter and adds access rights for user group on thing group received via request body.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/UserGroupID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ShareGroupAccessReq"
|
||||
responses:
|
||||
'200':
|
||||
description: User group shared with thing group.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/members/{memberId}/groups:
|
||||
get:
|
||||
summary: Gets memberships for a member with member id.
|
||||
description: |
|
||||
Array of groups that member belongs to.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/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:
|
||||
@@ -311,7 +394,7 @@ components:
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
description: Group description, free form text.
|
||||
parent_id:
|
||||
type: string
|
||||
format: ulid
|
||||
@@ -328,7 +411,7 @@ components:
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
description: Group description, free form text.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
@@ -389,6 +472,13 @@ components:
|
||||
type:
|
||||
type: string
|
||||
description: Type of entity
|
||||
ShareGroupAccessReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
thing_group_id:
|
||||
type: string
|
||||
description: Group ID of the Thing Group.
|
||||
format: uuid
|
||||
GroupsPage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -428,15 +518,28 @@ components:
|
||||
description: Total number of items.
|
||||
required:
|
||||
- groups
|
||||
PoliciesReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
object:
|
||||
type: string
|
||||
description: |
|
||||
Specifies an object field for the field.
|
||||
Object indicates application objects such as ThingID.
|
||||
subjects:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
policies:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
ApiKeyId:
|
||||
name: id
|
||||
description: API Key ID.
|
||||
@@ -445,6 +548,14 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
UserGroupID:
|
||||
name: userGroupID
|
||||
description: User Group ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Group ID.
|
||||
@@ -453,13 +564,13 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
MemberType:
|
||||
name: type
|
||||
description: Member type association.
|
||||
MemberId:
|
||||
name: memberId
|
||||
description: Member id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
enum: [users, things]
|
||||
format: uuid | ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
@@ -505,6 +616,7 @@ components:
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
@@ -518,30 +630,25 @@ components:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently.
|
||||
token:
|
||||
type: string
|
||||
format: jwt
|
||||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI"
|
||||
description: JWT for the entity that's sending Key request.
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
GroupCreateReq:
|
||||
GroupCreateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupReqSchema"
|
||||
$ref: "#/components/schemas/GroupReqSchema"
|
||||
GroupUpdateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupUpdateSchema"
|
||||
$ref: "#/components/schemas/GroupUpdateSchema"
|
||||
MembersReq:
|
||||
description: JSON array of member IDs.
|
||||
required: true
|
||||
@@ -549,6 +656,21 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembersReqSchema"
|
||||
ShareGroupAccessReq:
|
||||
description: test
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareGroupAccessReqSchema"
|
||||
PoliciesReq:
|
||||
description: JSON-formatted document describing adding policies request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PoliciesReqSchema"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
@@ -568,6 +690,8 @@ components:
|
||||
type: string
|
||||
description: Created group's relative URL.
|
||||
example: /groups/{groupId}
|
||||
ShareAccessRightRes:
|
||||
description: User group shared with thing group.
|
||||
GroupRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
@@ -592,3 +716,20 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -13,8 +13,6 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
@@ -22,7 +20,7 @@ paths:
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
@@ -38,7 +36,6 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
@@ -48,7 +45,7 @@ paths:
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -58,12 +55,11 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
@@ -78,7 +74,6 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
@@ -87,7 +82,7 @@ paths:
|
||||
description: Config updated.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
@@ -103,14 +98,13 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
'204':
|
||||
description: Config removed.
|
||||
'400':
|
||||
description: Failed due to malformed config ID.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -123,7 +117,6 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
@@ -132,7 +125,7 @@ paths:
|
||||
description: Config updated.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
@@ -149,7 +142,6 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
@@ -158,7 +150,7 @@ paths:
|
||||
description: Config updated.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Config does not exist.
|
||||
@@ -173,15 +165,19 @@ paths:
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid external key provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
description: Failed to retrieve corresponding config.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
@@ -191,8 +187,9 @@ paths:
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapEncAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/EncConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
'200':
|
||||
@@ -211,7 +208,6 @@ paths:
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
|
||||
@@ -220,10 +216,20 @@ paths:
|
||||
description: Config removed.
|
||||
'400':
|
||||
description: Failed due to malformed config's ID.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
@@ -331,31 +337,6 @@ components:
|
||||
- content
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
EncConfigAuth:
|
||||
name: configAuthorization
|
||||
description: |
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
@@ -514,3 +495,37 @@ components:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
bootstrapAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: string
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <external_key>"
|
||||
|
||||
bootstrapEncAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: aes-sha256-uuid
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <external_enc_key>"
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -0,0 +1,257 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Certs service
|
||||
description: HTTP API for Certs service
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/certs:
|
||||
post:
|
||||
summary: Creates a certificate for thing
|
||||
description: Creates a certificate for thing
|
||||
tags:
|
||||
- certs
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
description: Unexpected server-side error ocurred.
|
||||
/certs/{certID}:
|
||||
get:
|
||||
summary: Retrieves a certificate
|
||||
description: |
|
||||
Retrieves a certificate for a given cert ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/CertRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revokes a certificate
|
||||
description: |
|
||||
Revokes a certificate for a given cert ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/serials/{thingID}:
|
||||
get:
|
||||
summary: Retrieves certificates' serial IDs
|
||||
description: |
|
||||
Retrieves a list of certificates' serial IDs for a given thing ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/SerialsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding certificates.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
ThingID:
|
||||
name: thingID
|
||||
description: Thing ID
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
CertID:
|
||||
name: certID
|
||||
description: Serial of certificate
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Cert:
|
||||
type: object
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
issuing_ca:
|
||||
type: string
|
||||
description: CA Certificate that is used to issue client certs, usually intermediate.
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Serial:
|
||||
type: object
|
||||
properties:
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
CertsPage:
|
||||
type: object
|
||||
properties:
|
||||
certs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
SerialsPage:
|
||||
type: object
|
||||
properties:
|
||||
serials:
|
||||
type: array
|
||||
description: Certificate serials IDs.
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
Revoke:
|
||||
type: object
|
||||
properties:
|
||||
revocation_time:
|
||||
type: string
|
||||
description: Certificate revocation time
|
||||
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- thing_id
|
||||
- ttl
|
||||
- key_bits
|
||||
- key_type
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
ttl:
|
||||
type: string
|
||||
key_type:
|
||||
type: string
|
||||
key_bits:
|
||||
type: integer
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
CertRes:
|
||||
description: Certificate data.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
CertsPageRes:
|
||||
description: Certificates page.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CertsPage"
|
||||
SerialsPageRes:
|
||||
description: Serials page.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SerialsPage"
|
||||
RevokeRes:
|
||||
description: Certificate revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revoke"
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -10,8 +10,6 @@ paths:
|
||||
description: Creates a new subscription give a topic and contact.
|
||||
tags:
|
||||
- notifiers
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/Create"
|
||||
responses:
|
||||
@@ -30,8 +28,6 @@ paths:
|
||||
description: List subscriptions given list parameters.
|
||||
tags:
|
||||
- notifiers
|
||||
security:
|
||||
- Authorization: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Topic"
|
||||
- $ref: "#/components/parameters/Contact"
|
||||
@@ -42,7 +38,7 @@ paths:
|
||||
$ref: "#/components/responses/Page"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"403":
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -52,14 +48,12 @@ paths:
|
||||
description: Retrieves a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
security:
|
||||
- Authorization: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/View"
|
||||
"403":
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -68,24 +62,27 @@ paths:
|
||||
description: Removes a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
security:
|
||||
- Authorization: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"204":
|
||||
description: Subscription removed
|
||||
"403":
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
Authorization:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
schemas:
|
||||
Subscription:
|
||||
type: object
|
||||
@@ -204,3 +201,20 @@ components:
|
||||
$ref: "#/components/schemas/Page"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -13,23 +13,32 @@ paths:
|
||||
tags:
|
||||
- messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MessageReq"
|
||||
responses:
|
||||
'202':
|
||||
"202":
|
||||
description: Message is accepted for processing.
|
||||
'400':
|
||||
"400":
|
||||
description: Message discarded due to its malformed content.
|
||||
'403':
|
||||
description: Message discarded due to missing or invalid credentials.
|
||||
'404':
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Message discarded due to invalid channel id.
|
||||
'415':
|
||||
"415":
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
'500':
|
||||
description: Unexpected server-side error occurred.
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
@@ -96,14 +105,6 @@ components:
|
||||
$ref: "#/components/schemas/SenMLRecord"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
ID:
|
||||
name: id
|
||||
description: Unique channel identifier.
|
||||
@@ -116,13 +117,46 @@ components:
|
||||
requestBodies:
|
||||
MessageReq:
|
||||
description: |
|
||||
Message to be distributed. Since the platform expects messages to be
|
||||
properly formatted SenML in order to be post-processed, clients are
|
||||
obliged to specify Content-Type header for each published message.
|
||||
Note that all messages that aren't SenML will be accepted and published,
|
||||
but no post-processing will be applied.
|
||||
Message to be distributed. Since the platform expects messages to be
|
||||
properly formatted SenML in order to be post-processed, clients are
|
||||
obliged to specify Content-Type header for each published message.
|
||||
Note that all messages that aren't SenML will be accepted and published,
|
||||
but no post-processing will be applied.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SenMLArray"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Thing access: "Authorization: Thing <thing_key>"
|
||||
|
||||
basicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
* Things access: "Authorization: Basic <base64-encoded_credentials>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- basicAuth: []
|
||||
@@ -11,8 +11,6 @@ paths:
|
||||
description: Adds new device to proxy
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ProvisionReq"
|
||||
responses:
|
||||
@@ -20,10 +18,10 @@ paths:
|
||||
description: Created
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
description: Unauthorized.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
description: Unexpected server-side error ocurred.
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets current mapping.
|
||||
description: Gets current mapping. This can be used in UI
|
||||
@@ -31,28 +29,25 @@ paths:
|
||||
configuration created with provision service.
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/ProvisionRes"
|
||||
'403':
|
||||
description: Unauthorized.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
description: Unexpected server-side error ocurred.
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token. Used instead of credentials in env or config.toml.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ProvisionReq:
|
||||
description: MAC address of device or other identifier
|
||||
@@ -72,9 +67,28 @@ components:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
ProvisionRes:
|
||||
description: Current mapping JSON representation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -16,7 +16,6 @@ paths:
|
||||
tags:
|
||||
- messages
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
@@ -33,10 +32,20 @@ paths:
|
||||
$ref: "#/components/responses/MessagesPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
@@ -97,13 +106,6 @@ components:
|
||||
description: Time of updating measurement.
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Thing access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
@@ -210,6 +212,30 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesPage"
|
||||
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
thingAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <thing_key>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- thingAuth: []
|
||||
@@ -0,0 +1,23 @@
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Service status.
|
||||
enum:
|
||||
- pass
|
||||
version:
|
||||
type: string
|
||||
description: Service version.
|
||||
example: 0.0.1
|
||||
commit:
|
||||
type: string
|
||||
description: Service commit hash.
|
||||
example: 7d6f4dc4f7f0c1fa3dc24eddfb18bb5073ff4f62
|
||||
description:
|
||||
type: string
|
||||
description: Service description.
|
||||
example: <service_name> service
|
||||
build_time:
|
||||
type: string
|
||||
description: Service build time.
|
||||
example: 1970-01-01_00:00:00
|
||||
@@ -13,8 +13,6 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingCreateReq"
|
||||
responses:
|
||||
@@ -42,7 +40,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
@@ -73,8 +70,6 @@ paths:
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingsSearchReq"
|
||||
responses:
|
||||
@@ -98,8 +93,6 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingsCreateReq"
|
||||
responses:
|
||||
@@ -119,7 +112,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
'200':
|
||||
@@ -141,7 +133,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ThingUpdateReq"
|
||||
@@ -166,7 +157,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
'204':
|
||||
@@ -177,6 +167,31 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}/share:
|
||||
post:
|
||||
summary: Shares a thing with user identified by request body.
|
||||
description: |
|
||||
Adds 'read', 'write' or 'delete' policies to the user identified by the request body.
|
||||
Sharing a particular thing is only allowed to users who have 'write' access to that thing.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ShareThingReq"
|
||||
responses:
|
||||
'200':
|
||||
description: Policies shared.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'403':
|
||||
description: Lack of policies in order to share the thing.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}/key:
|
||||
patch:
|
||||
summary: Updates thing key
|
||||
@@ -185,7 +200,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyUpdateReq"
|
||||
@@ -212,8 +226,6 @@ paths:
|
||||
be the channel's owner.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ChannelCreateReq"
|
||||
responses:
|
||||
@@ -239,7 +251,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
@@ -265,8 +276,6 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ChannelsCreateReq"
|
||||
responses:
|
||||
@@ -288,7 +297,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
responses:
|
||||
'200':
|
||||
@@ -312,7 +320,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ChannelCreateReq"
|
||||
@@ -337,7 +344,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
responses:
|
||||
'204':
|
||||
@@ -356,8 +362,6 @@ paths:
|
||||
Channel and thing are owned by user identified using the provided access token.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConnCreateReq"
|
||||
responses:
|
||||
@@ -375,6 +379,29 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/disconnect:
|
||||
put:
|
||||
summary: Disconnect things and channels using lists of IDs.
|
||||
description: |
|
||||
Disconnect things from channels specified by lists of IDs.
|
||||
Channels and things are owned by user identified using the provided access token.
|
||||
tags:
|
||||
- things
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/DisconnReq"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/DisconnRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/{thingId}/channels:
|
||||
get:
|
||||
summary: List of channels connected to specified thing
|
||||
@@ -384,7 +411,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
@@ -411,7 +437,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
@@ -438,7 +463,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
@@ -460,7 +484,6 @@ paths:
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/ThingId"
|
||||
responses:
|
||||
@@ -550,7 +573,6 @@ paths:
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
@@ -570,6 +592,17 @@ paths:
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
@@ -585,7 +618,10 @@ components:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Thing unique identifier
|
||||
description: Thing unique identifier. This can be either
|
||||
provided by the user or left blank. If the user provides a UUID,
|
||||
it would be validated. If there is not one provided then
|
||||
the service will generate one in UUID format.
|
||||
ThingReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -731,16 +767,21 @@ components:
|
||||
description: Thing IDs
|
||||
items:
|
||||
type: string
|
||||
ShareThingReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
description: User ID.
|
||||
items:
|
||||
type: string
|
||||
policies:
|
||||
type: array
|
||||
description: Policies
|
||||
items:
|
||||
type: string
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
@@ -912,6 +953,13 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConnectionReqSchema"
|
||||
DisconnReq:
|
||||
description: JSON-formatted document describing the entities for disconnection.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConnectionReqSchema"
|
||||
IdentityReq:
|
||||
description: JSON-formatted document that contains thing key.
|
||||
required: true
|
||||
@@ -938,6 +986,13 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Thing ID by which thing is uniquely identified.
|
||||
ShareThingReq:
|
||||
description: JSON-formatted document describing sharing things policies.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareThingReqSchema"
|
||||
|
||||
responses:
|
||||
CreateThingRes:
|
||||
@@ -993,6 +1048,8 @@ components:
|
||||
type: string
|
||||
description: Created thing's relative URL.
|
||||
example: /things/{thingId}
|
||||
DisconnRes:
|
||||
description: Things disconnected.
|
||||
AccessGrantedRes:
|
||||
description: |
|
||||
Thing has access to the specified channel and the thing ID is returned.
|
||||
@@ -1013,3 +1070,20 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
format: byte
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -13,8 +13,6 @@ paths:
|
||||
the provided access token.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TwinReq"
|
||||
responses:
|
||||
@@ -22,7 +20,7 @@ paths:
|
||||
$ref: "#/components/responses/TwinCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
@@ -37,7 +35,6 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Name'
|
||||
@@ -47,7 +44,7 @@ paths:
|
||||
$ref: '#/components/responses/TwinsPageRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
@@ -58,14 +55,13 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/TwinRes'
|
||||
'400':
|
||||
description: Failed due to malformed twin's ID.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
@@ -79,7 +75,6 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/TwinReq'
|
||||
@@ -88,7 +83,7 @@ paths:
|
||||
description: Twin updated.
|
||||
'400':
|
||||
description: Failed due to malformed twin's ID or malformed JSON.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
@@ -102,14 +97,13 @@ paths:
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
responses:
|
||||
'204':
|
||||
description: Twin removed.
|
||||
'400':
|
||||
description: Failed due to malformed twin's ID.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
@@ -126,7 +120,6 @@ paths:
|
||||
- states
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TwinID'
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
responses:
|
||||
@@ -134,23 +127,25 @@ paths:
|
||||
$ref: '#/components/responses/StatesPageRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Twin does not exist.
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
@@ -365,3 +360,20 @@ components:
|
||||
$ref: '#/components/schemas/StatesPage'
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -20,12 +20,14 @@ paths:
|
||||
$ref: "#/components/responses/UserCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves users
|
||||
description: |
|
||||
@@ -36,7 +38,6 @@ paths:
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
@@ -46,7 +47,9 @@ paths:
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
description: |
|
||||
Missing or invalid access token provided.
|
||||
This endpoint is available only for administrators.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
@@ -60,8 +63,6 @@ paths:
|
||||
authorization token and the new received info.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UserUpdateReq"
|
||||
responses:
|
||||
@@ -71,7 +72,7 @@ paths:
|
||||
description: Failed due to malformed JSON.
|
||||
'404':
|
||||
description: Failed due to non existing user.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -83,14 +84,12 @@ paths:
|
||||
authorization token
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/UserRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
@@ -105,7 +104,6 @@ paths:
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
@@ -144,7 +142,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'403':
|
||||
'401':
|
||||
description: Failed due to using invalid credentials.
|
||||
content:
|
||||
application/json:
|
||||
@@ -206,8 +204,6 @@ paths:
|
||||
When authenticated user wants to change password.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/PasswordChange'
|
||||
responses:
|
||||
@@ -219,14 +215,18 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
Authorization:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: jwt
|
||||
|
||||
schemas:
|
||||
Token:
|
||||
type: object
|
||||
@@ -301,15 +301,8 @@ components:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
Referer:
|
||||
name: Referer
|
||||
description: Host being sent by browser.
|
||||
@@ -452,3 +445,20 @@ components:
|
||||
$ref: "#/components/schemas/UsersPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
+1553
-44
File diff suppressed because it is too large
Load Diff
+33
@@ -18,6 +18,9 @@ service AuthService {
|
||||
rpc Issue(IssueReq) returns (Token) {}
|
||||
rpc Identify(Token) returns (UserIdentity) {}
|
||||
rpc Authorize(AuthorizeReq) returns (AuthorizeRes) {}
|
||||
rpc AddPolicy(AddPolicyReq) returns (AddPolicyRes) {}
|
||||
rpc DeletePolicy(DeletePolicyReq) returns (DeletePolicyRes) {}
|
||||
rpc ListPolicies(ListPoliciesReq) returns (ListPoliciesRes) {}
|
||||
rpc Assign(Assignment) returns(google.protobuf.Empty) {}
|
||||
rpc Members(MembersReq) returns (MembersRes) {}
|
||||
}
|
||||
@@ -73,6 +76,36 @@ message AuthorizeRes {
|
||||
bool authorized = 1;
|
||||
}
|
||||
|
||||
message AddPolicyReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message AddPolicyRes {
|
||||
bool authorized = 1;
|
||||
}
|
||||
|
||||
message DeletePolicyReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message DeletePolicyRes {
|
||||
bool deleted = 1;
|
||||
}
|
||||
|
||||
message ListPoliciesReq {
|
||||
string sub = 1;
|
||||
string obj = 2;
|
||||
string act = 3;
|
||||
}
|
||||
|
||||
message ListPoliciesRes {
|
||||
repeated string policies = 1;
|
||||
}
|
||||
|
||||
message Assignment {
|
||||
string token = 1;
|
||||
string groupID = 2;
|
||||
|
||||
+26
-21
@@ -55,24 +55,29 @@ The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------|
|
||||
| MF_AUTH_LOG_LEVEL | Service level (debug, info, warn, error) | error |
|
||||
| MF_AUTH_DB_HOST | Database host address | localhost |
|
||||
| MF_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| MF_AUTH_DB_USER | Database user | mainflux |
|
||||
| MF_AUTH_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_AUTH_DB | Name of the database used by the service | auth |
|
||||
| MF_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_AUTH_HTTP_PORT | Auth service HTTP port | 8180 |
|
||||
| MF_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| MF_AUTH_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTH_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTH_SECRET | String used for signing tokens | auth |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831|
|
||||
| Variable | Description | Default |
|
||||
|-------------------------------|--------------------------------------------------------------------------|----------------|
|
||||
| MF_AUTH_LOG_LEVEL | Service level (debug, info, warn, error) | error |
|
||||
| MF_AUTH_DB_HOST | Database host address | localhost |
|
||||
| MF_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| MF_AUTH_DB_USER | Database user | mainflux |
|
||||
| MF_AUTH_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_AUTH_DB | Name of the database used by the service | auth |
|
||||
| MF_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_AUTH_HTTP_PORT | Auth service HTTP port | 8180 |
|
||||
| MF_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| MF_AUTH_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_AUTH_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_AUTH_SECRET | String used for signing tokens | auth |
|
||||
| MF_AUTH_LOGIN_TOKEN_DURATION | The login token expiration period | 10h |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_KETO_READ_REMOTE_HOST | Keto Read Host | mainflux-keto |
|
||||
| MF_KETO_WRITE_REMOTE_HOST | Keto Write Host | mainflux-keto |
|
||||
| MF_KETO_READ_REMOTE_PORT | Keto Read Port | 4466 |
|
||||
| MF_KETO_WRITE_REMOTE_PORT | Keto Write Port | 4467 |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -95,7 +100,7 @@ make auth
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_AUTH_LOG_LEVEL=[Service log level] MF_AUTH_DB_HOST=[Database host address] MF_AUTH_DB_PORT=[Database host port] MF_AUTH_DB_USER=[Database user] MF_AUTH_DB_PASS=[Database password] MF_AUTH_DB=[Name of the database used by the service] MF_AUTH_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTH_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTH_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTH_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTH_HTTP_PORT=[Service HTTP port] MF_AUTH_GRPC_PORT=[Service gRPC port] MF_AUTH_SECRET=[String used for signing tokens] MF_AUTH_SERVER_CERT=[Path to server certificate] MF_AUTH_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] $GOBIN/mainflux-auth
|
||||
MF_AUTH_LOG_LEVEL=[Service log level] MF_AUTH_DB_HOST=[Database host address] MF_AUTH_DB_PORT=[Database host port] MF_AUTH_DB_USER=[Database user] MF_AUTH_DB_PASS=[Database password] MF_AUTH_DB=[Name of the database used by the service] MF_AUTH_DB_SSL_MODE=[SSL mode to connect to the database with] MF_AUTH_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_AUTH_DB_SSL_KEY=[Path to the PEM encoded key file] MF_AUTH_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_AUTH_HTTP_PORT=[Service HTTP port] MF_AUTH_GRPC_PORT=[Service gRPC port] MF_AUTH_SECRET=[String used for signing tokens] MF_AUTH_SERVER_CERT=[Path to server certificate] MF_AUTH_SERVER_KEY=[Path to server key] MF_JAEGER_URL=[Jaeger server URL] MF_AUTH_LOGIN_TOKEN_DURATION=[The login token expiration period] $GOBIN/mainflux-auth
|
||||
```
|
||||
|
||||
If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but password reset functionality will not work.
|
||||
@@ -103,6 +108,6 @@ If `MF_EMAIL_TEMPLATE` doesn't point to any file service will function but passw
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](openapi.yml).
|
||||
the [API documentation](https://api.mainflux.io/?urls.primaryName=auth-openapi.yml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
[doc]: https://docs.mainflux.io
|
||||
|
||||
+116
-8
@@ -23,12 +23,15 @@ const (
|
||||
var _ mainflux.AuthServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
assign endpoint.Endpoint
|
||||
members endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
issue endpoint.Endpoint
|
||||
identify endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
addPolicy endpoint.Endpoint
|
||||
deletePolicy endpoint.Endpoint
|
||||
listPolicies endpoint.Endpoint
|
||||
assign endpoint.Endpoint
|
||||
members endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
@@ -58,6 +61,30 @@ func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Du
|
||||
decodeAuthorizeResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
).Endpoint()),
|
||||
addPolicy: kitot.TraceClient(tracer, "add_policy")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"AddPolicy",
|
||||
encodeAddPolicyRequest,
|
||||
decodeAddPolicyResponse,
|
||||
mainflux.AddPolicyRes{},
|
||||
).Endpoint()),
|
||||
deletePolicy: kitot.TraceClient(tracer, "delete_policy")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"DeletePolicy",
|
||||
encodeDeletePolicyRequest,
|
||||
decodeDeletePolicyResponse,
|
||||
mainflux.DeletePolicyRes{},
|
||||
).Endpoint()),
|
||||
listPolicies: kitot.TraceClient(tracer, "list_policies")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"ListPolicies",
|
||||
encodeListPoliciesRequest,
|
||||
decodeListPoliciesResponse,
|
||||
mainflux.ListPoliciesRes{},
|
||||
).Endpoint()),
|
||||
assign: kitot.TraceClient(tracer, "assign")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
@@ -129,9 +156,9 @@ func (client grpcClient) Authorize(ctx context.Context, req *mainflux.AuthorizeR
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.authorize(ctx, authReq{Act: req.Act, Obj: req.Obj, Sub: req.Sub})
|
||||
res, err := client.authorize(ctx, authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.AuthorizeRes{Authorized: false}, err
|
||||
return &mainflux.AuthorizeRes{}, err
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
@@ -152,6 +179,87 @@ func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) AddPolicy(ctx context.Context, in *mainflux.AddPolicyReq, opts ...grpc.CallOption) (*mainflux.AddPolicyRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.addPolicy(ctx, policyReq{Act: in.GetAct(), Obj: in.GetObj(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.AddPolicyRes{}, err
|
||||
}
|
||||
|
||||
apr := res.(addPolicyRes)
|
||||
return &mainflux.AddPolicyRes{Authorized: apr.authorized}, err
|
||||
}
|
||||
|
||||
func decodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AddPolicyRes)
|
||||
return addPolicyRes{authorized: res.Authorized}, nil
|
||||
}
|
||||
|
||||
func encodeAddPolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(policyReq)
|
||||
return &mainflux.AddPolicyReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) DeletePolicy(ctx context.Context, in *mainflux.DeletePolicyReq, opts ...grpc.CallOption) (*mainflux.DeletePolicyRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.deletePolicy(ctx, policyReq{Act: in.GetAct(), Obj: in.GetObj(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.DeletePolicyRes{}, err
|
||||
}
|
||||
|
||||
dpr := res.(deletePolicyRes)
|
||||
return &mainflux.DeletePolicyRes{Deleted: dpr.deleted}, err
|
||||
}
|
||||
|
||||
func decodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.DeletePolicyRes)
|
||||
return deletePolicyRes{deleted: res.GetDeleted()}, nil
|
||||
}
|
||||
|
||||
func encodeDeletePolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(policyReq)
|
||||
return &mainflux.DeletePolicyReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) ListPolicies(ctx context.Context, in *mainflux.ListPoliciesReq, opts ...grpc.CallOption) (*mainflux.ListPoliciesRes, error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.listPolicies(ctx, listPoliciesReq{Obj: in.GetObj(), Act: in.GetAct(), Sub: in.GetSub()})
|
||||
if err != nil {
|
||||
return &mainflux.ListPoliciesRes{}, err
|
||||
}
|
||||
|
||||
lpr := res.(listPoliciesRes)
|
||||
return &mainflux.ListPoliciesRes{Policies: lpr.policies}, err
|
||||
}
|
||||
|
||||
func decodeListPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.ListPoliciesRes)
|
||||
return listPoliciesRes{policies: res.GetPolicies()}, nil
|
||||
}
|
||||
|
||||
func encodeListPoliciesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(listPoliciesReq)
|
||||
return &mainflux.ListPoliciesReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client grpcClient) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
@@ -62,17 +62,53 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
_, err := svc.Identify(ctx, req.token)
|
||||
err := svc.Authorize(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
return authorizeRes{authorized: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
authorized, err := svc.Authorize(ctx, req.token, req.Sub, req.Obj, req.Obj)
|
||||
if err != nil {
|
||||
return authorizeRes{}, err
|
||||
func addPolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return addPolicyRes{}, err
|
||||
}
|
||||
|
||||
return authorizeRes{authorized: authorized}, err
|
||||
err := svc.AddPolicy(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return addPolicyRes{}, err
|
||||
}
|
||||
return addPolicyRes{authorized: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
func deletePolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policyReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
|
||||
err := svc.DeletePolicy(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
return deletePolicyRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listPoliciesEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listPoliciesReq)
|
||||
|
||||
page, err := svc.ListPolicies(ctx, auth.PolicyReq{Subject: req.Sub, Object: req.Obj, Relation: req.Act})
|
||||
if err != nil {
|
||||
return deletePolicyRes{}, err
|
||||
}
|
||||
return listPoliciesRes{policies: page.Policies}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+215
-14
@@ -34,6 +34,10 @@ const (
|
||||
|
||||
numOfThings = 5
|
||||
numOfUsers = 5
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
var svc auth.Service
|
||||
@@ -42,9 +46,14 @@ func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: authoritiesObj, Relation: memberRelation})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
t := jwt.New(secret)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t)
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) {
|
||||
@@ -71,7 +80,7 @@ func TestIssue(t *testing.T) {
|
||||
desc: "issue for user with valid token",
|
||||
id: id,
|
||||
email: email,
|
||||
kind: auth.UserKey,
|
||||
kind: auth.LoginKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
@@ -100,11 +109,12 @@ func TestIssue(t *testing.T) {
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
kind: auth.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
email: "",
|
||||
kind: auth.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,7 +127,7 @@ func TestIssue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, recoverySecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
@@ -162,15 +172,15 @@ func TestIdentify(t *testing.T) {
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
idt: mainflux.UserIdentity{},
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
err: status.Error(codes.Unauthenticated, "unauthenticated access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user that doesn't exist",
|
||||
desc: "identify user with empty token",
|
||||
token: "",
|
||||
idt: mainflux.UserIdentity{},
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -185,8 +195,194 @@ func TestIdentify(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
ar mainflux.AuthorizeRes
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: authoritiesObj,
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AuthorizeRes{Authorized: true},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized relation",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: authoritiesObj,
|
||||
relation: "unauthorizedRelation",
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized object",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: "unauthorizedobject",
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized subject",
|
||||
token: loginSecret,
|
||||
subject: "unauthorizedSubject",
|
||||
object: authoritiesObj,
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with invalid ACL",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
ar: mainflux.AuthorizeRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ar, err := client.Authorize(context.Background(), &mainflux.AuthorizeReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.ar, *ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.ar, *ar))
|
||||
}
|
||||
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPolicy(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
groupAdminObj := "groupadmin"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
ar mainflux.AddPolicyRes
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "add groupadmin policy to user",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: groupAdminObj,
|
||||
relation: memberRelation,
|
||||
ar: mainflux.AddPolicyRes{Authorized: true},
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "add policy to user with invalid ACL",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
ar: mainflux.AddPolicyRes{Authorized: false},
|
||||
err: nil,
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
apr, err := client.AddPolicy(context.Background(), &mainflux.AddPolicyReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
if apr != nil {
|
||||
assert.Equal(t, tc.ar, *apr, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.ar, *apr))
|
||||
}
|
||||
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePolicy(t *testing.T) {
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
readRelation := "read"
|
||||
thingID := "thing"
|
||||
|
||||
apr, err := client.AddPolicy(context.Background(), &mainflux.AddPolicyReq{Sub: id, Obj: thingID, Act: readRelation})
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding read policy to user expected to succeed: %s", err))
|
||||
assert.True(t, apr.GetAuthorized(), fmt.Sprintf("Adding read policy expected to make user authorized, expected %v got %v", true, apr.GetAuthorized()))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
subject string
|
||||
object string
|
||||
relation string
|
||||
dpr *mainflux.DeletePolicyRes
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "delete valid policy",
|
||||
token: loginSecret,
|
||||
subject: id,
|
||||
object: thingID,
|
||||
relation: readRelation,
|
||||
dpr: &mainflux.DeletePolicyRes{Deleted: true},
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "delete invalid policy",
|
||||
token: loginSecret,
|
||||
subject: "",
|
||||
object: "",
|
||||
relation: "",
|
||||
dpr: &mainflux.DeletePolicyRes{Deleted: false},
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
dpr, err := client.DeletePolicy(context.Background(), &mainflux.DeletePolicyReq{Sub: tc.subject, Obj: tc.object, Act: tc.relation})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
assert.Equal(t, tc.dpr.GetDeleted(), dpr.GetDeleted(), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.dpr.GetDeleted(), dpr.GetDeleted()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMembers(t *testing.T) {
|
||||
_, token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
@@ -196,10 +392,13 @@ func TestMembers(t *testing.T) {
|
||||
|
||||
var things []string
|
||||
for i := 0; i < numOfThings; i++ {
|
||||
id, err := uuid.New().ID()
|
||||
thID, err := uuid.New().ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
|
||||
|
||||
things = append(things, id)
|
||||
err = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: thID, Relation: "owner"})
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding a policy expected to succeed: %s", err))
|
||||
|
||||
things = append(things, thID)
|
||||
}
|
||||
|
||||
var users []string
|
||||
@@ -212,6 +411,8 @@ func TestMembers(t *testing.T) {
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), token, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
|
||||
err = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: group.ID, Relation: "groupadmin"})
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding a policy expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, thingsType, things...)
|
||||
assert.Nil(t, err, fmt.Sprintf("Assign members to expected to succeed: %s", err))
|
||||
|
||||
+46
-22
@@ -5,6 +5,7 @@ package grpc
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type identityReq struct {
|
||||
@@ -14,12 +15,12 @@ type identityReq struct {
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.kind != auth.UserKey &&
|
||||
if req.kind != auth.LoginKey &&
|
||||
req.kind != auth.APIKey &&
|
||||
req.kind != auth.RecoveryKey {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -33,12 +34,12 @@ type issueReq struct {
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
if req.email == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrMissingEmail
|
||||
}
|
||||
if req.keyType != auth.UserKey &&
|
||||
if req.keyType != auth.LoginKey &&
|
||||
req.keyType != auth.APIKey &&
|
||||
req.keyType != auth.RecoveryKey {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrInvalidAuthKey
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -53,10 +54,10 @@ type assignReq struct {
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.groupID == "" || req.memberID == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -71,13 +72,13 @@ type membersReq struct {
|
||||
|
||||
func (req membersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.groupID == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.memberType == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingMemberType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -87,28 +88,51 @@ func (req membersReq) validate() error {
|
||||
// 2. object - an entity over which action will be executed
|
||||
// 3. action - type of action that will be executed (read/write)
|
||||
type authReq struct {
|
||||
token string
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
func (req authReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.Sub == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Obj == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Act == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingPolicyAct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type policyReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
func (req policyReq) validate() error {
|
||||
if req.Sub == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
|
||||
if req.Obj == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
if req.Act == "" {
|
||||
return apiutil.ErrMissingPolicyAct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listPoliciesReq struct {
|
||||
Sub string
|
||||
Obj string
|
||||
Act string
|
||||
}
|
||||
|
||||
@@ -15,6 +15,19 @@ type issueRes struct {
|
||||
type authorizeRes struct {
|
||||
authorized bool
|
||||
}
|
||||
|
||||
type addPolicyRes struct {
|
||||
authorized bool
|
||||
}
|
||||
|
||||
type deletePolicyRes struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
type listPoliciesRes struct {
|
||||
policies []string
|
||||
}
|
||||
|
||||
type membersRes struct {
|
||||
total uint64
|
||||
offset uint64
|
||||
|
||||
+97
-15
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -20,11 +21,14 @@ import (
|
||||
var _ mainflux.AuthServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
issue kitgrpc.Handler
|
||||
identify kitgrpc.Handler
|
||||
authorize kitgrpc.Handler
|
||||
assign kitgrpc.Handler
|
||||
members kitgrpc.Handler
|
||||
issue kitgrpc.Handler
|
||||
identify kitgrpc.Handler
|
||||
authorize kitgrpc.Handler
|
||||
addPolicy kitgrpc.Handler
|
||||
deletePolicy kitgrpc.Handler
|
||||
listPolicies kitgrpc.Handler
|
||||
assign kitgrpc.Handler
|
||||
members kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new AuthServiceServer instance.
|
||||
@@ -45,6 +49,21 @@ func NewServer(tracer opentracing.Tracer, svc auth.Service) mainflux.AuthService
|
||||
decodeAuthorizeRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
addPolicy: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "add_policy")(addPolicyEndpoint(svc)),
|
||||
decodeAddPolicyRequest,
|
||||
encodeAddPolicyResponse,
|
||||
),
|
||||
deletePolicy: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_policy")(deletePolicyEndpoint(svc)),
|
||||
decodeDeletePolicyRequest,
|
||||
encodeDeletePolicyResponse,
|
||||
),
|
||||
listPolicies: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "list_policies")(listPoliciesEndpoint(svc)),
|
||||
decodeListPoliciesRequest,
|
||||
encodeListPoliciesResponse,
|
||||
),
|
||||
assign: kitgrpc.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
@@ -74,14 +93,38 @@ func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*main
|
||||
return res.(*mainflux.UserIdentity), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Authorize(ctx context.Context, token *mainflux.AuthorizeReq) (*mainflux.AuthorizeRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, token)
|
||||
func (s *grpcServer) Authorize(ctx context.Context, req *mainflux.AuthorizeReq) (*mainflux.AuthorizeRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.AuthorizeRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) AddPolicy(ctx context.Context, req *mainflux.AddPolicyReq) (*mainflux.AddPolicyRes, error) {
|
||||
_, res, err := s.addPolicy.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.AddPolicyRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) DeletePolicy(ctx context.Context, req *mainflux.DeletePolicyReq) (*mainflux.DeletePolicyRes, error) {
|
||||
_, res, err := s.deletePolicy.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.DeletePolicyRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) ListPolicies(ctx context.Context, req *mainflux.ListPoliciesReq) (*mainflux.ListPoliciesRes, error) {
|
||||
_, res, err := s.listPolicies.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.ListPoliciesRes), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Assign(ctx context.Context, token *mainflux.Assignment) (*empty.Empty, error) {
|
||||
_, res, err := s.assign.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
@@ -120,12 +163,22 @@ func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AuthorizeReq)
|
||||
return authReq{Act: req.Act, Obj: req.Obj, Sub: req.Sub}, nil
|
||||
return authReq{Act: req.GetAct(), Obj: req.GetObj(), Sub: req.GetSub()}, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AuthorizeRes)
|
||||
return res, nil
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &mainflux.AuthorizeRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAddPolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AddPolicyReq)
|
||||
return policyReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(addPolicyRes)
|
||||
return &mainflux.AddPolicyRes{Authorized: res.authorized}, nil
|
||||
}
|
||||
|
||||
func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
@@ -133,6 +186,26 @@ func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, e
|
||||
return assignReq{token: req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func decodeDeletePolicyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.DeletePolicyReq)
|
||||
return policyReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(deletePolicyRes)
|
||||
return &mainflux.DeletePolicyRes{Deleted: res.deleted}, nil
|
||||
}
|
||||
|
||||
func decodeListPoliciesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.ListPoliciesReq)
|
||||
return listPoliciesReq{Sub: req.GetSub(), Obj: req.GetObj(), Act: req.GetAct()}, nil
|
||||
}
|
||||
|
||||
func encodeListPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(listPoliciesRes)
|
||||
return &mainflux.ListPoliciesRes{Policies: res.policies}, nil
|
||||
}
|
||||
|
||||
func decodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.MembersReq)
|
||||
return membersReq{
|
||||
@@ -164,12 +237,21 @@ func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, auth.ErrMalformedEntity):
|
||||
return status.Error(codes.InvalidArgument, "received invalid token request")
|
||||
case errors.Contains(err, auth.ErrUnauthorizedAccess):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, auth.ErrKeyExpired):
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrInvalidAuthKey,
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMissingPolicyAct:
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
errors.Contains(err, auth.ErrKeyExpired),
|
||||
err == apiutil.ErrMissingEmail,
|
||||
err == apiutil.ErrBearerToken:
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
|
||||
@@ -137,11 +137,21 @@ func listMemberships(svc auth.Service) endpoint.Endpoint {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
return buildGroupsResponseTree(page), nil
|
||||
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
|
||||
}
|
||||
|
||||
return buildGroupsResponse(page), nil
|
||||
if err := svc.AssignGroupAccessRights(ctx, req.token, req.ThingGroupID, req.userGroupID); err != nil {
|
||||
return shareGroupRes{}, err
|
||||
}
|
||||
return shareGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +250,7 @@ func listMembersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
return buildUsersResponse(page), nil
|
||||
return buildUsersResponse(page, req.groupType), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +350,7 @@ func buildGroupsResponse(gp auth.GroupPage) groupPageRes {
|
||||
return res
|
||||
}
|
||||
|
||||
func buildUsersResponse(mp auth.MemberPage) memberPageRes {
|
||||
func buildUsersResponse(mp auth.MemberPage, groupType string) memberPageRes {
|
||||
res := memberPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: mp.Total,
|
||||
@@ -348,11 +358,12 @@ func buildUsersResponse(mp auth.MemberPage) memberPageRes {
|
||||
Limit: mp.Limit,
|
||||
Name: mp.Name,
|
||||
},
|
||||
Members: []interface{}{},
|
||||
Type: groupType,
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
for _, m := range mp.Members {
|
||||
res.Members = append(res.Members, m)
|
||||
res.Members = append(res.Members, m.ID)
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package groups_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
email = "user@example.com"
|
||||
secret = "secret"
|
||||
id = "testID"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
keys := mocks.NewKeyRepository()
|
||||
groups := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
policies := mocks.NewKetoMock(map[string][]mocks.MockSubjectSet{})
|
||||
return auth.New(keys, groups, idProvider, t, policies, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestShareGroupAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
type shareGroupAccessReq struct {
|
||||
token string
|
||||
userGroupID string
|
||||
ThingGroupID string `json:"thing_group_id"`
|
||||
}
|
||||
data := shareGroupAccessReq{token: apiToken, userGroupID: "ug", ThingGroupID: "tg"}
|
||||
invalidData := shareGroupAccessReq{token: apiToken, userGroupID: "ug", ThingGroupID: ""}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
userGroupID string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "share a user group with thing group",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with invalid thing group",
|
||||
req: toJSON(invalidData),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share an invalid user group with thing group",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share an invalid user group with invalid thing group",
|
||||
req: toJSON(invalidData),
|
||||
contentType: contentType,
|
||||
auth: apiToken,
|
||||
userGroupID: "",
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with thing group with invalid content type",
|
||||
req: toJSON(data),
|
||||
contentType: "",
|
||||
auth: apiToken,
|
||||
userGroupID: "ug",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "share a user group with thing group with invalid token",
|
||||
req: toJSON(data),
|
||||
contentType: contentType,
|
||||
auth: "token",
|
||||
userGroupID: "ug",
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: ts.Client(),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/groups/%s/share", ts.URL, tc.userGroupID),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package groups
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type createGroupReq struct {
|
||||
@@ -15,10 +15,10 @@ type createGroupReq struct {
|
||||
|
||||
func (req createGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if len(req.Name) > maxNameSize || req.Name == "" {
|
||||
return errors.Wrap(auth.ErrMalformedEntity, auth.ErrBadGroupName)
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -34,11 +34,11 @@ type updateGroupReq struct {
|
||||
|
||||
func (req updateGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -56,11 +56,11 @@ type listGroupsReq struct {
|
||||
|
||||
func (req listGroupsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.level > auth.MaxLevel || req.level < auth.MinLevel {
|
||||
return auth.ErrMaxLevelExceeded
|
||||
return apiutil.ErrMaxLevelExceeded
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -78,11 +78,11 @@ type listMembersReq struct {
|
||||
|
||||
func (req listMembersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -93,17 +93,16 @@ type listMembershipsReq struct {
|
||||
id string
|
||||
offset uint64
|
||||
limit uint64
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembershipsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -118,11 +117,37 @@ type assignReq struct {
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Type == "" || req.groupID == "" || len(req.Members) == 0 {
|
||||
return auth.ErrMalformedEntity
|
||||
if req.Type == "" {
|
||||
return apiutil.ErrMissingMemberType
|
||||
}
|
||||
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.Members) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type shareGroupAccessReq struct {
|
||||
token string
|
||||
userGroupID string
|
||||
ThingGroupID string `json:"thing_group_id"`
|
||||
}
|
||||
|
||||
func (req shareGroupAccessReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ThingGroupID == "" || req.userGroupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -134,11 +159,15 @@ type unassignReq struct {
|
||||
|
||||
func (req unassignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.groupID == "" || len(req.Members) == 0 {
|
||||
return auth.ErrMalformedEntity
|
||||
if req.groupID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if len(req.Members) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -151,11 +180,11 @@ type groupReq struct {
|
||||
|
||||
func (req groupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -18,7 +18,8 @@ var (
|
||||
|
||||
type memberPageRes struct {
|
||||
pageRes
|
||||
Members []interface{}
|
||||
Type string `json:"type"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (res memberPageRes) Code() int {
|
||||
@@ -33,6 +34,21 @@ func (res memberPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type shareGroupRes struct {
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res shareGroupRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type viewGroupRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -156,7 +172,3 @@ func (res unassignRes) Headers() map[string]string {
|
||||
func (res unassignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package groups
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -12,16 +11,12 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/httputil"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxNameSize = 254
|
||||
@@ -37,9 +32,9 @@ const (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
mux.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_group")(createGroupEndpoint(svc)),
|
||||
@@ -69,6 +64,13 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:subjectGroupID/share", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "share_group_access")(shareGroupAccessEndpoint(svc)),
|
||||
decodeShareGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(listGroupsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
@@ -119,31 +121,42 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeShareGroupRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := shareGroupAccessReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
userGroupID: bone.GetValue(r, "subjectGroupID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
l, err := httputil.ReadUintQuery(r, levelKey, defLevel)
|
||||
l, err := apiutil.ReadUintQuery(r, levelKey, defLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := httputil.ReadBoolQuery(r, treeKey, false)
|
||||
t, err := apiutil.ReadBoolQuery(r, treeKey, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listGroupsReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
level: l,
|
||||
metadata: m,
|
||||
tree: t,
|
||||
@@ -153,37 +166,33 @@ func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
}
|
||||
|
||||
func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := httputil.ReadBoolQuery(r, treeKey, false)
|
||||
tree, err := apiutil.ReadBoolQuery(r, treeKey, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := httputil.ReadStringQuery(r, groupType, "")
|
||||
t, err := apiutil.ReadStringQuery(r, groupType, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembersReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
groupType: t,
|
||||
offset: o,
|
||||
@@ -195,37 +204,27 @@ func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{},
|
||||
}
|
||||
|
||||
func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := httputil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := httputil.ReadBoolQuery(r, treeKey, false)
|
||||
m, err := apiutil.ReadMetadataQuery(r, metadataKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembershipsReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "memberID"),
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
tree: tree,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -233,36 +232,36 @@ func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface
|
||||
|
||||
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req createGroupReq
|
||||
req := createGroupReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrFailedDecode, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
req.token = r.Header.Get("Authorization")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req updateGroupReq
|
||||
req := updateGroupReq{
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrFailedDecode, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
req.id = bone.GetValue(r, "groupID")
|
||||
req.token = r.Header.Get("Authorization")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := groupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
@@ -271,12 +270,12 @@ func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
|
||||
func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := assignReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -285,13 +284,13 @@ func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error
|
||||
func decodeUnassignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := unassignReq{
|
||||
assignReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -317,29 +316,38 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, auth.ErrMalformedEntity):
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrEmptyList,
|
||||
err == apiutil.ErrMissingMemberType,
|
||||
err == apiutil.ErrNameSize:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, auth.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrNotFound):
|
||||
case errors.Contains(err, errors.ErrAuthentication):
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, auth.ErrConflict):
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
|
||||
case errors.Contains(err, errors.ErrCreateEntity),
|
||||
errors.Contains(err, errors.ErrUpdateEntity),
|
||||
errors.Contains(err, errors.ErrViewEntity),
|
||||
errors.Contains(err, errors.ErrRemoveEntity):
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,19 @@ import (
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type issueRequest struct {
|
||||
@@ -50,7 +53,7 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
@@ -65,11 +68,17 @@ func newService() auth.Service {
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return auth.New(repo, groupRepo, idProvider, t)
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: "authorities", Relation: "member"})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New())
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -80,14 +89,14 @@ func toJSON(data interface{}) string {
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
uk := issueRequest{Type: auth.UserKey}
|
||||
lk := issueRequest{Type: auth.LoginKey}
|
||||
ak := issueRequest{Type: auth.APIKey, Duration: time.Hour}
|
||||
rk := issueRequest{Type: auth.RecoveryKey}
|
||||
|
||||
@@ -99,11 +108,11 @@ func TestIssue(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "issue user key",
|
||||
req: toJSON(uk),
|
||||
desc: "issue login key with empty token",
|
||||
req: toJSON(lk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusCreated,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
@@ -117,11 +126,11 @@ func TestIssue(t *testing.T) {
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue user key wrong content type",
|
||||
req: toJSON(uk),
|
||||
desc: "issue login key wrong content type",
|
||||
req: toJSON(lk),
|
||||
ct: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
@@ -134,38 +143,38 @@ func TestIssue(t *testing.T) {
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key unauthorized",
|
||||
desc: "issue key with an invalid token",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key with empty token",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusBadRequest,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid request",
|
||||
req: "{",
|
||||
ct: contentType,
|
||||
token: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON",
|
||||
req: "{invalid}",
|
||||
ct: contentType,
|
||||
token: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON content",
|
||||
req: `{"Type":{"key":"value"}}`,
|
||||
ct: contentType,
|
||||
token: "",
|
||||
token: loginSecret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
@@ -187,7 +196,7 @@ func TestIssue(t *testing.T) {
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
|
||||
|
||||
@@ -217,10 +226,10 @@ func TestRetrieve(t *testing.T) {
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key unauthorized",
|
||||
desc: "retrieve a key with an invalid token",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -239,12 +248,12 @@ func TestRetrieve(t *testing.T) {
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), IssuerID: id, Subject: email}
|
||||
|
||||
k, _, err := svc.Issue(context.Background(), loginSecret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
@@ -269,10 +278,10 @@ func TestRevoke(t *testing.T) {
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a key unauthorized",
|
||||
desc: "revoke key with invalid token",
|
||||
id: k.ID,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden},
|
||||
status: http.StatusUnauthorized},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
type issueKeyReq struct {
|
||||
@@ -17,12 +18,16 @@ type issueKeyReq struct {
|
||||
|
||||
// It is not possible to issue Reset key using HTTP API.
|
||||
func (req issueKeyReq) validate() error {
|
||||
if req.Type == auth.UserKey {
|
||||
return nil
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.token == "" || (req.Type != auth.APIKey) {
|
||||
return auth.ErrMalformedEntity
|
||||
|
||||
if req.Type != auth.LoginKey &&
|
||||
req.Type != auth.RecoveryKey &&
|
||||
req.Type != auth.APIKey {
|
||||
return apiutil.ErrInvalidAPIKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,8 +37,12 @@ type keyReq struct {
|
||||
}
|
||||
|
||||
func (req keyReq) validate() error {
|
||||
if req.token == "" || req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,7 +69,3 @@ func (res revokeKeyRes) Headers() map[string]string {
|
||||
func (res revokeKeyRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package keys
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -15,17 +14,18 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
var errUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
mux.Post("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
@@ -53,13 +53,12 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo
|
||||
|
||||
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
req := issueKeyReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := issueKeyReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -67,7 +66,7 @@ func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
@@ -93,27 +92,26 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, auth.ErrMalformedEntity):
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrInvalidAPIKey:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, auth.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrNotFound):
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, auth.ErrConflict):
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func createPolicyEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policiesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return createPolicyRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.AddPolicies(ctx, req.token, req.Object, req.SubjectIDs, req.Policies); err != nil {
|
||||
return createPolicyRes{}, err
|
||||
}
|
||||
|
||||
return createPolicyRes{created: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deletePoliciesEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(policiesReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deletePoliciesRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.DeletePolicies(ctx, req.token, req.Object, req.SubjectIDs, req.Policies); err != nil {
|
||||
return deletePoliciesRes{}, err
|
||||
}
|
||||
|
||||
return deletePoliciesRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package policies_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
httpapi "github.com/mainflux/mainflux/auth/api/http"
|
||||
"github.com/mainflux/mainflux/auth/jwt"
|
||||
"github.com/mainflux/mainflux/auth/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = uuid.Prefix + "-000000000001"
|
||||
email = "user@example.com"
|
||||
unauthzID = uuid.Prefix + "-000000000002"
|
||||
unauthzEmail = "unauthz@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
|
||||
req.Header.Set("Referer", "http://localhost")
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: "authorities", Relation: "member"})
|
||||
mockAuthzDB[unauthzID] = append(mockAuthzDB[unauthzID], mocks.MockSubjectSet{Object: "users", Relation: "member"})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(svc, mocktracer.New(), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
type addPolicyRequest struct {
|
||||
SubjectIDs []string `json:"subjects"`
|
||||
Policies []string `json:"policies"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
func TestAddPolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, userLoginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: unauthzID, Subject: unauthzEmail})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing unauthorized user's key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
valid := addPolicyRequest{Object: "obj", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
multipleValid := addPolicyRequest{Object: "obj", Policies: []string{"write", "delete"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidObject := addPolicyRequest{Object: "", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidPolicies := addPolicyRequest{Object: "obj", Policies: []string{"read", "invalid"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidSubjects := addPolicyRequest{Object: "obj", Policies: []string{"read", "access"}, SubjectIDs: []string{"", "user2"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ct string
|
||||
status int
|
||||
req string
|
||||
}{
|
||||
{
|
||||
desc: "Add policies with authorized access",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusCreated,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add multiple policies to multiple user",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusCreated,
|
||||
req: toJSON(multipleValid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with unauthorized access",
|
||||
token: userLoginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusForbidden,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid token",
|
||||
token: "invalid",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty token",
|
||||
token: "",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid content type",
|
||||
token: loginSecret,
|
||||
ct: "text/html",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty content type",
|
||||
token: loginSecret,
|
||||
ct: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(valid),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid object field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidObject),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid policies field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidPolicies),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with invalid subjects field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidSubjects),
|
||||
},
|
||||
{
|
||||
desc: "Add policies with empty request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/policies", ts.URL),
|
||||
contentType: tc.ct,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
_, userLoginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: unauthzID, Subject: unauthzEmail})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing unauthorized user's key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
policies := addPolicyRequest{Object: "obj", Policies: []string{"read", "write", "delete"}, SubjectIDs: []string{"user1", "user2", "user3"}}
|
||||
err = svc.AddPolicies(context.Background(), loginSecret, policies.Object, policies.SubjectIDs, policies.Policies)
|
||||
assert.Nil(t, err, fmt.Sprintf("Adding policies expected to succeed: %s", err))
|
||||
|
||||
validSingleDeleteReq := addPolicyRequest{Object: "obj", Policies: []string{"read"}, SubjectIDs: []string{"user1"}}
|
||||
validMultipleDeleteReq := addPolicyRequest{Object: "obj", Policies: []string{"write", "delete"}, SubjectIDs: []string{"user2", "user3"}}
|
||||
invalidObject := addPolicyRequest{Object: "", Policies: []string{"read"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidPolicies := addPolicyRequest{Object: "obj", Policies: []string{"read", "invalid"}, SubjectIDs: []string{"user1", "user2"}}
|
||||
invalidSubjects := addPolicyRequest{Object: "obj", Policies: []string{"read", "access"}, SubjectIDs: []string{"", "user2"}}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
ct string
|
||||
req string
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "Delete policies with unauthorized access",
|
||||
token: userLoginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusForbidden,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid token",
|
||||
token: "invalid",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty token",
|
||||
token: "",
|
||||
ct: contentType,
|
||||
status: http.StatusUnauthorized,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with authorized access",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusNoContent,
|
||||
req: toJSON(validSingleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete multiple policies to multiple user",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusNoContent,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid content type",
|
||||
token: loginSecret,
|
||||
ct: "text/html",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty content type",
|
||||
token: loginSecret,
|
||||
ct: "",
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
req: toJSON(validMultipleDeleteReq),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid object field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidObject),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid policies field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidPolicies),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with invalid subjects field in request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: toJSON(invalidSubjects),
|
||||
},
|
||||
{
|
||||
desc: "Delete policies with empty request body",
|
||||
token: loginSecret,
|
||||
ct: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
req: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/policies", ts.URL),
|
||||
contentType: tc.ct,
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package policies
|
||||
|
||||
import "github.com/mainflux/mainflux/internal/apiutil"
|
||||
|
||||
// Action represents an enum for the policies used in the Mainflux.
|
||||
type Action int
|
||||
|
||||
const (
|
||||
Create Action = iota
|
||||
Read
|
||||
Write
|
||||
Delete
|
||||
Access
|
||||
Member
|
||||
Unknown
|
||||
)
|
||||
|
||||
var actions = map[string]Action{
|
||||
"create": Create,
|
||||
"read": Read,
|
||||
"write": Write,
|
||||
"delete": Delete,
|
||||
"access": Access,
|
||||
"member": Member,
|
||||
}
|
||||
|
||||
type policiesReq struct {
|
||||
token string
|
||||
SubjectIDs []string `json:"subjects"`
|
||||
Policies []string `json:"policies"`
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
func (req policiesReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if len(req.SubjectIDs) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
if len(req.Policies) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
if req.Object == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
|
||||
for _, policy := range req.Policies {
|
||||
if _, ok := actions[policy]; !ok {
|
||||
return apiutil.ErrMalformedPolicy
|
||||
}
|
||||
}
|
||||
|
||||
for _, subID := range req.SubjectIDs {
|
||||
if subID == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package policies
|
||||
|
||||
import "net/http"
|
||||
|
||||
type createPolicyRes struct {
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res createPolicyRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type deletePoliciesRes struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Code() int {
|
||||
if res.deleted {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deletePoliciesRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer, logger logger.Logger) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
|
||||
mux.Post("/policies", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_policy_bulk")(createPolicyEndpoint(svc)),
|
||||
decodePoliciesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/policies", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_policies")(deletePoliciesEndpoint(svc)),
|
||||
decodePoliciesRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodePoliciesRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := policiesReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrEmptyList,
|
||||
err == apiutil.ErrMissingPolicyObj,
|
||||
err == apiutil.ErrMissingPolicySub,
|
||||
err == apiutil.ErrMalformedPolicy:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,19 @@ import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/api/http/groups"
|
||||
"github.com/mainflux/mainflux/auth/api/http/keys"
|
||||
"github.com/mainflux/mainflux/auth/api/http/policies"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func MakeHandler(svc auth.Service, tracer opentracing.Tracer) http.Handler {
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, tracer opentracing.Tracer, logger logger.Logger) http.Handler {
|
||||
mux := bone.New()
|
||||
mux = keys.MakeHandler(svc, mux, tracer)
|
||||
mux = groups.MakeHandler(svc, mux, tracer)
|
||||
mux.GetFunc("/version", mainflux.Version("auth"))
|
||||
mux = keys.MakeHandler(svc, mux, tracer, logger)
|
||||
mux = groups.MakeHandler(svc, mux, tracer, logger)
|
||||
mux = policies.MakeHandler(svc, mux, tracer, logger)
|
||||
mux.GetFunc("/health", mainflux.Health("auth"))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
return mux
|
||||
}
|
||||
|
||||
+77
-3
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
@@ -26,6 +26,19 @@ func LoggingMiddleware(svc auth.Service, logger log.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListPolicies(ctx context.Context, pr auth.PolicyReq) (p auth.PolicyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_policies took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListPolicies(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Issue(ctx context.Context, token string, newKey auth.Key) (key auth.Key, secret string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
d := "infinite duration"
|
||||
@@ -82,7 +95,7 @@ func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id auth.
|
||||
return lm.svc.Identify(ctx, key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, token, sub, obj, act string) (auth bool, err error) {
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method authorize took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -91,8 +104,56 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, token, sub, obj, act
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.Authorize(ctx, pr)
|
||||
}
|
||||
|
||||
return lm.svc.Authorize(ctx, token, sub, obj, act)
|
||||
func (lm *loggingMiddleware) AddPolicy(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add_policy took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_policy_bulk took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AddPolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeletePolicy(ctx context.Context, pr auth.PolicyReq) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method delete_policy took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method delete_policies took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
return lm.svc.DeletePolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (g auth.Group, err error) {
|
||||
@@ -237,3 +298,16 @@ func (lm *loggingMiddleware) Unassign(ctx context.Context, token string, groupID
|
||||
|
||||
return lm.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method share_group_access took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AssignGroupAccessRights(ctx, token, thingGroupID, userGroupID)
|
||||
}
|
||||
|
||||
+54
-2
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
@@ -28,6 +30,15 @@ func MetricsMiddleware(svc auth.Service, counter metrics.Counter, latency metric
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListPolicies(ctx context.Context, pr auth.PolicyReq) (p auth.PolicyPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_policies").Add(1)
|
||||
ms.latency.With("method", "list_policies").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListPolicies(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.Key) (auth.Key, string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_key").Add(1)
|
||||
@@ -64,13 +75,45 @@ func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.I
|
||||
return ms.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, token, sub, obj, act string) (auth bool, err error) {
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "authorize").Add(1)
|
||||
ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.Authorize(ctx, pr)
|
||||
}
|
||||
|
||||
return ms.svc.Authorize(ctx, token, sub, obj, act)
|
||||
func (ms *metricsMiddleware) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_policy").Add(1)
|
||||
ms.latency.With("method", "add_policy").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_policy_bulk").Add(1)
|
||||
ms.latency.With("method", "create_policy_bulk").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AddPolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "delete_policy").Add(1)
|
||||
ms.latency.With("method", "delete_policy").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "delete_policies").Add(1)
|
||||
ms.latency.With("method", "delete_policies").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.DeletePolicies(ctx, token, object, subjectIDs, relations)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
@@ -168,3 +211,12 @@ func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID string
|
||||
|
||||
return ms.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "share_group_access").Add(1)
|
||||
ms.latency.With("method", "share_group_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AssignGroupAccessRights(ctx, token, thingGroupID, userGroupID)
|
||||
}
|
||||
|
||||
+20
-35
@@ -6,46 +6,20 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const MaxLevel = uint64(5)
|
||||
const MinLevel = uint64(1)
|
||||
const (
|
||||
// MaxLevel represents the maximum group hierarchy level.
|
||||
MaxLevel = uint64(5)
|
||||
// MinLevel represents the minimum group hierarchy level.
|
||||
MinLevel = uint64(1)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMaxLevelExceeded malformed entity.
|
||||
ErrMaxLevelExceeded = errors.New("level must be less than or equal 5")
|
||||
|
||||
// ErrBadGroupName malformed entity.
|
||||
ErrBadGroupName = errors.New("incorrect group name")
|
||||
|
||||
// ErrGroupConflict group conflict.
|
||||
ErrGroupConflict = errors.New("group already exists")
|
||||
|
||||
// ErrCreateGroup indicates failure to create group.
|
||||
ErrCreateGroup = errors.New("failed to create group")
|
||||
|
||||
// ErrFetchGroups indicates failure to fetch groups.
|
||||
ErrFetchGroups = errors.New("failed to fetch groups")
|
||||
|
||||
// ErrUpdateGroup indicates failure to update group.
|
||||
ErrUpdateGroup = errors.New("failed to update group")
|
||||
|
||||
// ErrDeleteGroup indicates failure to delete group.
|
||||
ErrDeleteGroup = errors.New("failed to delete group")
|
||||
|
||||
// ErrGroupNotFound indicates failure to find group.
|
||||
ErrGroupNotFound = errors.New("failed to find group")
|
||||
|
||||
// ErrAssignToGroup indicates failure to assign member to a group.
|
||||
ErrAssignToGroup = errors.New("failed to assign member to a group")
|
||||
|
||||
// ErrUnassignFromGroup indicates failure to unassign member from a group.
|
||||
ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
|
||||
|
||||
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
|
||||
ErrUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// ErrFailedDecode indicates failed to decode request body
|
||||
ErrFailedDecode = errors.New("failed to decode request body")
|
||||
|
||||
// ErrMissingParent indicates that parent can't be found
|
||||
ErrMissingParent = errors.New("failed to retrieve parent")
|
||||
|
||||
@@ -54,18 +28,18 @@ var (
|
||||
|
||||
// ErrMemberAlreadyAssigned indicates that members is already assigned.
|
||||
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
|
||||
|
||||
// ErrSelectEntity indicates error while reading entity from database
|
||||
ErrSelectEntity = errors.New("select entity from db error")
|
||||
)
|
||||
|
||||
// GroupMetadata defines the Metadata type.
|
||||
type GroupMetadata map[string]interface{}
|
||||
|
||||
// Member represents the member information.
|
||||
type Member struct {
|
||||
ID string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Group represents the group information.
|
||||
type Group struct {
|
||||
ID string
|
||||
OwnerID string
|
||||
@@ -85,6 +59,7 @@ type Group struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PageMetadata contains page metadata that helps navigation.
|
||||
type PageMetadata struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
@@ -96,16 +71,22 @@ type PageMetadata struct {
|
||||
Metadata GroupMetadata
|
||||
}
|
||||
|
||||
// GroupPage contains page related metadata as well as list of groups that
|
||||
// belong to this page.
|
||||
type GroupPage struct {
|
||||
PageMetadata
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
// MemberPage contains page related metadata as well as list of members that
|
||||
// belong to this page.
|
||||
type MemberPage struct {
|
||||
PageMetadata
|
||||
Members []Member
|
||||
}
|
||||
|
||||
// GroupService specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type GroupService interface {
|
||||
// CreateGroup creates new group.
|
||||
CreateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
@@ -139,8 +120,12 @@ type GroupService interface {
|
||||
|
||||
// Unassign removes member with memberID from group identified by groupID.
|
||||
Unassign(ctx context.Context, token, groupID string, memberIDs ...string) error
|
||||
|
||||
// AssignGroupAccessRights adds access rights on thing groups to user group.
|
||||
AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error
|
||||
}
|
||||
|
||||
// GroupRepository specifies a group persistence API.
|
||||
type GroupRepository interface {
|
||||
// Save group
|
||||
Save(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
@@ -21,7 +21,7 @@ func key() auth.Key {
|
||||
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
|
||||
return auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.UserKey,
|
||||
Type: auth.LoginKey,
|
||||
Subject: "user@email.com",
|
||||
IssuerID: "",
|
||||
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
|
||||
@@ -83,7 +83,7 @@ func TestParse(t *testing.T) {
|
||||
desc: "parse ivalid key",
|
||||
key: auth.Key{},
|
||||
token: "invalid",
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "parse expired key",
|
||||
|
||||
@@ -6,7 +6,7 @@ package jwt
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
@@ -21,7 +21,7 @@ type claims struct {
|
||||
|
||||
func (c claims) Valid() error {
|
||||
if c.Type == nil || *c.Type > auth.APIKey || c.Issuer != issuerName {
|
||||
return auth.ErrMalformedEntity
|
||||
return errors.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return c.StandardClaims.Valid()
|
||||
@@ -62,7 +62,7 @@ func (svc tokenizer) Parse(token string) (auth.Key, error) {
|
||||
c := claims{}
|
||||
_, err := jwt.ParseWithClaims(token, &c, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, auth.ErrUnauthorizedAccess
|
||||
return nil, errors.ErrAuthentication
|
||||
}
|
||||
return []byte(svc.secret), nil
|
||||
})
|
||||
@@ -75,7 +75,7 @@ func (svc tokenizer) Parse(token string) (auth.Key, error) {
|
||||
}
|
||||
return auth.Key{}, errors.Wrap(auth.ErrKeyExpired, err)
|
||||
}
|
||||
return auth.Key{}, errors.Wrap(auth.ErrUnauthorizedAccess, err)
|
||||
return auth.Key{}, errors.Wrap(errors.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
return c.toKey(), nil
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package keto contains PolicyAgent implementation using Keto.
|
||||
package keto
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
subjectSetRegex = "^.{1,}:.{1,}#.{1,}$" // expected subject set structure is <namespace>:<object>#<relation>
|
||||
ketoNamespace = "members"
|
||||
)
|
||||
|
||||
type policyAgent struct {
|
||||
writer acl.WriteServiceClient
|
||||
checker acl.CheckServiceClient
|
||||
reader acl.ReadServiceClient
|
||||
}
|
||||
|
||||
// NewPolicyAgent returns a gRPC communication functionalities
|
||||
// to communicate with ORY Keto.
|
||||
func NewPolicyAgent(checker acl.CheckServiceClient, writer acl.WriteServiceClient, reader acl.ReadServiceClient) auth.PolicyAgent {
|
||||
return policyAgent{checker: checker, writer: writer, reader: reader}
|
||||
}
|
||||
|
||||
func (pa policyAgent) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
res, err := pa.checker.Check(context.Background(), &acl.CheckRequest{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: getSubject(pr),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errors.ErrAuthorization)
|
||||
}
|
||||
if !res.GetAllowed() {
|
||||
return errors.ErrAuthorization
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa policyAgent) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
var ss *acl.Subject
|
||||
switch isSubjectSet(pr.Subject) {
|
||||
case true:
|
||||
namespace, object, relation := parseSubjectSet(pr.Subject)
|
||||
ss = &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{Namespace: namespace, Object: object, Relation: relation}},
|
||||
}
|
||||
default:
|
||||
ss = &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
trt := pa.writer.TransactRelationTuples
|
||||
_, err := trt(context.Background(), &acl.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*acl.RelationTupleDelta{
|
||||
{
|
||||
Action: acl.RelationTupleDelta_INSERT,
|
||||
RelationTuple: &acl.RelationTuple{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: ss,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (pa policyAgent) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
trt := pa.writer.TransactRelationTuples
|
||||
_, err := trt(context.Background(), &acl.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*acl.RelationTupleDelta{
|
||||
{
|
||||
Action: acl.RelationTupleDelta_DELETE,
|
||||
RelationTuple: &acl.RelationTuple{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
Subject: &acl.Subject{Ref: &acl.Subject_Id{
|
||||
Id: pr.Subject,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (pa policyAgent) RetrievePolicies(ctx context.Context, pr auth.PolicyReq) ([]*acl.RelationTuple, error) {
|
||||
var ss *acl.Subject
|
||||
switch isSubjectSet(pr.Subject) {
|
||||
case true:
|
||||
namespace, object, relation := parseSubjectSet(pr.Subject)
|
||||
ss = &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{Namespace: namespace, Object: object, Relation: relation}},
|
||||
}
|
||||
default:
|
||||
ss = &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
res, err := pa.reader.ListRelationTuples(ctx, &acl.ListRelationTuplesRequest{
|
||||
Query: &acl.ListRelationTuplesRequest_Query{
|
||||
Namespace: ketoNamespace,
|
||||
Relation: pr.Relation,
|
||||
Subject: ss,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return []*acl.RelationTuple{}, err
|
||||
}
|
||||
|
||||
tuple := res.GetRelationTuples()
|
||||
for res.NextPageToken != "" {
|
||||
tuple = append(tuple, res.GetRelationTuples()...)
|
||||
}
|
||||
|
||||
return tuple, nil
|
||||
}
|
||||
|
||||
// getSubject returns a 'subject' field for ACL(access control lists).
|
||||
// If the given PolicyReq argument contains a subject as subject set,
|
||||
// it returns subject set; otherwise, it returns a subject.
|
||||
func getSubject(pr auth.PolicyReq) *acl.Subject {
|
||||
if isSubjectSet(pr.Subject) {
|
||||
return &acl.Subject{
|
||||
Ref: &acl.Subject_Set{Set: &acl.SubjectSet{
|
||||
Namespace: ketoNamespace,
|
||||
Object: pr.Object,
|
||||
Relation: pr.Relation,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
return &acl.Subject{Ref: &acl.Subject_Id{Id: pr.Subject}}
|
||||
}
|
||||
|
||||
// isSubjectSet returns true when given subject is subject set.
|
||||
// Otherwise, it returns false.
|
||||
func isSubjectSet(subject string) bool {
|
||||
r, err := regexp.Compile(subjectSetRegex)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return r.MatchString(subject)
|
||||
}
|
||||
|
||||
func parseSubjectSet(subjectSet string) (namespace, object, relation string) {
|
||||
r := strings.Split(subjectSet, ":")
|
||||
if len(r) != 2 {
|
||||
return
|
||||
}
|
||||
namespace = r[0]
|
||||
|
||||
r = strings.Split(r[1], "#")
|
||||
if len(r) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
object = r[0]
|
||||
relation = r[1]
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package keto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSubjectSet(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
subjectSet string
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
desc: "check valid subject set",
|
||||
subjectSet: "namespace:object#relation",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing namespace field",
|
||||
subjectSet: ":object#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing object field",
|
||||
subjectSet: "namespace:#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing relation field",
|
||||
subjectSet: "namespace:object#",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, empty subject set",
|
||||
subjectSet: ":#",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing subject set identifier",
|
||||
subjectSet: "namespace:#relation",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, missing object field",
|
||||
subjectSet: "namespace:object",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
desc: "check invalid subject set, unexpected object field",
|
||||
subjectSet: "namespace:object@relation",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
iss := isSubjectSet(tc.subjectSet)
|
||||
assert.Equal(t, iss, tc.result, fmt.Sprintf("%s expected to be %v, got %v\n", tc.desc, tc.result, iss))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetSubject(t *testing.T) {
|
||||
p1 := auth.PolicyReq{Subject: "subject", Object: "object", Relation: "relation"}
|
||||
s1 := getSubject(p1)
|
||||
ref1 := s1.GetRef()
|
||||
_, ok := ref1.(*acl.Subject_Id)
|
||||
assert.True(t, ok, fmt.Errorf("subject reference of %#v is expected to be (*acl.Subject_Id), got %T", p1, ref1))
|
||||
|
||||
p2 := auth.PolicyReq{Subject: "members:group#access", Object: "object", Relation: "relation"}
|
||||
s2 := getSubject(p2)
|
||||
ref2 := s2.GetRef()
|
||||
_, ok = ref2.(*acl.Subject_Set)
|
||||
assert.True(t, ok, fmt.Errorf("subject reference of %#v is expected to be (*acl.Subject_Set), got %T", p2, ref2))
|
||||
}
|
||||
+2
-2
@@ -22,8 +22,8 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
// UserKey is temporary User key received on successfull login.
|
||||
UserKey uint32 = iota
|
||||
// LoginKey is temporary User key received on successfull login.
|
||||
LoginKey uint32 = iota
|
||||
// RecoveryKey represents a key for resseting password.
|
||||
RecoveryKey
|
||||
// APIKey enables the one to act on behalf of the user.
|
||||
|
||||
+11
-10
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ auth.GroupRepository = (*groupRepositoryMock)(nil)
|
||||
@@ -55,14 +56,14 @@ func (grm *groupRepositoryMock) Save(ctx context.Context, group auth.Group) (aut
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[group.ID]; ok {
|
||||
return auth.Group{}, auth.ErrGroupConflict
|
||||
return auth.Group{}, errors.ErrConflict
|
||||
}
|
||||
path := group.ID
|
||||
|
||||
if group.ParentID != "" {
|
||||
parent, ok := grm.groups[group.ParentID]
|
||||
if !ok {
|
||||
return auth.Group{}, auth.ErrCreateGroup
|
||||
return auth.Group{}, errors.ErrCreateEntity
|
||||
}
|
||||
if _, ok := grm.children[group.ParentID]; !ok {
|
||||
grm.children[group.ParentID] = make(map[string]auth.Group)
|
||||
@@ -84,7 +85,7 @@ func (grm *groupRepositoryMock) Update(ctx context.Context, group auth.Group) (a
|
||||
defer grm.mu.Unlock()
|
||||
up, ok := grm.groups[group.ID]
|
||||
if !ok {
|
||||
return auth.Group{}, auth.ErrNotFound
|
||||
return auth.Group{}, errors.ErrNotFound
|
||||
}
|
||||
up.Name = group.Name
|
||||
up.Description = group.Description
|
||||
@@ -99,7 +100,7 @@ func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[id]; !ok {
|
||||
return auth.ErrGroupNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
if len(grm.members[id]) > 0 {
|
||||
@@ -131,7 +132,7 @@ func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (au
|
||||
|
||||
val, ok := grm.groups[id]
|
||||
if !ok {
|
||||
return auth.Group{}, auth.ErrGroupNotFound
|
||||
return auth.Group{}, errors.ErrNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -155,13 +156,13 @@ func (grm *groupRepositoryMock) Unassign(ctx context.Context, groupID string, me
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return auth.ErrGroupNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
for _, memberID := range memberIDs {
|
||||
for typ, m := range grm.members[groupID] {
|
||||
_, ok := m[memberID]
|
||||
if !ok {
|
||||
return auth.ErrGroupNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
delete(grm.members[groupID][typ], memberID)
|
||||
delete(grm.memberships[memberID], groupID)
|
||||
@@ -175,7 +176,7 @@ func (grm *groupRepositoryMock) Assign(ctx context.Context, groupID, groupType s
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return auth.ErrGroupNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
if _, ok := grm.members[groupID]; !ok {
|
||||
@@ -229,7 +230,7 @@ func (grm *groupRepositoryMock) Members(ctx context.Context, groupID, groupType
|
||||
var items []auth.Member
|
||||
members, ok := grm.members[groupID][groupType]
|
||||
if !ok {
|
||||
return auth.MemberPage{}, auth.ErrGroupNotFound
|
||||
return auth.MemberPage{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
@@ -259,7 +260,7 @@ func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID
|
||||
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return auth.GroupPage{}, auth.ErrGroupNotFound
|
||||
return auth.GroupPage{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
groups := make([]auth.Group, 0)
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ auth.KeyRepository = (*keyRepositoryMock)(nil)
|
||||
@@ -29,7 +30,7 @@ func (krm *keyRepositoryMock) Save(ctx context.Context, key auth.Key) (string, e
|
||||
defer krm.mu.Unlock()
|
||||
|
||||
if _, ok := krm.keys[key.ID]; ok {
|
||||
return "", auth.ErrConflict
|
||||
return "", errors.ErrConflict
|
||||
}
|
||||
|
||||
krm.keys[key.ID] = key
|
||||
@@ -43,7 +44,7 @@ func (krm *keyRepositoryMock) Retrieve(ctx context.Context, issuerID, id string)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return auth.Key{}, auth.ErrNotFound
|
||||
return auth.Key{}, errors.ErrNotFound
|
||||
}
|
||||
func (krm *keyRepositoryMock) Remove(ctx context.Context, issuerID, id string) error {
|
||||
krm.mu.Lock()
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
type MockSubjectSet struct {
|
||||
Object string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type policyAgentMock struct {
|
||||
mu sync.Mutex
|
||||
// authzDb stores 'subject' as a key, and subject policies as a value.
|
||||
authzDB map[string][]MockSubjectSet
|
||||
}
|
||||
|
||||
// NewKetoMock returns a mock service for Keto.
|
||||
// This mock is not implemented yet.
|
||||
func NewKetoMock(db map[string][]MockSubjectSet) auth.PolicyAgent {
|
||||
return &policyAgentMock{authzDB: db}
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
for _, ss := range ssList {
|
||||
if ss.Object == pr.Object && ss.Relation == pr.Relation {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.ErrAuthorization
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
pa.authzDB[pr.Subject] = append(pa.authzDB[pr.Subject], MockSubjectSet{Object: pr.Object, Relation: pr.Relation})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
for k, ss := range ssList {
|
||||
if ss.Object == pr.Object && ss.Relation == pr.Relation {
|
||||
ssList[k] = MockSubjectSet{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pa *policyAgentMock) RetrievePolicies(ctx context.Context, pr auth.PolicyReq) ([]*acl.RelationTuple, error) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
ssList := pa.authzDB[pr.Subject]
|
||||
tuple := []*acl.RelationTuple{}
|
||||
for _, ss := range ssList {
|
||||
if ss.Relation == pr.Relation {
|
||||
tuple = append(tuple, &acl.RelationTuple{Object: ss.Object, Relation: ss.Relation})
|
||||
}
|
||||
}
|
||||
return tuple, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1"
|
||||
)
|
||||
|
||||
// PolicyReq represents an argument struct for making a policy related
|
||||
// function calls.
|
||||
type PolicyReq struct {
|
||||
Subject string
|
||||
Object string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type PolicyPage struct {
|
||||
Policies []string
|
||||
}
|
||||
|
||||
// Authz represents a authorization service. It exposes
|
||||
// functionalities through `auth` to perform authorization.
|
||||
type Authz interface {
|
||||
// Authorize checks authorization of the given `subject`. Basically,
|
||||
// Authorize verifies that Is `subject` allowed to `relation` on
|
||||
// `object`. Authorize returns a non-nil error if the subject has
|
||||
// no relation on the object (which simply means the operation is
|
||||
// denied).
|
||||
Authorize(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicy creates a policy for the given subject, so that, after
|
||||
// AddPolicy, `subject` has a `relation` on `object`. Returns a non-nil
|
||||
// error in case of failures.
|
||||
AddPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicies adds new policies for given subjects. This method is
|
||||
// only allowed to use as an admin.
|
||||
AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error
|
||||
|
||||
// DeletePolicy removes a policy.
|
||||
DeletePolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// DeletePolicies deletes policies for given subjects. This method is
|
||||
// only allowed to use as an admin.
|
||||
DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error
|
||||
|
||||
// ListPolicies lists policies based on the given PolicyReq structure.
|
||||
ListPolicies(ctx context.Context, pr PolicyReq) (PolicyPage, error)
|
||||
}
|
||||
|
||||
// PolicyAgent facilitates the communication to authorization
|
||||
// services and implements Authz functionalities for certain
|
||||
// authorization services (e.g. ORY Keto).
|
||||
type PolicyAgent interface {
|
||||
// CheckPolicy checks if the subject has a relation on the object.
|
||||
// It returns a non-nil error if the subject has no relation on
|
||||
// the object (which simply means the operation is denied).
|
||||
CheckPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// AddPolicy creates a policy for the given subject, so that, after
|
||||
// AddPolicy, `subject` has a `relation` on `object`. Returns a non-nil
|
||||
// error in case of failures.
|
||||
AddPolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
// DeletePolicy removes a policy.
|
||||
DeletePolicy(ctx context.Context, pr PolicyReq) error
|
||||
|
||||
RetrievePolicies(ctx context.Context, pr PolicyReq) ([]*acl.RelationTuple, error)
|
||||
}
|
||||
+32
-33
@@ -17,11 +17,10 @@ import (
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
var (
|
||||
errStringToUUID = errors.New("error converting string")
|
||||
errStringToUUID = errors.New("error converting string to uuid")
|
||||
errGetTotal = errors.New("failed to get total number of groups")
|
||||
errCreateMetadataQuery = errors.New("failed to create query for metadata")
|
||||
|
||||
@@ -46,13 +45,13 @@ func NewGroupRepo(db Database) auth.GroupRepository {
|
||||
|
||||
func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
// For root group path is initialized with id
|
||||
q := `INSERT INTO groups (name, description, id, path, owner_id, metadata, created_at, updated_at)
|
||||
VALUES (:name, :description, :id, :id, :owner_id, :metadata, :created_at, :updated_at)
|
||||
q := `INSERT INTO groups (name, description, id, path, owner_id, metadata, created_at, updated_at)
|
||||
VALUES (:name, :description, :id, :id, :owner_id, :metadata, :created_at, :updated_at)
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
if g.ParentID != "" {
|
||||
// Path is constructed in insert_group_tr - init.go
|
||||
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at)
|
||||
VALUES ( :name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at)
|
||||
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at)
|
||||
VALUES ( :name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at)
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
}
|
||||
|
||||
@@ -67,15 +66,15 @@ func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, e
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, err)
|
||||
case errDuplicate:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, errors.New(pqErr.Message))
|
||||
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
@@ -89,12 +88,12 @@ func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, e
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
|
||||
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
|
||||
dbu, err := toDBGroup(g)
|
||||
if err != nil {
|
||||
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
|
||||
@@ -103,19 +102,19 @@ func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group,
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
|
||||
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbu = dbGroup{}
|
||||
if err := row.StructScan(&dbu); err != nil {
|
||||
return g, errors.Wrap(auth.ErrUpdateGroup, err)
|
||||
return g, errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return toGroup(dbu)
|
||||
@@ -128,7 +127,7 @@ func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
}
|
||||
dbg, err := toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrUpdateGroup, err)
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
@@ -137,25 +136,25 @@ func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
switch pqErr.Constraint {
|
||||
case groupIDFkeyy:
|
||||
return errors.Wrap(auth.ErrGroupNotEmpty, err)
|
||||
}
|
||||
return errors.Wrap(auth.ErrGroupConflict, err)
|
||||
return errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
|
||||
return errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrDeleteGroup, err)
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(auth.ErrDeleteGroup, err)
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -167,10 +166,10 @@ func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Gro
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups WHERE id = $1`
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return auth.Group{}, errors.Wrap(auth.ErrGroupNotFound, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(auth.ErrSelectEntity, err)
|
||||
return auth.Group{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
return toGroup(dbu)
|
||||
}
|
||||
@@ -186,7 +185,7 @@ func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata)
|
||||
mq = fmt.Sprintf(" AND %s", metaQuery)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
|
||||
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
|
||||
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", pm)
|
||||
@@ -240,7 +239,7 @@ func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) < :level`
|
||||
|
||||
@@ -370,7 +369,7 @@ func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm a
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
@@ -426,7 +425,7 @@ func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string,
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
|
||||
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
|
||||
VALUES(:group_id, :member_id, :type, :created_at, :updated_at)`
|
||||
|
||||
for _, id := range ids {
|
||||
@@ -444,9 +443,9 @@ func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string,
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
return errors.Wrap(auth.ErrConflict, errors.New(pqErr.Detail))
|
||||
return errors.Wrap(errors.ErrConflict, errors.New(pqErr.Detail))
|
||||
case errDuplicate:
|
||||
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pqErr.Detail))
|
||||
}
|
||||
@@ -483,9 +482,9 @@ func (gr groupRepository) Unassign(ctx context.Context, groupID string, ids ...s
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
return errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return errors.Wrap(auth.ErrConflict, err)
|
||||
return errors.Wrap(errors.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +718,7 @@ func (m *dbMetadata) Scan(value interface{}) error {
|
||||
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return users.ErrScanMetadata
|
||||
return errors.ErrScanMetadata
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, m); err != nil {
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestGroupSave(t *testing.T) {
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: auth.ErrGroupConflict,
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid name",
|
||||
@@ -81,7 +81,7 @@ func TestGroupSave(t *testing.T) {
|
||||
OwnerID: usrID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid description",
|
||||
@@ -91,7 +91,7 @@ func TestGroupSave(t *testing.T) {
|
||||
Name: groupName,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent",
|
||||
@@ -121,7 +121,7 @@ func TestGroupSave(t *testing.T) {
|
||||
OwnerID: usrID,
|
||||
Name: "wrongParent",
|
||||
},
|
||||
err: auth.ErrCreateGroup,
|
||||
err: errors.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestGroupRetrieveByID(t *testing.T) {
|
||||
assert.True(t, retrieved.Path == fmt.Sprintf("%s.%s", group1.ID, group2.ID), fmt.Sprintf("Save group, Path: expected %s got %s\n", fmt.Sprintf("%s.%s", group1.ID, group2.ID), retrieved.Path))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), generateGroupID(t))
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", auth.ErrGroupNotFound, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", errors.ErrNotFound, err))
|
||||
}
|
||||
|
||||
func TestGroupUpdate(t *testing.T) {
|
||||
@@ -245,7 +245,7 @@ func TestGroupUpdate(t *testing.T) {
|
||||
ID: "wrong",
|
||||
Name: groupName + "-2",
|
||||
},
|
||||
err: auth.ErrUpdateGroup,
|
||||
err: errors.ErrUpdateEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid name",
|
||||
@@ -253,7 +253,7 @@ func TestGroupUpdate(t *testing.T) {
|
||||
ID: groupID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid description",
|
||||
@@ -261,7 +261,7 @@ func TestGroupUpdate(t *testing.T) {
|
||||
ID: groupID,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ func TestGroupDelete(t *testing.T) {
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete parent with children with no members: expected %v got %v\n", nil, err))
|
||||
|
||||
_, err = groupRepo.RetrieveByID(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
|
||||
@@ -43,7 +43,7 @@ func (kr repo) Save(ctx context.Context, key auth.Key) (string, error) {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
if pqErr.Code.Name() == errDuplicate {
|
||||
return "", errors.Wrap(auth.ErrConflict, pqErr)
|
||||
return "", errors.Wrap(errors.ErrConflict, pqErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (kr repo) Retrieve(ctx context.Context, issuerID, id string) (auth.Key, err
|
||||
if err := kr.db.QueryRowxContext(ctx, q, issuerID, id).StructScan(&key); err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return auth.Key{}, errors.Wrap(auth.ErrNotFound, err)
|
||||
return auth.Key{}, errors.Wrap(errors.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return auth.Key{}, errors.Wrap(errRetrieve, err)
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestKeySave(t *testing.T) {
|
||||
ID: id,
|
||||
IssuerID: id,
|
||||
},
|
||||
err: auth.ErrConflict,
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -98,16 +98,16 @@ func TestKeyRetrieve(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unauthorized",
|
||||
desc: "retrieve key with empty issuer id",
|
||||
id: key.ID,
|
||||
owner: "",
|
||||
err: auth.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unknown key",
|
||||
desc: "retrieve non-existent key",
|
||||
id: "",
|
||||
owner: key.IssuerID,
|
||||
err: auth.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestMain(m *testing.M) {
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.8-alpine", cfg)
|
||||
container, err := pool.Run("postgres", "13.3-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
+168
-61
@@ -5,6 +5,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
@@ -13,27 +14,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
loginDuration = 10 * time.Hour
|
||||
recoveryDuration = 5 * time.Minute
|
||||
thingsGroupType = "things"
|
||||
|
||||
authoritiesObject = "authorities"
|
||||
memberRelation = "member"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnauthorizedAccess represents unauthorized access.
|
||||
ErrUnauthorizedAccess = errors.New("unauthorized access")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid owner or ID).
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrNotFound indicates a non-existing entity request.
|
||||
ErrNotFound = errors.New("entity not found")
|
||||
|
||||
// ErrGenerateGroupID indicates error in creating group.
|
||||
ErrGenerateGroupID = errors.New("failed to generate group id")
|
||||
|
||||
// ErrConflict indicates that entity already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
// ErrFailedToRetrieveMembers failed to retrieve group members.
|
||||
ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members")
|
||||
|
||||
@@ -49,7 +37,7 @@ var (
|
||||
// ErrFailedToRetrieveChildren failed to retrieve groups.
|
||||
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new user key")
|
||||
errIssueUser = errors.New("failed to issue new login key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
@@ -68,7 +56,7 @@ type Authn interface {
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(ctx context.Context, token, id string) error
|
||||
|
||||
// Retrieve retrieves data for the Key identified by the provided
|
||||
// RetrieveKey retrieves data for the Key identified by the provided
|
||||
// ID, that is issued by the user identified by the provided key.
|
||||
RetrieveKey(ctx context.Context, token, id string) (Key, error)
|
||||
|
||||
@@ -78,14 +66,7 @@ type Authn interface {
|
||||
Identify(ctx context.Context, token string) (Identity, error)
|
||||
}
|
||||
|
||||
// Authz specifies an API for the authorization and will be implemented
|
||||
// by evaluation of policies.
|
||||
type Authz interface {
|
||||
// Authorize checks access rights
|
||||
Authorize(ctx context.Context, token, sub, obj, act string) (bool, error)
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
// Token is a string value of the actual Key and is used to authenticate
|
||||
// an Auth service request.
|
||||
@@ -93,28 +74,32 @@ type Service interface {
|
||||
Authn
|
||||
Authz
|
||||
|
||||
// Implements groups API, creating groups, assigning members
|
||||
// GroupService implements groups API, creating groups, assigning members
|
||||
GroupService
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
groups GroupRepository
|
||||
idProvider mainflux.IDProvider
|
||||
ulidProvider mainflux.IDProvider
|
||||
tokenizer Tokenizer
|
||||
keys KeyRepository
|
||||
groups GroupRepository
|
||||
idProvider mainflux.IDProvider
|
||||
ulidProvider mainflux.IDProvider
|
||||
agent PolicyAgent
|
||||
tokenizer Tokenizer
|
||||
loginDuration time.Duration
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer) Service {
|
||||
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer, policyAgent PolicyAgent, duration time.Duration) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
groups: groups,
|
||||
idProvider: idp,
|
||||
ulidProvider: ulid.New(),
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
groups: groups,
|
||||
idProvider: idp,
|
||||
ulidProvider: ulid.New(),
|
||||
agent: policyAgent,
|
||||
loginDuration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +113,7 @@ func (svc service) Issue(ctx context.Context, token string, key Key) (Key, strin
|
||||
case RecoveryKey:
|
||||
return svc.tmpKey(recoveryDuration, key)
|
||||
default:
|
||||
return svc.tmpKey(loginDuration, key)
|
||||
return svc.tmpKey(svc.loginDuration, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,15 +148,91 @@ func (svc service) Identify(ctx context.Context, token string) (Identity, error)
|
||||
}
|
||||
|
||||
switch key.Type {
|
||||
case APIKey, RecoveryKey, UserKey:
|
||||
case RecoveryKey, LoginKey:
|
||||
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
||||
case APIKey:
|
||||
_, err := svc.keys.Retrieve(context.TODO(), key.IssuerID, key.ID)
|
||||
if err != nil {
|
||||
return Identity{}, errors.ErrAuthentication
|
||||
}
|
||||
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
||||
default:
|
||||
return Identity{}, ErrUnauthorizedAccess
|
||||
return Identity{}, errors.ErrAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Authorize(ctx context.Context, token, sub, obj, act string) (bool, error) {
|
||||
return true, nil
|
||||
func (svc service) Authorize(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.CheckPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) AddPolicy(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.AddPolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Authorize(ctx, PolicyReq{Object: authoritiesObject, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, subjectID := range subjectIDs {
|
||||
for _, relation := range relations {
|
||||
if err := svc.AddPolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add '%s' policy on object '%s' for subject '%s': %s", relation, object, subjectID, err), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) DeletePolicy(ctx context.Context, pr PolicyReq) error {
|
||||
return svc.agent.DeletePolicy(ctx, pr)
|
||||
}
|
||||
|
||||
func (svc service) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user identified by token is the admin.
|
||||
if err := svc.Authorize(ctx, PolicyReq{Object: authoritiesObject, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, subjectID := range subjectIDs {
|
||||
for _, relation := range relations {
|
||||
if err := svc.DeletePolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy on object '%s' for subject '%s': %s", relation, object, subjectID, err), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.agent.AddPolicy(ctx, PolicyReq{Object: thingGroupID, Relation: memberRelation, Subject: fmt.Sprintf("%s:%s#%s", "members", userGroupID, memberRelation)})
|
||||
}
|
||||
|
||||
func (svc service) ListPolicies(ctx context.Context, pr PolicyReq) (PolicyPage, error) {
|
||||
res, err := svc.agent.RetrievePolicies(ctx, pr)
|
||||
if err != nil {
|
||||
return PolicyPage{}, err
|
||||
}
|
||||
var page PolicyPage
|
||||
for _, tuple := range res {
|
||||
page.Policies = append(page.Policies, tuple.GetObject())
|
||||
}
|
||||
return page, err
|
||||
}
|
||||
|
||||
func (svc service) tmpKey(duration time.Duration, key Key) (Key, string, error) {
|
||||
@@ -218,9 +279,9 @@ func (svc service) login(token string) (string, string, error) {
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Only user key token is valid for login.
|
||||
if key.Type != UserKey || key.IssuerID == "" {
|
||||
return "", "", ErrUnauthorizedAccess
|
||||
// Only login key token is valid for login.
|
||||
if key.Type != LoginKey || key.IssuerID == "" {
|
||||
return "", "", errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return key.IssuerID, key.Subject, nil
|
||||
@@ -229,12 +290,12 @@ func (svc service) login(token string) (string, string, error) {
|
||||
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
ulid, err := svc.ulidProvider.ID()
|
||||
if err != nil {
|
||||
return Group{}, errors.Wrap(ErrGenerateGroupID, err)
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
timestamp := getTimestmap()
|
||||
@@ -249,33 +310,37 @@ func (svc service) CreateGroup(ctx context.Context, token string, group Group) (
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: group.ID, Relation: memberRelation, Subject: user.ID}); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (svc service) ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAll(ctx, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListParents(ctx context.Context, token string, childID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAllParents(ctx, childID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListChildren(ctx context.Context, token string, parentID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveAllChildren(ctx, parentID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListMembers(ctx context.Context, token string, groupID, groupType string, pm PageMetadata) (MemberPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return MemberPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return MemberPage{}, err
|
||||
}
|
||||
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
|
||||
if err != nil {
|
||||
@@ -286,14 +351,14 @@ func (svc service) ListMembers(ctx context.Context, token string, groupID, group
|
||||
|
||||
func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return err
|
||||
}
|
||||
return svc.groups.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
group.UpdatedAt = getTimestmap()
|
||||
@@ -302,28 +367,70 @@ func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (
|
||||
|
||||
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Group{}, err
|
||||
}
|
||||
return svc.groups.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) Assign(ctx context.Context, token string, groupID, groupType string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return err
|
||||
}
|
||||
return svc.groups.Assign(ctx, groupID, groupType, memberIDs...)
|
||||
|
||||
if err := svc.groups.Assign(ctx, groupID, groupType, memberIDs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if groupType == thingsGroupType {
|
||||
ss := fmt.Sprintf("%s:%s#%s", "members", groupID, memberRelation)
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
for _, action := range []string{"read", "write", "delete"} {
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: memberID, Relation: action, Subject: ss}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add thing: '%s' to thing group: '%s'", memberID, groupID), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
if err := svc.agent.AddPolicy(ctx, PolicyReq{Object: groupID, Relation: memberRelation, Subject: memberID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot add user: '%s' to user group: '%s'", memberID, groupID), errs)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (svc service) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return err
|
||||
}
|
||||
return svc.groups.Unassign(ctx, groupID, memberIDs...)
|
||||
|
||||
ss := fmt.Sprintf("%s:%s#%s", "members", groupID, memberRelation)
|
||||
var errs error
|
||||
for _, memberID := range memberIDs {
|
||||
// If the member is a user, <groupID>#member@memberID must be deleted.
|
||||
if err := svc.agent.DeletePolicy(ctx, PolicyReq{Object: groupID, Relation: memberRelation, Subject: memberID}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete a membership of member '%s' from group '%s'", memberID, groupID), errs)
|
||||
}
|
||||
|
||||
// If the member is a Thing, memberID#read|write|delete@(members:groupID#member) must be deleted.
|
||||
for _, action := range []string{"read", "write", "delete"} {
|
||||
if err := svc.agent.DeletePolicy(ctx, PolicyReq{Object: memberID, Relation: action, Subject: ss}); err != nil {
|
||||
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy from member '%s'", action, memberID), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := svc.groups.Unassign(ctx, groupID, memberIDs...)
|
||||
return errors.Wrap(err, errs)
|
||||
}
|
||||
|
||||
func (svc service) ListMemberships(ctx context.Context, token string, memberID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.Memberships(ctx, memberID, pm)
|
||||
}
|
||||
|
||||
+342
-56
@@ -26,19 +26,28 @@ const (
|
||||
id = "testID"
|
||||
groupName = "mfx"
|
||||
description = "Description"
|
||||
|
||||
memberRelation = "member"
|
||||
authoritiesObj = "authorities"
|
||||
loginDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
func newService() auth.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
mockAuthzDB := map[string][]mocks.MockSubjectSet{}
|
||||
mockAuthzDB[id] = append(mockAuthzDB[id], mocks.MockSubjectSet{Object: authoritiesObj, Relation: memberRelation})
|
||||
ketoMock := mocks.NewKetoMock(mockAuthzDB)
|
||||
|
||||
t := jwt.New(secret)
|
||||
return auth.New(repo, groupRepo, idProvider, t)
|
||||
return auth.New(repo, groupRepo, idProvider, t, ketoMock, loginDuration)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -48,18 +57,18 @@ func TestIssue(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue user key",
|
||||
desc: "issue login key",
|
||||
key: auth.Key{
|
||||
Type: auth.UserKey,
|
||||
Type: auth.LoginKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
token: secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue user key with no time",
|
||||
desc: "issue login key with no time",
|
||||
key: auth.Key{
|
||||
Type: auth.UserKey,
|
||||
Type: auth.LoginKey,
|
||||
},
|
||||
token: secret,
|
||||
err: auth.ErrInvalidKeyIssuedAt,
|
||||
@@ -74,13 +83,13 @@ func TestIssue(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthorized",
|
||||
desc: "issue API key with an invalid token",
|
||||
key: auth.Key{
|
||||
Type: auth.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
token: "invalid",
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "issue API key with no time",
|
||||
@@ -117,7 +126,7 @@ func TestIssue(t *testing.T) {
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{
|
||||
Type: auth.APIKey,
|
||||
@@ -135,22 +144,22 @@ func TestRevoke(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke user key",
|
||||
desc: "revoke login key",
|
||||
id: newKey.ID,
|
||||
token: secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke non-existing user key",
|
||||
desc: "revoke non-existing login key",
|
||||
id: newKey.ID,
|
||||
token: secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke unauthorized",
|
||||
desc: "revoke with empty login key",
|
||||
id: newKey.ID,
|
||||
token: "",
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -162,7 +171,7 @@ func TestRevoke(t *testing.T) {
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), Subject: email, IssuerID: id})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), Subject: email, IssuerID: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
@@ -172,11 +181,11 @@ func TestRetrieve(t *testing.T) {
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, userToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
_, userToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
apiKey, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login's key expected to succeed: %s", err))
|
||||
|
||||
_, resetToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
@@ -188,34 +197,34 @@ func TestRetrieve(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve user key",
|
||||
desc: "retrieve login key",
|
||||
id: apiKey.ID,
|
||||
token: userToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve non-existing user key",
|
||||
desc: "retrieve non-existing login key",
|
||||
id: "invalid",
|
||||
token: userToken,
|
||||
err: auth.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve unauthorized",
|
||||
desc: "retrieve with wrong login key",
|
||||
id: apiKey.ID,
|
||||
token: "wrong",
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with API token",
|
||||
id: apiKey.ID,
|
||||
token: apiToken,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with reset token",
|
||||
id: apiKey.ID,
|
||||
token: resetToken,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -228,21 +237,21 @@ func TestRetrieve(t *testing.T) {
|
||||
func TestIdentify(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
_, recoverySecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
|
||||
_, apiSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: auth.APIKey, IssuerID: id, Subject: email, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
exp1 := time.Now().Add(-2 * time.Second)
|
||||
_, expSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing expired user key expected to succeed: %s", err))
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing expired login key expected to succeed: %s", err))
|
||||
|
||||
_, invalidSecret, err := svc.Issue(context.Background(), loginSecret, auth.Key{Type: 22, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -278,13 +287,13 @@ func TestIdentify(t *testing.T) {
|
||||
desc: "identify expired key",
|
||||
key: invalidSecret,
|
||||
idt: auth.Identity{},
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "identify invalid key",
|
||||
key: "invalid",
|
||||
idt: auth.Identity{},
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -297,7 +306,7 @@ func TestIdentify(t *testing.T) {
|
||||
|
||||
func TestCreateGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -324,6 +333,9 @@ func TestCreateGroup(t *testing.T) {
|
||||
parent, err := svc.CreateGroup(context.Background(), apiToken, parentGroup)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating parent group expected to succeed: %s", err))
|
||||
|
||||
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: parent.ID, Relation: memberRelation, Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Checking parent group owner's policy expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group auth.Group
|
||||
@@ -353,19 +365,24 @@ func TestCreateGroup(t *testing.T) {
|
||||
Name: groupName,
|
||||
ParentID: "xxxxxxxxxx",
|
||||
},
|
||||
err: auth.ErrCreateGroup,
|
||||
err: errors.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.CreateGroup(context.Background(), apiToken, tc.group)
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
if err == nil {
|
||||
authzErr := svc.Authorize(context.Background(), auth.PolicyReq{Object: g.ID, Relation: memberRelation, Subject: g.OwnerID})
|
||||
assert.Nil(t, authzErr, fmt.Sprintf("%s - Checking group owner's policy expected to succeed: %s", tc.desc, authzErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -422,7 +439,7 @@ func TestUpdateGroup(t *testing.T) {
|
||||
|
||||
func TestViewGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -461,16 +478,16 @@ func TestViewGroup(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "view group with unauthorized token",
|
||||
desc: "view group with invalid token",
|
||||
token: "wrongtoken",
|
||||
groupID: group.ID,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "view group for wrong id",
|
||||
token: apiToken,
|
||||
groupID: "wrong",
|
||||
err: auth.ErrGroupNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -482,7 +499,7 @@ func TestViewGroup(t *testing.T) {
|
||||
|
||||
func TestListGroups(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -535,7 +552,7 @@ func TestListGroups(t *testing.T) {
|
||||
token: "wrongToken",
|
||||
level: 5,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -550,7 +567,7 @@ func TestListGroups(t *testing.T) {
|
||||
|
||||
func TestListChildren(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -601,7 +618,7 @@ func TestListChildren(t *testing.T) {
|
||||
token: "wrongToken",
|
||||
level: 5,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -615,7 +632,7 @@ func TestListChildren(t *testing.T) {
|
||||
|
||||
func TestListParents(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -666,7 +683,7 @@ func TestListParents(t *testing.T) {
|
||||
token: "wrongToken",
|
||||
level: 5,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -680,7 +697,7 @@ func TestListParents(t *testing.T) {
|
||||
|
||||
func TestListMembers(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -743,7 +760,7 @@ func TestListMembers(t *testing.T) {
|
||||
offset: 0,
|
||||
limit: n,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -758,7 +775,7 @@ func TestListMembers(t *testing.T) {
|
||||
|
||||
func TestListMemberships(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -788,6 +805,7 @@ func TestListMemberships(t *testing.T) {
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
|
||||
_ = svc.AddPolicy(context.Background(), auth.PolicyReq{Subject: id, Object: memberID, Relation: "owner"})
|
||||
err = svc.Assign(context.Background(), apiToken, g.ID, "things", memberID)
|
||||
require.Nil(t, err, fmt.Sprintf("Assign member expected to succeed: %s\n", err))
|
||||
}
|
||||
@@ -822,7 +840,7 @@ func TestListMemberships(t *testing.T) {
|
||||
offset: 0,
|
||||
limit: n,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -836,7 +854,7 @@ func TestListMemberships(t *testing.T) {
|
||||
|
||||
func TestRemoveGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -865,10 +883,10 @@ func TestRemoveGroup(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
err = svc.RemoveGroup(context.Background(), "wrongToken", group.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrAuthentication), fmt.Sprintf("Unauthorized access: expected %v got %v", errors.ErrAuthentication, err))
|
||||
|
||||
err = svc.RemoveGroup(context.Background(), apiToken, "wrongID")
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Remove group with wrong id: expected %v got %v", auth.ErrGroupNotFound, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Remove group with wrong id: expected %v got %v", errors.ErrNotFound, err))
|
||||
|
||||
gp, err := svc.ListGroups(context.Background(), apiToken, auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("list groups unexpected error: %s", err))
|
||||
@@ -885,7 +903,7 @@ func TestRemoveGroup(t *testing.T) {
|
||||
|
||||
func TestAssign(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -919,18 +937,27 @@ func TestAssign(t *testing.T) {
|
||||
err = svc.Assign(context.Background(), apiToken, group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
|
||||
// check access control policies things members.
|
||||
subjectSet := fmt.Sprintf("%s:%s#%s", "members", group.ID, memberRelation)
|
||||
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: mid, Relation: "read", Subject: subjectSet})
|
||||
require.Nil(t, err, fmt.Sprintf("entites having an access to group %s must have %s policy on %s: %s", group.ID, "read", mid, err))
|
||||
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: mid, Relation: "write", Subject: subjectSet})
|
||||
require.Nil(t, err, fmt.Sprintf("entites having an access to group %s must have %s policy on %s: %s", group.ID, "write", mid, err))
|
||||
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: mid, Relation: "delete", Subject: subjectSet})
|
||||
require.Nil(t, err, fmt.Sprintf("entites having an access to group %s must have %s policy on %s: %s", group.ID, "delete", mid, err))
|
||||
|
||||
mp, err := svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Offset: 0, Limit: 10})
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
|
||||
err = svc.Assign(context.Background(), "wrongToken", group.ID, "things", mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrAuthentication), fmt.Sprintf("Unauthorized access: expected %v got %v", errors.ErrAuthentication, err))
|
||||
|
||||
}
|
||||
|
||||
func TestUnassign(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
@@ -976,8 +1003,267 @@ func TestUnassign(t *testing.T) {
|
||||
assert.True(t, mp.Total == 0, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 0, mp.Total))
|
||||
|
||||
err = svc.Unassign(context.Background(), "wrongToken", group.ID, mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrAuthentication), fmt.Sprintf("Unauthorized access: expected %v got %v", errors.ErrAuthentication, err))
|
||||
|
||||
err = svc.Unassign(context.Background(), apiToken, group.ID, mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
|
||||
assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
pr := auth.PolicyReq{Object: authoritiesObj, Relation: memberRelation, Subject: id}
|
||||
err := svc.Authorize(context.Background(), pr)
|
||||
require.Nil(t, err, fmt.Sprintf("authorizing initial %v policy expected to succeed: %s", pr, err))
|
||||
}
|
||||
|
||||
func TestAddPolicy(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
pr := auth.PolicyReq{Object: "obj", Relation: "rel", Subject: "sub"}
|
||||
err := svc.AddPolicy(context.Background(), pr)
|
||||
require.Nil(t, err, fmt.Sprintf("adding %v policy expected to succeed: %v", pr, err))
|
||||
|
||||
err = svc.Authorize(context.Background(), pr)
|
||||
require.Nil(t, err, fmt.Sprintf("checking shared %v policy expected to be succeed: %#v", pr, err))
|
||||
}
|
||||
|
||||
func TestDeletePolicy(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
pr := auth.PolicyReq{Object: authoritiesObj, Relation: memberRelation, Subject: id}
|
||||
err := svc.DeletePolicy(context.Background(), pr)
|
||||
require.Nil(t, err, fmt.Sprintf("deleting %v policy expected to succeed: %s", pr, err))
|
||||
}
|
||||
|
||||
func TestAssignAccessRights(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
userGroupID := "user-group"
|
||||
thingGroupID := "thing-group"
|
||||
err = svc.AssignGroupAccessRights(context.Background(), apiToken, thingGroupID, userGroupID)
|
||||
require.Nil(t, err, fmt.Sprintf("sharing the user group with thing group expected to succeed: %v", err))
|
||||
|
||||
err = svc.Authorize(context.Background(), auth.PolicyReq{Object: thingGroupID, Relation: memberRelation, Subject: fmt.Sprintf("%s:%s#%s", "members", userGroupID, memberRelation)})
|
||||
require.Nil(t, err, fmt.Sprintf("checking shared group access policy expected to be succeed: %#v", err))
|
||||
}
|
||||
|
||||
func TestAddPolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
thingID, err := idProvider.ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tmpID := "tmpid"
|
||||
readPolicy := "read"
|
||||
writePolicy := "write"
|
||||
deletePolicy := "delete"
|
||||
|
||||
// Add read policy to users.
|
||||
err = svc.AddPolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{readPolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
|
||||
|
||||
// Add write and delete policies to users.
|
||||
err = svc.AddPolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{writePolicy, deletePolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("adding multiple policies expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
policy auth.PolicyReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "check valid 'read' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: id},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'write' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: id},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'delete' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: id},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'read' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: tmpID},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'write' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: tmpID},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'delete' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: tmpID},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check invalid 'access' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: "access", Subject: id},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check invalid 'access' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: "access", Subject: tmpID},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Authorize(context.Background(), tc.policy)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
thingID, err := idProvider.ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
tmpID := "tmpid"
|
||||
readPolicy := "read"
|
||||
writePolicy := "write"
|
||||
deletePolicy := "delete"
|
||||
memberPolicy := "member"
|
||||
|
||||
// Add read, write and delete policies to users.
|
||||
err = svc.AddPolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{readPolicy, writePolicy, deletePolicy, memberPolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
|
||||
|
||||
// Delete multiple policies from single user.
|
||||
err = svc.DeletePolicies(context.Background(), apiToken, thingID, []string{id}, []string{readPolicy, writePolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("deleting policies from single user expected to succeed: %s", err))
|
||||
|
||||
// Delete multiple policies from multiple user.
|
||||
err = svc.DeletePolicies(context.Background(), apiToken, thingID, []string{id, tmpID}, []string{deletePolicy, memberPolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("deleting policies from multiple user expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
policy auth.PolicyReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "check non-existing 'read' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: id},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check non-existing 'write' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: id},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check non-existing 'delete' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: id},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check non-existing 'member' policy of user with id",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: memberPolicy, Subject: id},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check non-existing 'delete' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: tmpID},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check non-existing 'member' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: memberPolicy, Subject: tmpID},
|
||||
err: errors.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'read' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: tmpID},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "check valid 'write' policy of user with tmpid",
|
||||
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: tmpID},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Authorize(context.Background(), tc.policy)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPolicies(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.LoginKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
readPolicy := "read"
|
||||
pageLen := 15
|
||||
|
||||
// Add arbitrary policies to the user.
|
||||
for i := 0; i < pageLen; i++ {
|
||||
err = svc.AddPolicies(context.Background(), apiToken, fmt.Sprintf("thing-%d", i), []string{id}, []string{readPolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
|
||||
}
|
||||
|
||||
page, err := svc.ListPolicies(context.Background(), auth.PolicyReq{Subject: id, Relation: readPolicy})
|
||||
assert.Nil(t, err, fmt.Sprintf("listing policies expected to succeed: %s", err))
|
||||
assert.Equal(t, pageLen, len(page.Policies), fmt.Sprintf("unexpected listing page size, expected %d, got %d: %v", pageLen, len(page.Policies), err))
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -115,6 +115,6 @@ Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. Thi
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](openapi.yml).
|
||||
the [API documentation](https://api.mainflux.io/?urls.primaryName=bootstrap-openapi.yml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
[doc]: https://docs.mainflux.io
|
||||
|
||||
+18
-18
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
@@ -34,7 +34,7 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(req.token, config)
|
||||
saved, err := svc.Add(ctx, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,13 +49,13 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.UpdateCert(req.key, req.thingID, req.ClientCert, req.ClientKey, req.CACert); err != nil {
|
||||
if err := svc.UpdateCert(ctx, req.token, req.thingID, req.ClientCert, req.ClientKey, req.CACert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := svc.View(req.key, req.id)
|
||||
config, err := svc.View(ctx, req.token, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
@@ -116,7 +116,7 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(req.key, config); err != nil {
|
||||
if err := svc.Update(ctx, req.token, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -130,14 +130,14 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateConnReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.UpdateConnections(req.key, req.id, req.Channels); err != nil {
|
||||
if err := svc.UpdateConnections(ctx, req.token, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -151,14 +151,14 @@ func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.List(req.key, req.filter, req.offset, req.limit)
|
||||
page, err := svc.List(ctx, req.token, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,14 +197,14 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.Remove(req.key, req.id); err != nil {
|
||||
if err := svc.Remove(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -213,13 +213,13 @@ func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := svc.Bootstrap(req.key, req.id, secure)
|
||||
cfg, err := svc.Bootstrap(ctx, req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -229,14 +229,14 @@ func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, sec
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(changeStateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.ChangeState(req.key, req.id, req.State); err != nil {
|
||||
if err := svc.ChangeState(ctx, req.token, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+100
-95
@@ -4,6 +4,7 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
@@ -22,6 +23,9 @@ import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
bsapi "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
@@ -79,11 +83,14 @@ var (
|
||||
CACert: "newca",
|
||||
}
|
||||
|
||||
bsErrorRes = toJSON(errorRes{bootstrap.ErrBootstrap.Error()})
|
||||
unauthRes = toJSON(errorRes{bootstrap.ErrUnauthorizedAccess.Error()})
|
||||
malformedRes = toJSON(errorRes{bootstrap.ErrMalformedEntity.Error()})
|
||||
extKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrExternalKeyNotFound.Error()})
|
||||
extSecKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrSecureBootstrap.Error()})
|
||||
bsErrorRes = toJSON(apiutil.ErrorRes{Err: bootstrap.ErrBootstrap.Error()})
|
||||
authnRes = toJSON(apiutil.ErrorRes{Err: errors.ErrAuthentication.Error()})
|
||||
authzRes = toJSON(apiutil.ErrorRes{Err: errors.ErrAuthorization.Error()})
|
||||
malformedRes = toJSON(apiutil.ErrorRes{Err: errors.ErrMalformedEntity.Error()})
|
||||
extKeyRes = toJSON(apiutil.ErrorRes{Err: bootstrap.ErrExternalKey.Error()})
|
||||
extSecKeyRes = toJSON(apiutil.ErrorRes{Err: bootstrap.ErrExternalKeySecure.Error()})
|
||||
missingIDRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrMissingID.Error()})
|
||||
missingKeyRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrBearerKey.Error()})
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -92,6 +99,7 @@ type testRequest struct {
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
key string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
@@ -116,7 +124,10 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
}
|
||||
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
if tr.key != "" {
|
||||
req.Header.Set("Authorization", apiutil.ThingPrefix+tr.key)
|
||||
}
|
||||
|
||||
if tr.contentType != "" {
|
||||
@@ -147,7 +158,7 @@ func dec(in []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, bootstrap.ErrMalformedEntity
|
||||
return nil, errors.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
@@ -159,7 +170,7 @@ func dec(in []byte) ([]byte, error) {
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
ThingsURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
@@ -184,12 +195,14 @@ func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := thingsapi.MakeHandler(mocktracer.New(), svc)
|
||||
logger := logger.NewMock()
|
||||
mux := thingsapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newBootstrapServer(svc bootstrap.Service) *httptest.Server {
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader(encKey))
|
||||
logger := logger.NewMock()
|
||||
mux := bsapi.MakeHandler(svc, bootstrap.NewConfigReader(encKey), logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -199,10 +212,10 @@ func toJSON(data interface{}) string {
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
data := toJSON(addReq)
|
||||
@@ -224,11 +237,11 @@ func TestAdd(t *testing.T) {
|
||||
location string
|
||||
}{
|
||||
{
|
||||
desc: "add a config unauthorized",
|
||||
desc: "add a config with invalid token",
|
||||
req: data,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
location: "",
|
||||
},
|
||||
{
|
||||
@@ -313,6 +326,7 @@ func TestAdd(t *testing.T) {
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
@@ -323,10 +337,10 @@ func TestAdd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
c := newConfig([]bootstrap.Channel{})
|
||||
|
||||
@@ -339,7 +353,7 @@ func TestView(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var channels []channel
|
||||
@@ -366,10 +380,10 @@ func TestView(t *testing.T) {
|
||||
res config
|
||||
}{
|
||||
{
|
||||
desc: "view a config unauthorized",
|
||||
desc: "view a config with invalid token",
|
||||
auth: invalidToken,
|
||||
id: saved.MFThing,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: config{},
|
||||
},
|
||||
{
|
||||
@@ -390,7 +404,7 @@ func TestView(t *testing.T) {
|
||||
desc: "view a config with an empty token",
|
||||
auth: "",
|
||||
id: saved.MFThing,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: config{},
|
||||
},
|
||||
}
|
||||
@@ -420,15 +434,15 @@ func TestView(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
data := toJSON(updateReq)
|
||||
@@ -442,12 +456,12 @@ func TestUpdate(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "update unauthorized",
|
||||
desc: "update with invalid token",
|
||||
req: data,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update with an empty token",
|
||||
@@ -455,7 +469,7 @@ func TestUpdate(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update a valid config",
|
||||
@@ -514,15 +528,15 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
data := toJSON(updateReq)
|
||||
@@ -536,12 +550,12 @@ func TestUpdateCert(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "update unauthorized",
|
||||
desc: "update with invalid token",
|
||||
req: data,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update with an empty token",
|
||||
@@ -549,7 +563,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update a valid config",
|
||||
@@ -609,15 +623,15 @@ func TestUpdateCert(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
data := toJSON(updateReq)
|
||||
@@ -636,12 +650,12 @@ func TestUpdateConnections(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "update connections unauthorized",
|
||||
desc: "update connections with invalid token",
|
||||
req: data,
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update connections with an empty token",
|
||||
@@ -649,7 +663,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "update connections valid config",
|
||||
@@ -722,13 +736,13 @@ func TestList(t *testing.T) {
|
||||
var active, inactive []config
|
||||
list := make([]config, configNum)
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/configs")
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
for i := 0; i < configNum; i++ {
|
||||
c.ExternalID = strconv.Itoa(i)
|
||||
@@ -736,7 +750,7 @@ func TestList(t *testing.T) {
|
||||
c.Name = fmt.Sprintf("%s-%d", addName, i)
|
||||
c.ExternalKey = fmt.Sprintf("%s%s", addExternalKey, strconv.Itoa(i))
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var channels []channel
|
||||
@@ -762,7 +776,7 @@ func TestList(t *testing.T) {
|
||||
if i%2 == 0 {
|
||||
state = bootstrap.Inactive
|
||||
}
|
||||
err := svc.ChangeState(validToken, list[i].MFThing, state)
|
||||
err := svc.ChangeState(context.Background(), validToken, list[i].MFThing, state)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
|
||||
list[i].State = state
|
||||
if state == bootstrap.Inactive {
|
||||
@@ -780,17 +794,17 @@ func TestList(t *testing.T) {
|
||||
res configPage
|
||||
}{
|
||||
{
|
||||
desc: "view list unauthorized",
|
||||
desc: "view list with invalid token",
|
||||
auth: invalidToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 10),
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
desc: "view list with an empty token",
|
||||
auth: "",
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 10),
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
@@ -833,13 +847,8 @@ func TestList(t *testing.T) {
|
||||
desc: "view with limit greater than allowed",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 1000),
|
||||
status: http.StatusOK,
|
||||
res: configPage{
|
||||
Total: uint64(len(list)),
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Configs: list[:100],
|
||||
},
|
||||
status: http.StatusBadRequest,
|
||||
res: configPage{},
|
||||
},
|
||||
{
|
||||
desc: "view list with no specified limit and offset",
|
||||
@@ -970,15 +979,15 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -988,15 +997,15 @@ func TestRemove(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "remove unauthorized",
|
||||
desc: "remove with invalid token",
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
}, {
|
||||
desc: "remove with an empty token",
|
||||
id: saved.MFThing,
|
||||
auth: "",
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existing config",
|
||||
@@ -1032,15 +1041,15 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
encExternKey, err := enc([]byte(c.ExternalKey))
|
||||
@@ -1092,23 +1101,23 @@ func TestBootstrap(t *testing.T) {
|
||||
externalID: "",
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusBadRequest,
|
||||
res: malformedRes,
|
||||
res: missingIDRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with unknown key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: unknown,
|
||||
status: http.StatusNotFound,
|
||||
res: extKeyNotFoundRes,
|
||||
status: http.StatusForbidden,
|
||||
res: extKeyRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with an empty key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "",
|
||||
status: http.StatusForbidden,
|
||||
res: unauthRes,
|
||||
status: http.StatusUnauthorized,
|
||||
res: missingKeyRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
@@ -1131,8 +1140,8 @@ func TestBootstrap(t *testing.T) {
|
||||
desc: "bootstrap secure with unencrypted key",
|
||||
externalID: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: extSecKeyNotFoundRes,
|
||||
status: http.StatusForbidden,
|
||||
res: extSecKeyRes,
|
||||
secure: true,
|
||||
},
|
||||
}
|
||||
@@ -1142,7 +1151,7 @@ func TestBootstrap(t *testing.T) {
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/things/bootstrap/%s", bs.URL, tc.externalID),
|
||||
token: tc.externalKey,
|
||||
key: tc.externalKey,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
@@ -1160,15 +1169,15 @@ func TestBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
auth := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, ts.URL)
|
||||
ts := newThingsServer(newThingsService(auth))
|
||||
svc := newService(auth, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
c := newConfig([]bootstrap.Channel{{ID: "1"}})
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
inactive := fmt.Sprintf("{\"state\": %d}", bootstrap.Inactive)
|
||||
@@ -1183,12 +1192,12 @@ func TestChangeState(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "change state unauthorized",
|
||||
desc: "change state with invalid token",
|
||||
id: saved.MFThing,
|
||||
auth: invalidToken,
|
||||
state: active,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "change state with an empty token",
|
||||
@@ -1196,7 +1205,7 @@ func TestChangeState(t *testing.T) {
|
||||
auth: "",
|
||||
state: active,
|
||||
contentType: contentType,
|
||||
status: http.StatusForbidden,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "change state with invalid content type",
|
||||
@@ -1286,7 +1295,3 @@ type configPage struct {
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []config `json:"configs"`
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+28
-27
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -25,7 +26,7 @@ func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Se
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) Add(ctx context.Context, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -35,10 +36,10 @@ func (lm *loggingMiddleware) Add(token string, cfg bootstrap.Config) (saved boot
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(token, cfg)
|
||||
return lm.svc.Add(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -48,10 +49,10 @@ func (lm *loggingMiddleware) View(token, id string) (saved bootstrap.Config, err
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(token, id)
|
||||
return lm.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update for token %s and thing %s took %s to complete", token, cfg.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -61,10 +62,10 @@ func (lm *loggingMiddleware) Update(token string, cfg bootstrap.Config) (err err
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(token, cfg)
|
||||
return lm.svc.Update(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, token, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_cert for thing with id %s took %s to complete", thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -74,10 +75,10 @@ func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, c
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(token, thingID, clientCert, clientKey, caCert)
|
||||
return lm.svc.UpdateCert(ctx, token, thingID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_connections for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -87,10 +88,10 @@ func (lm *loggingMiddleware) UpdateConnections(token, id string, connections []s
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(token, id, connections)
|
||||
return lm.svc.UpdateConnections(ctx, token, id, connections)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list for token %s and offset %d and limit %d took %s to complete", token, offset, limit, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -100,10 +101,10 @@ func (lm *loggingMiddleware) List(token string, filter bootstrap.Filter, offset,
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(token, filter, offset, limit)
|
||||
return lm.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Remove(token, id string) (err error) {
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -113,10 +114,10 @@ func (lm *loggingMiddleware) Remove(token, id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(token, id)
|
||||
return lm.svc.Remove(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method bootstrap for thing with external id %s took %s to complete", externalID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -126,10 +127,10 @@ func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bo
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
func (lm *loggingMiddleware) ChangeState(ctx context.Context, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method change_state for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -139,10 +140,10 @@ func (lm *loggingMiddleware) ChangeState(token, id string, state bootstrap.State
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(token, id, state)
|
||||
return lm.svc.ChangeState(ctx, token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_channel_handler for channel %s took %s to complete", channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -152,10 +153,10 @@ func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (er
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(channel)
|
||||
return lm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_config_handler for config %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -165,10 +166,10 @@ func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(id)
|
||||
return lm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_channel_handler for channel %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -178,10 +179,10 @@ func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(id)
|
||||
return lm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
func (lm *loggingMiddleware) DisconnectThingHandler(ctx context.Context, channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method disconnect_thing_handler for channel %s and thing %s took %s to complete", channelID, thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -191,5 +192,5 @@ func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
return lm.svc.DisconnectThingHandler(ctx, channelID, thingID)
|
||||
}
|
||||
|
||||
+28
-27
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +build !test
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
@@ -29,119 +30,119 @@ func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency m
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Add(token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) Add(ctx context.Context, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Add(token, cfg)
|
||||
return mm.svc.Add(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) View(token, id string) (saved bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) View(ctx context.Context, token, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.View(token, id)
|
||||
return mm.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Update(token string, cfg bootstrap.Config) (err error) {
|
||||
func (mm *metricsMiddleware) Update(ctx context.Context, token string, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Update(token, cfg)
|
||||
return mm.svc.Update(ctx, token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, token, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
return mm.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateConnections(token, id string, connections []string) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(token, id, connections)
|
||||
return mm.svc.UpdateConnections(ctx, token, id, connections)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) List(token string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(token, filter, offset, limit)
|
||||
return mm.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Remove(token, id string) (err error) {
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(token, id)
|
||||
return mm.svc.Remove(ctx, token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ChangeState(token, id string, state bootstrap.State) (err error) {
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(token, id, state)
|
||||
return mm.svc.ChangeState(ctx, token, id, state)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(channel)
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(id)
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(id)
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(ctx context.Context, channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_thing_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_thing_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
return mm.svc.DisconnectThingHandler(ctx, channelID, thingID)
|
||||
}
|
||||
|
||||
+42
-33
@@ -3,7 +3,12 @@
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/bootstrap"
|
||||
import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
@@ -24,54 +29,58 @@ type addReq struct {
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" || req.ExternalKey == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
key string
|
||||
id string
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
key string
|
||||
token string
|
||||
thingID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
@@ -79,49 +88,49 @@ type updateCertReq struct {
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.thingID == "" {
|
||||
return bootstrap.ErrNotFound
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
key string
|
||||
token string
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.limit == 0 || req.limit > maxLimit {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -134,34 +143,34 @@ type bootstrapReq struct {
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
return apiutil.ErrBootstrapState
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -21,21 +22,21 @@ func TestAddReqValidation(t *testing.T) {
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,28 +54,28 @@ func TestAddReqValidation(t *testing.T) {
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
@@ -84,29 +85,29 @@ func TestEntityReqValidation(t *testing.T) {
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
@@ -117,27 +118,27 @@ func TestUpdateReqValidation(t *testing.T) {
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
thingID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
thingID: "thingID",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty thing key",
|
||||
key: "key",
|
||||
desc: "empty thing id",
|
||||
token: "token",
|
||||
thingID: "",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
thingID: tc.thingID,
|
||||
}
|
||||
|
||||
@@ -148,29 +149,29 @@ func TestUpdateCertReqValidation(t *testing.T) {
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
@@ -182,36 +183,36 @@ func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
key string
|
||||
token string
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "too large limit",
|
||||
key: "key",
|
||||
token: "token",
|
||||
offset: 0,
|
||||
limit: maxLimit + 1,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
limit: maxLimitSize + 1,
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "zero limit",
|
||||
key: "key",
|
||||
desc: "default limit",
|
||||
token: "token",
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
limit: defLimit,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
@@ -232,13 +233,13 @@ func TestBootstrapReqValidation(t *testing.T) {
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -256,37 +257,37 @@ func TestBootstrapReqValidation(t *testing.T) {
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
token: "token",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
key: "key",
|
||||
token: "token",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: apiutil.ErrBootstrapState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
@@ -121,7 +121,3 @@ func (res stateRes) Headers() map[string]string {
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+82
-105
@@ -6,37 +6,37 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxLimit = 100
|
||||
defaultLimit = 10
|
||||
contentType = "application/json"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidLimitParam = errors.New("invalid limit query param")
|
||||
errInvalidOffsetParam = errors.New("invalid offset query param")
|
||||
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
partialMatch = []string{"name"}
|
||||
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
partialMatch = []string{"name"}
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Handler {
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger logger.Logger) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
r := bone.New()
|
||||
|
||||
@@ -100,7 +100,7 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version("bootstrap"))
|
||||
r.GetFunc("/health", mainflux.Health("bootstrap"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
@@ -111,9 +111,9 @@ func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{token: r.Header.Get("Authorization")}
|
||||
req := addReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -124,10 +124,12 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
req := updateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -139,12 +141,11 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
thingID: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -155,33 +156,38 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
req := updateConnReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errors.ErrInvalidQueryParams
|
||||
}
|
||||
|
||||
offset, limit, err := parsePagePrams(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := parseFilter(q)
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: filter,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
filter: parseFilter(q),
|
||||
offset: o,
|
||||
limit: l,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -190,7 +196,7 @@ func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := bootstrapReq{
|
||||
id: bone.GetValue(r, "external_id"),
|
||||
key: r.Header.Get("Authorization"),
|
||||
key: apiutil.ExtractThingKey(r),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -201,10 +207,12 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
req := changeStateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -212,8 +220,8 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := entityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -248,77 +256,46 @@ func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interfac
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch errorVal := err.(type) {
|
||||
case errors.Error:
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
switch {
|
||||
case errors.Contains(errorVal, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errors.Contains(errorVal, errors.ErrInvalidQueryParams):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken,
|
||||
err == apiutil.ErrBearerKey:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errors.Contains(err, errors.ErrInvalidQueryParams),
|
||||
errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrBootstrapState,
|
||||
err == apiutil.ErrLimitSize:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errors.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, bootstrap.ErrExternalKey),
|
||||
errors.Contains(err, bootstrap.ErrExternalKeySecure),
|
||||
errors.Contains(err, errors.ErrAuthorization):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, bootstrap.ErrThings):
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
case errors.Contains(err, errors.ErrCreateEntity),
|
||||
errors.Contains(err, errors.ErrUpdateEntity),
|
||||
errors.Contains(err, errors.ErrViewEntity),
|
||||
errors.Contains(err, errors.ErrRemoveEntity):
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
case errors.Contains(errorVal, bootstrap.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, bootstrap.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(errorVal, bootstrap.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(errorVal, bootstrap.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(errorVal, bootstrap.ErrThings):
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
case errors.Contains(errorVal, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if errorVal.Msg() != "" {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func parseUint(s string) (uint64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
ret, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.ErrInvalidQueryParams
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func parsePagePrams(q url.Values) (uint64, uint64, error) {
|
||||
offset, err := parseUint(q.Get("offset"))
|
||||
q.Del("offset")
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(errInvalidOffsetParam, err)
|
||||
}
|
||||
|
||||
limit, err := parseUint(q.Get("limit"))
|
||||
q.Del("limit")
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(errInvalidLimitParam, err)
|
||||
}
|
||||
|
||||
if limit > maxLimit {
|
||||
limit = maxLimit
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = defaultLimit
|
||||
}
|
||||
|
||||
return offset, limit, nil
|
||||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
|
||||
+11
-10
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,7 +41,7 @@ func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []str
|
||||
|
||||
for _, v := range crm.configs {
|
||||
if v.MFThing == config.MFThing || v.ExternalID == config.ExternalID {
|
||||
return "", bootstrap.ErrConflict
|
||||
return "", errors.ErrConflict
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +70,10 @@ func (crm *configRepositoryMock) RetrieveByID(token, id string) (bootstrap.Confi
|
||||
|
||||
c, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
return bootstrap.Config{}, errors.ErrNotFound
|
||||
}
|
||||
if c.Owner != token {
|
||||
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
|
||||
return bootstrap.Config{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return c, nil
|
||||
@@ -137,7 +138,7 @@ func (crm *configRepositoryMock) RetrieveByExternalID(externalID string) (bootst
|
||||
}
|
||||
}
|
||||
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
return bootstrap.Config{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
|
||||
@@ -146,7 +147,7 @@ func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
|
||||
|
||||
cfg, ok := crm.configs[config.MFThing]
|
||||
if !ok || cfg.Owner != config.Owner {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
cfg.Name = config.Name
|
||||
@@ -167,7 +168,7 @@ func (crm *configRepositoryMock) UpdateCert(owner, thingID, clientCert, clientKe
|
||||
}
|
||||
}
|
||||
if _, ok := crm.configs[forUpdate.MFThing]; !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
forUpdate.ClientCert = clientCert
|
||||
forUpdate.ClientKey = clientKey
|
||||
@@ -183,7 +184,7 @@ func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []
|
||||
|
||||
config, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
for _, ch := range channels {
|
||||
@@ -194,7 +195,7 @@ func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []
|
||||
for _, conn := range connections {
|
||||
ch, ok := crm.channels[conn]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
config.MFChannels = append(config.MFChannels, ch)
|
||||
}
|
||||
@@ -223,10 +224,10 @@ func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.S
|
||||
|
||||
config, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
if config.Owner != token {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
|
||||
config.State = state
|
||||
|
||||
+36
-28
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func (svc *mainfluxThings) CreateThings(_ context.Context, owner string, ths ...
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return []things.Thing{}, things.ErrUnauthorizedAccess
|
||||
return []things.Thing{}, errors.ErrAuthentication
|
||||
}
|
||||
for i := range ths {
|
||||
svc.counter++
|
||||
@@ -59,7 +60,7 @@ func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (thing
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, things.ErrUnauthorizedAccess
|
||||
return things.Thing{}, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.Email {
|
||||
@@ -67,7 +68,7 @@ func (svc *mainfluxThings) ViewThing(_ context.Context, owner, id string) (thing
|
||||
|
||||
}
|
||||
|
||||
return things.Thing{}, things.ErrNotFound
|
||||
return things.Thing{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs []string) error {
|
||||
@@ -76,11 +77,11 @@ func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
for _, chID := range chIDs {
|
||||
if svc.channels[chID].Owner != userID.Email {
|
||||
return things.ErrUnauthorizedAccess
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
for _, thID := range thIDs {
|
||||
svc.connections[chID] = append(svc.connections[chID], thID)
|
||||
@@ -90,35 +91,38 @@ func (svc *mainfluxThings) Connect(_ context.Context, owner string, chIDs, thIDs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Disconnect(_ context.Context, owner, chanID, thingID string) error {
|
||||
func (svc *mainfluxThings) Disconnect(_ context.Context, owner string, chIDs, thIDs []string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil || svc.channels[chanID].Owner != userID.Email {
|
||||
return things.ErrUnauthorizedAccess
|
||||
if err != nil {
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
|
||||
ids := svc.connections[chanID]
|
||||
i := 0
|
||||
for _, t := range ids {
|
||||
if t == thingID {
|
||||
break
|
||||
for _, chID := range chIDs {
|
||||
if svc.channels[chID].Owner != userID.Email {
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i == len(ids) {
|
||||
return things.ErrNotFound
|
||||
}
|
||||
ids := svc.connections[chID]
|
||||
var count int
|
||||
var newConns []string
|
||||
for _, thID := range thIDs {
|
||||
for _, id := range ids {
|
||||
if id == thID {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
newConns = append(newConns, id)
|
||||
}
|
||||
|
||||
var tmp []string
|
||||
if i != len(ids)-2 {
|
||||
tmp = ids[i+1:]
|
||||
if len(newConns)-len(ids) != count {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
svc.connections[chID] = newConns
|
||||
}
|
||||
}
|
||||
ids = append(ids[:i], tmp...)
|
||||
svc.connections[chanID] = ids
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -128,11 +132,11 @@ func (svc *mainfluxThings) RemoveThing(_ context.Context, owner, id string) erro
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
return errors.ErrAuthentication
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; !ok || t.Owner != userID.Email {
|
||||
return things.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
delete(svc.things, id)
|
||||
@@ -156,7 +160,7 @@ func (svc *mainfluxThings) ViewChannel(_ context.Context, owner, id string) (thi
|
||||
if c, ok := svc.channels[id]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return things.Channel{}, things.ErrNotFound
|
||||
return things.Channel{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateThing(context.Context, string, things.Thing) error {
|
||||
@@ -185,7 +189,7 @@ func (svc *mainfluxThings) CreateChannels(_ context.Context, owner string, chs .
|
||||
|
||||
userID, err := svc.auth.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return []things.Channel{}, things.ErrUnauthorizedAccess
|
||||
return []things.Channel{}, errors.ErrAuthentication
|
||||
}
|
||||
for i := range chs {
|
||||
svc.counter++
|
||||
@@ -225,6 +229,10 @@ func (svc *mainfluxThings) Identify(context.Context, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ShareThing(ctx context.Context, token, thingID string, actions, userIDs []string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func findIndex(list []string, val string) int {
|
||||
for i, v := range list {
|
||||
if v == val {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -18,8 +18,8 @@ type serviceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.AuthServiceClient {
|
||||
// NewAuthClient creates mock of users service.
|
||||
func NewAuthClient(users map[string]string) mainflux.AuthServiceClient {
|
||||
return &serviceMock{users}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (svc serviceMock) Identify(ctx context.Context, in *mainflux.Token, opts ..
|
||||
if id, ok := svc.users[in.Value]; ok {
|
||||
return &mainflux.UserIdentity{Email: id, Id: id}, nil
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
return nil, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ...grpc.CallOption) (*mainflux.Token, error) {
|
||||
@@ -37,13 +37,25 @@ func (svc serviceMock) Issue(ctx context.Context, in *mainflux.IssueReq, opts ..
|
||||
return &mainflux.Token{Value: id}, nil
|
||||
}
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
return nil, errors.ErrAuthentication
|
||||
}
|
||||
|
||||
func (svc serviceMock) Authorize(ctx context.Context, req *mainflux.AuthorizeReq, _ ...grpc.CallOption) (r *mainflux.AuthorizeRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) AddPolicy(ctx context.Context, in *mainflux.AddPolicyReq, opts ...grpc.CallOption) (*mainflux.AddPolicyRes, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) DeletePolicy(ctx context.Context, in *mainflux.DeletePolicyReq, opts ...grpc.CallOption) (*mainflux.DeletePolicyRes, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) ListPolicies(ctx context.Context, in *mainflux.ListPoliciesReq, opts ...grpc.CallOption) (*mainflux.ListPoliciesRes, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc serviceMock) Members(ctx context.Context, req *mainflux.MembersReq, _ ...grpc.CallOption) (r *mainflux.MembersRes, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
const (
|
||||
duplicateErr = "unique_violation"
|
||||
uuidErr = "invalid input syntax for type uuid"
|
||||
connConstraintErr = "connections_config_id_fkey"
|
||||
fkViolation = "foreign_key_violation"
|
||||
connConstraintErr = "connections_config_id_config_owner_fkey"
|
||||
configFieldsNum = 8
|
||||
chanFieldsNum = 3
|
||||
connFieldsNum = 2
|
||||
@@ -29,17 +29,11 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveDB = errors.New("failed to save bootstrap configuration to database")
|
||||
errMarshalChannel = errors.New("failed to marshal channel into json")
|
||||
errUnmarshalChannel = errors.New("failed to unmarshal json to channel")
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errRetrieve = errors.New("failed to retreive bootstrap configuration from database")
|
||||
errUpdate = errors.New("failed to update bootstrap configuration in database")
|
||||
errRemove = errors.New("failed to remove bootstrap configuration from database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
|
||||
)
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
@@ -61,7 +55,7 @@ func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (stri
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
return "", errors.Wrap(errors.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
dbcfg := toDBConfig(cfg)
|
||||
@@ -69,28 +63,26 @@ func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (stri
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = bootstrap.ErrConflict
|
||||
e = errors.ErrConflict
|
||||
}
|
||||
|
||||
cr.rollback("Failed to insert a Config", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
cr.rollback("Failed to insert a Config", tx)
|
||||
return "", errors.Wrap(errors.ErrCreateEntity, e)
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels", tx, err)
|
||||
|
||||
cr.rollback("Failed to insert Channels", tx)
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(cfg, chsConnIDs, tx); err != nil {
|
||||
cr.rollback("Failed to insert connections", tx, err)
|
||||
|
||||
cr.rollback("Failed to insert connections", tx)
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
cr.rollback("Failed to commit Config save", tx)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return cfg.MFThing, nil
|
||||
@@ -109,10 +101,10 @@ func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, err
|
||||
if err := cr.db.QueryRowx(q, id, owner).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
return empty, errors.Wrap(errors.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
return empty, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
@@ -123,7 +115,7 @@ func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, err
|
||||
rows, err := cr.db.NamedQuery(q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -132,13 +124,13 @@ func (cr configRepository) RetrieveByID(owner, id string) (bootstrap.Config, err
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
dbch.Owner = nullString(dbcfg.Owner)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
@@ -206,9 +198,9 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
|
||||
if err := cr.db.QueryRowx(q, externalID).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
return empty, errors.Wrap(errors.ErrNotFound, err)
|
||||
}
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
return empty, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
@@ -219,7 +211,7 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
|
||||
rows, err := cr.db.NamedQuery(q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -228,13 +220,13 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
return bootstrap.Config{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
@@ -254,16 +246,16 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
|
||||
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdate, err)
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdate, err)
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -274,16 +266,16 @@ func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caC
|
||||
|
||||
res, err := cr.db.Exec(q, clientCert, clientKey, caCert, thingID, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -292,28 +284,27 @@ func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caC
|
||||
func (cr configRepository) UpdateConnections(owner, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if err := insertChannels(owner, channels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels during the update", tx, err)
|
||||
|
||||
return err
|
||||
cr.rollback("Failed to insert Channels during the update", tx)
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if err := updateConnections(owner, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
}
|
||||
cr.rollback("Failed to update connections during the update", tx, err)
|
||||
|
||||
return err
|
||||
cr.rollback("Failed to update connections during the update", tx)
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config update", tx, err)
|
||||
cr.rollback("Failed to commit Config update", tx)
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -322,7 +313,7 @@ func (cr configRepository) UpdateConnections(owner, id string, channels []bootst
|
||||
func (cr configRepository) Remove(owner, id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
if _, err := cr.db.Exec(q, id, owner); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
@@ -337,16 +328,16 @@ func (cr configRepository) ChangeState(owner, id string, state bootstrap.State)
|
||||
|
||||
res, err := cr.db.Exec(q, state, id, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -361,14 +352,14 @@ func (cr configRepository) ListExisting(owner string, ids []string) ([]bootstrap
|
||||
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
|
||||
rows, err := cr.db.Queryx(q, owner, pq.Array(ids))
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
return []bootstrap.Channel{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var dbch dbChannel
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
|
||||
return []bootstrap.Channel{}, err
|
||||
return []bootstrap.Channel{}, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
@@ -391,7 +382,7 @@ func (cr configRepository) RemoveThing(id string) error {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -399,7 +390,7 @@ func (cr configRepository) RemoveThing(id string) error {
|
||||
func (cr configRepository) UpdateChannel(c bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", c)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errors.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata WHERE mainflux_channel = :mainflux_channel`
|
||||
@@ -449,9 +440,7 @@ func (cr configRepository) retrieveAll(owner string, filter bootstrap.Filter) (s
|
||||
return fmt.Sprintf(template, f), params
|
||||
}
|
||||
|
||||
func (cr configRepository) rollback(content string, tx *sqlx.Tx, err error) {
|
||||
cr.log.Error(fmt.Sprintf("%s %s", content, err))
|
||||
|
||||
func (cr configRepository) rollback(content string, tx *sqlx.Tx) {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
|
||||
}
|
||||
@@ -476,7 +465,7 @@ func insertChannels(owner string, channels []bootstrap.Channel, tx *sqlx.Tx) err
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = bootstrap.ErrConflict
|
||||
e = errors.ErrConflict
|
||||
}
|
||||
return e
|
||||
}
|
||||
@@ -641,7 +630,7 @@ func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, errors.Wrap(errMarshalChannel, err)
|
||||
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
@@ -658,7 +647,7 @@ func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, errors.Wrap(errUnmarshalChannel, err)
|
||||
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
|
||||
@@ -26,8 +26,8 @@ var (
|
||||
ExternalKey: "external-key",
|
||||
Owner: "user@email.com",
|
||||
MFChannels: []bootstrap.Channel{
|
||||
bootstrap.Channel{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
|
||||
bootstrap.Channel{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
|
||||
{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
|
||||
{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
@@ -74,19 +74,19 @@ func TestSave(t *testing.T) {
|
||||
desc: "save config with same Thing ID",
|
||||
config: duplicateThing,
|
||||
connections: nil,
|
||||
err: bootstrap.ErrConflict,
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
connections: nil,
|
||||
err: bootstrap.ErrConflict,
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Channels",
|
||||
config: duplicateChannels,
|
||||
connections: channels,
|
||||
err: bootstrap.ErrConflict,
|
||||
err: errors.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -130,19 +130,19 @@ func TestRetrieveByID(t *testing.T) {
|
||||
desc: "retrieve config with wrong owner",
|
||||
owner: "2",
|
||||
id: id,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing config",
|
||||
owner: c.Owner,
|
||||
id: nonexistentConfID.String(),
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a config with invalid ID",
|
||||
owner: c.Owner,
|
||||
id: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -256,7 +256,7 @@ func TestRetrieveByExternalID(t *testing.T) {
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
@@ -301,7 +301,7 @@ func TestUpdate(t *testing.T) {
|
||||
{
|
||||
desc: "update with wrong owner",
|
||||
config: wrongOwner,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
@@ -353,7 +353,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
owner: "wrong",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
@@ -411,7 +411,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
@@ -467,7 +467,7 @@ func TestRemove(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(c.Owner, id)
|
||||
require.True(t, errors.Contains(err, bootstrap.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
|
||||
require.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, errors.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,13 +498,13 @@ func TestChangeState(t *testing.T) {
|
||||
desc: "change state with wrong owner",
|
||||
id: saved,
|
||||
owner: "2",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state with wrong id",
|
||||
id: "wrong",
|
||||
owner: c.Owner,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
|
||||
@@ -5,7 +5,6 @@ package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -28,7 +27,7 @@ var (
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
@@ -36,9 +35,9 @@ func TestMain(m *testing.M) {
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.8-alpine", cfg)
|
||||
container, err := pool.Run("postgres", "13.3-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
@@ -51,7 +50,7 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
@@ -67,7 +66,7 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
@@ -75,7 +74,7 @@ func TestMain(m *testing.M) {
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -39,7 +40,7 @@ func dec(in []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, bootstrap.ErrMalformedEntity
|
||||
return nil, errors.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
@@ -30,7 +31,7 @@ const (
|
||||
// Subscriber represents event source for things and channels provisioning.
|
||||
type Subscriber interface {
|
||||
// Subscribes to given subject and receives events.
|
||||
Subscribe(string) error
|
||||
Subscribe(context.Context, string) error
|
||||
}
|
||||
|
||||
type eventStore struct {
|
||||
@@ -50,14 +51,14 @@ func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string,
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Subscribe(subject string) error {
|
||||
err := es.client.XGroupCreateMkStream(stream, group, "$").Err()
|
||||
func (es eventStore) Subscribe(ctx context.Context, subject string) error {
|
||||
err := es.client.XGroupCreateMkStream(ctx, stream, group, "$").Err()
|
||||
if err != nil && err.Error() != exists {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
streams, err := es.client.XReadGroup(&redis.XReadGroupArgs{
|
||||
streams, err := es.client.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||||
Group: group,
|
||||
Consumer: es.consumer,
|
||||
Streams: []string{stream, ">"},
|
||||
@@ -74,22 +75,22 @@ func (es eventStore) Subscribe(subject string) error {
|
||||
switch event["operation"] {
|
||||
case thingRemove:
|
||||
rte := decodeRemoveThing(event)
|
||||
err = es.handleRemoveThing(rte)
|
||||
err = es.svc.RemoveConfigHandler(ctx, rte.id)
|
||||
case thingDisconnect:
|
||||
dte := decodeDisconnectThing(event)
|
||||
err = es.handleDisconnectThing(dte)
|
||||
err = es.svc.DisconnectThingHandler(ctx, dte.channelID, dte.thingID)
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(event)
|
||||
err = es.handleUpdateChannel(uce)
|
||||
err = es.handleUpdateChannel(ctx, uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(event)
|
||||
err = es.handleRemoveChannel(rce)
|
||||
err = es.svc.RemoveChannelHandler(ctx, rce.id)
|
||||
}
|
||||
if err != nil {
|
||||
es.logger.Warn(fmt.Sprintf("Failed to handle event sourcing: %s", err.Error()))
|
||||
break
|
||||
}
|
||||
es.client.XAck(stream, group, msg.ID)
|
||||
es.client.XAck(ctx, stream, group, msg.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,25 +128,13 @@ func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) handleRemoveThing(rte removeEvent) error {
|
||||
return es.svc.RemoveConfigHandler(rte.id)
|
||||
}
|
||||
|
||||
func (es eventStore) handleUpdateChannel(uce updateChannelEvent) error {
|
||||
func (es eventStore) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
}
|
||||
return es.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (es eventStore) handleRemoveChannel(rce removeEvent) error {
|
||||
return es.svc.RemoveChannelHandler(rce.id)
|
||||
}
|
||||
|
||||
func (es eventStore) handleDisconnectThing(dte disconnectEvent) error {
|
||||
return es.svc.DisconnectThingHandler(dte.channelID, dte.thingID)
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func read(event map[string]interface{}, key, def string) string {
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
@@ -38,7 +39,7 @@ func TestMain(m *testing.M) {
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
return redisClient.Ping().Err()
|
||||
return redisClient.Ping(context.Background()).Err()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
package producer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
@@ -31,8 +32,8 @@ func NewEventStoreMiddleware(svc bootstrap.Service, client *redis.Client) bootst
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(token, cfg)
|
||||
func (es eventStore) Add(ctx context.Context, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(ctx, token, cfg)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
@@ -52,17 +53,17 @@ func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
es.add(ctx, ev)
|
||||
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es eventStore) View(token, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(token, id)
|
||||
func (es eventStore) View(ctx context.Context, token, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(ctx, token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) Update(token string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(token, cfg); err != nil {
|
||||
func (es eventStore) Update(ctx context.Context, token string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(ctx, token, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -73,17 +74,17 @@ func (es eventStore) Update(token string, cfg bootstrap.Config) error {
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
es.add(ctx, ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
func (es eventStore) UpdateCert(ctx context.Context, token, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(ctx, token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateConnections(token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(token, id, connections); err != nil {
|
||||
func (es eventStore) UpdateConnections(ctx context.Context, token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(ctx, token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -93,17 +94,17 @@ func (es eventStore) UpdateConnections(token, id string, connections []string) e
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
es.add(ctx, ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) List(token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(token, filter, offset, limit)
|
||||
func (es eventStore) List(ctx context.Context, token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(ctx, token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (es eventStore) Remove(token, id string) error {
|
||||
if err := es.svc.Remove(token, id); err != nil {
|
||||
func (es eventStore) Remove(ctx context.Context, token, id string) error {
|
||||
if err := es.svc.Remove(ctx, token, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -112,13 +113,13 @@ func (es eventStore) Remove(token, id string) error {
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
es.add(ctx, ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(externalKey, externalID, secure)
|
||||
func (es eventStore) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
externalID: externalID,
|
||||
@@ -130,13 +131,13 @@ func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (boo
|
||||
ev.success = false
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
es.add(ctx, ev)
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es eventStore) ChangeState(token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(token, id, state); err != nil {
|
||||
func (es eventStore) ChangeState(ctx context.Context, token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(ctx, token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -146,33 +147,33 @@ func (es eventStore) ChangeState(token, id string, state bootstrap.State) error
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
es.add(ev)
|
||||
es.add(ctx, ev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveConfigHandler(id string) error {
|
||||
return es.svc.RemoveConfigHandler(id)
|
||||
func (es eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
return es.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveChannelHandler(id string) error {
|
||||
return es.svc.RemoveChannelHandler(id)
|
||||
func (es eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
return es.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateChannelHandler(channel bootstrap.Channel) error {
|
||||
return es.UpdateChannelHandler(channel)
|
||||
func (es eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (es eventStore) DisconnectThingHandler(channelID, thingID string) error {
|
||||
return es.svc.DisconnectThingHandler(channelID, thingID)
|
||||
func (es eventStore) DisconnectThingHandler(ctx context.Context, channelID, thingID string) error {
|
||||
return es.svc.DisconnectThingHandler(ctx, channelID, thingID)
|
||||
}
|
||||
|
||||
func (es eventStore) add(ev event) error {
|
||||
func (es eventStore) add(ctx context.Context, ev event) error {
|
||||
record := &redis.XAddArgs{
|
||||
Stream: streamID,
|
||||
MaxLenApprox: streamLen,
|
||||
Values: ev.encode(),
|
||||
}
|
||||
|
||||
return es.client.XAdd(record).Err()
|
||||
return es.client.XAdd(ctx, record).Err()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
@@ -11,8 +12,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
|
||||
@@ -64,7 +66,7 @@ var (
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
ThingsURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
@@ -86,12 +88,13 @@ func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc)
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
func TestAdd(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
@@ -132,17 +135,17 @@ func TestAdd(t *testing.T) {
|
||||
desc: "create invalid config",
|
||||
config: invalidConfig,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.token, tc.config)
|
||||
_, err := svc.Add(context.Background(), tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
@@ -160,26 +163,26 @@ func TestAdd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
svcConfig, svcErr := svc.View(validToken, saved.MFThing)
|
||||
svcConfig, svcErr := svc.View(context.Background(), validToken, saved.MFThing)
|
||||
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
esConfig, esErr := svc.View(validToken, saved.MFThing)
|
||||
esConfig, esErr := svc.View(context.Background(), validToken, saved.MFThing)
|
||||
|
||||
assert.Equal(t, svcConfig, esConfig, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfig, esConfig))
|
||||
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
@@ -189,9 +192,9 @@ func TestUpdate(t *testing.T) {
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
modified := saved
|
||||
modified.Content = "new-config"
|
||||
@@ -224,17 +227,17 @@ func TestUpdate(t *testing.T) {
|
||||
desc: "update non-existing config",
|
||||
config: nonExisting,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.token, tc.config)
|
||||
err := svc.Update(context.Background(), tc.token, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
@@ -252,16 +255,16 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -289,17 +292,17 @@ func TestUpdateConnections(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
connections: []string{"256"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
|
||||
err := svc.UpdateConnections(context.Background(), tc.token, tc.id, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
@@ -316,37 +319,37 @@ func TestUpdateConnections(t *testing.T) {
|
||||
}
|
||||
}
|
||||
func TestList(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
_, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
offset := uint64(0)
|
||||
limit := uint64(10)
|
||||
svcConfigs, svcErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
|
||||
svcConfigs, svcErr := svc.List(context.Background(), validToken, bootstrap.Filter{}, offset, limit)
|
||||
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
esConfigs, esErr := svc.List(validToken, bootstrap.Filter{}, offset, limit)
|
||||
esConfigs, esErr := svc.List(context.Background(), validToken, bootstrap.Filter{}, offset, limit)
|
||||
|
||||
assert.Equal(t, svcConfigs, esConfigs, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcConfigs, esConfigs))
|
||||
assert.Equal(t, svcErr, esErr, fmt.Sprintf("event sourcing changed service behavior: expected %v got %v", svcErr, esErr))
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -370,17 +373,17 @@ func TestRemove(t *testing.T) {
|
||||
desc: "remove config with invalid credentials",
|
||||
id: saved.MFThing,
|
||||
token: "",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.token, tc.id)
|
||||
err := svc.Remove(context.Background(), tc.token, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
@@ -398,18 +401,18 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -434,7 +437,7 @@ func TestBootstrap(t *testing.T) {
|
||||
desc: "bootstrap with an error",
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "external",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: bootstrap.ErrExternalKey,
|
||||
event: map[string]interface{}{
|
||||
"external_id": saved.ExternalID,
|
||||
"success": "0",
|
||||
@@ -446,10 +449,10 @@ func TestBootstrap(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Bootstrap(tc.externalKey, tc.externalID, false)
|
||||
_, err := svc.Bootstrap(context.Background(), tc.externalKey, tc.externalID, false)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
@@ -467,18 +470,18 @@ func TestBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
svc = producer.NewEventStoreMiddleware(svc, redisClient)
|
||||
|
||||
c := config
|
||||
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
redisClient.FlushAll().Err()
|
||||
redisClient.FlushAll(context.Background()).Err()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -506,17 +509,17 @@ func TestChangeState(t *testing.T) {
|
||||
id: saved.MFThing,
|
||||
token: "",
|
||||
state: bootstrap.Inactive,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
event: nil,
|
||||
},
|
||||
}
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.token, tc.id, tc.state)
|
||||
err := svc.ChangeState(context.Background(), tc.token, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
streams := redisClient.XRead(context.Background(), &redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
Count: 1,
|
||||
Block: time.Second,
|
||||
|
||||
+39
-52
@@ -16,28 +16,15 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound = errors.New("non-existent entity")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification.
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrConflict indicates that entity with the same ID or external ID already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
// ErrThings indicates failure to communicate with Mainflux Things service.
|
||||
// It can be due to networking error or invalid/unauthorized request.
|
||||
// It can be due to networking error or invalid/unauthenticated request.
|
||||
ErrThings = errors.New("failed to receive response from Things service")
|
||||
|
||||
// ErrExternalKeyNotFound indicates a non-existent bootstrap configuration for given external key
|
||||
ErrExternalKeyNotFound = errors.New("failed to get bootstrap configuration for given external key")
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key
|
||||
ErrExternalKey = errors.New("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrSecureBootstrap indicates error in getting bootstrap configuration for given encrypted external key
|
||||
ErrSecureBootstrap = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key
|
||||
ErrExternalKeySecure = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
@@ -63,48 +50,48 @@ var _ Service = (*bootstrapService)(nil)
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Add adds new Thing Config to the user identified by the provided token.
|
||||
Add(token string, cfg Config) (Config, error)
|
||||
Add(ctx context.Context, token string, cfg Config) (Config, error)
|
||||
|
||||
// View returns Thing Config with given ID belonging to the user identified by the given token.
|
||||
View(token, id string) (Config, error)
|
||||
View(ctx context.Context, token, id string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(token string, cfg Config) error
|
||||
Update(ctx context.Context, token string, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(token, thingID, clientCert, clientKey, caCert string) error
|
||||
UpdateCert(ctx context.Context, token, thingID, clientCert, clientKey, caCert string) error
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(token, id string, connections []string) error
|
||||
UpdateConnections(ctx context.Context, token, id string, connections []string) error
|
||||
|
||||
// List returns subset of Configs with given search params that belong to the
|
||||
// user identified by the given token.
|
||||
List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
List(ctx context.Context, token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
Remove(token, id string) error
|
||||
Remove(ctx context.Context, token, id string) error
|
||||
|
||||
// Bootstrap returns Config to the Thing with provided external ID using external key.
|
||||
Bootstrap(externalKey, externalID string, secure bool) (Config, error)
|
||||
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Thing with given ID and owner.
|
||||
ChangeState(token, id string, state State) error
|
||||
ChangeState(ctx context.Context, token, id string, state State) error
|
||||
|
||||
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
|
||||
// handlers for events. That's why these methods surpass ownership check.
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(channel Channel) error
|
||||
UpdateChannelHandler(ctx context.Context, channel Channel) error
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(id string) error
|
||||
RemoveConfigHandler(ctx context.Context, id string) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(id string) error
|
||||
RemoveChannelHandler(ctx context.Context, id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
|
||||
DisconnectThingHandler(channelID, thingID string) error
|
||||
DisconnectThingHandler(ctx context.Context, channelID, thingID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
@@ -133,7 +120,7 @@ func New(auth mainflux.AuthServiceClient, configs ConfigRepository, sdk mfsdk.SD
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
|
||||
func (bs bootstrapService) Add(ctx context.Context, token string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
@@ -180,7 +167,7 @@ func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(token, id string) (Config, error) {
|
||||
func (bs bootstrapService) View(ctx context.Context, token, id string) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
@@ -189,7 +176,7 @@ func (bs bootstrapService) View(token, id string) (Config, error) {
|
||||
return bs.configs.RetrieveByID(owner, id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(token string, cfg Config) error {
|
||||
func (bs bootstrapService) Update(ctx context.Context, token string, cfg Config) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -200,7 +187,7 @@ func (bs bootstrapService) Update(token string, cfg Config) error {
|
||||
return bs.configs.Update(cfg)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caCert string) error {
|
||||
func (bs bootstrapService) UpdateCert(ctx context.Context, token, thingID, clientCert, clientKey, caCert string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -211,7 +198,7 @@ func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(token, id string, connections []string) error {
|
||||
func (bs bootstrapService) UpdateConnections(ctx context.Context, token, id string, connections []string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -259,7 +246,7 @@ func (bs bootstrapService) UpdateConnections(token, id string, connections []str
|
||||
}
|
||||
if err := bs.sdk.Connect(conIDs, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedConnect) {
|
||||
return ErrMalformedEntity
|
||||
return errors.ErrMalformedEntity
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
@@ -268,7 +255,7 @@ func (bs bootstrapService) UpdateConnections(token, id string, connections []str
|
||||
return bs.configs.UpdateConnections(owner, id, channels, connections)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
func (bs bootstrapService) List(ctx context.Context, token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return ConfigsPage{}, err
|
||||
@@ -277,7 +264,7 @@ func (bs bootstrapService) List(token string, filter Filter, offset, limit uint6
|
||||
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Remove(token, id string) error {
|
||||
func (bs bootstrapService) Remove(ctx context.Context, token, id string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -288,7 +275,7 @@ func (bs bootstrapService) Remove(token, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
|
||||
func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalID)
|
||||
if err != nil {
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
@@ -297,19 +284,19 @@ func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrSecureBootstrap, err)
|
||||
return Config{}, errors.Wrap(ErrExternalKeySecure, err)
|
||||
}
|
||||
externalKey = dec
|
||||
}
|
||||
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, errors.Wrap(ErrExternalKeyNotFound, ErrNotFound)
|
||||
return Config{}, ErrExternalKey
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(token, id string, state State) error {
|
||||
func (bs bootstrapService) ChangeState(ctx context.Context, token, id string, state State) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -351,28 +338,28 @@ func (bs bootstrapService) ChangeState(token, id string, state State) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
|
||||
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
|
||||
if err := bs.configs.UpdateChannel(channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(id string) error {
|
||||
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveThing(id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(id string) error {
|
||||
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveChannel(id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
|
||||
func (bs bootstrapService) DisconnectThingHandler(ctx context.Context, channelID, thingID string) error {
|
||||
if err := bs.configs.DisconnectThing(channelID, thingID); err != nil {
|
||||
return errors.Wrap(errDisconnectThing, err)
|
||||
}
|
||||
@@ -385,7 +372,7 @@ func (bs bootstrapService) identify(token string) (string, error) {
|
||||
|
||||
res, err := bs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
return "", errors.ErrAuthentication
|
||||
}
|
||||
|
||||
return res.GetEmail(), nil
|
||||
@@ -406,7 +393,7 @@ func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
|
||||
thing, err := bs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedFetch) {
|
||||
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, ErrNotFound)
|
||||
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, errors.ErrNotFound)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
@@ -437,7 +424,7 @@ func (bs bootstrapService) connectionChannels(channels, existing []string, token
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(id, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
@@ -490,14 +477,14 @@ func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
func (bs bootstrapService) dec(in string) (string, error) {
|
||||
ciphertext, err := hex.DecodeString(in)
|
||||
if err != nil {
|
||||
return "", ErrNotFound
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", ErrMalformedEntity
|
||||
return "", err
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
+65
-62
@@ -4,6 +4,7 @@
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
@@ -56,7 +58,7 @@ var (
|
||||
func newService(auth mainflux.AuthServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
ThingsURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
@@ -78,7 +80,8 @@ func newThingsService(auth mainflux.AuthServiceClient) things.Service {
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc)
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
@@ -98,7 +101,7 @@ func enc(in []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
@@ -127,35 +130,35 @@ func TestAdd(t *testing.T) {
|
||||
desc: "add a config with an invalid ID",
|
||||
config: neID,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "add a config with wrong credentials",
|
||||
config: config,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "add a config with invalid list of channels",
|
||||
config: wrongChannels,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.token, tc.config)
|
||||
_, err := svc.Add(context.Background(), tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -174,24 +177,24 @@ func TestView(t *testing.T) {
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.View(tc.token, tc.id)
|
||||
_, err := svc.View(context.Background(), tc.token, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
@@ -200,7 +203,7 @@ func TestUpdate(t *testing.T) {
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
modifiedCreated := saved
|
||||
@@ -226,24 +229,24 @@ func TestUpdate(t *testing.T) {
|
||||
desc: "update a non-existing config",
|
||||
config: nonExisting,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config with wrong credentials",
|
||||
config: saved,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.token, tc.config)
|
||||
err := svc.Update(context.Background(), tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
@@ -252,7 +255,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
saved, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -281,7 +284,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
caCert: "newCert",
|
||||
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update config cert with wrong credentials",
|
||||
@@ -290,18 +293,18 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateCert(tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
err := svc.UpdateCert(context.Background(), tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
@@ -310,15 +313,15 @@ func TestUpdateConnections(t *testing.T) {
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
created, err := svc.Add(validToken, c)
|
||||
created, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
externalID, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = externalID.String()
|
||||
active, err := svc.Add(validToken, c)
|
||||
active, err := svc.Add(context.Background(), validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
err = svc.ChangeState(validToken, active.MFThing, bootstrap.Active)
|
||||
err = svc.ChangeState(context.Background(), validToken, active.MFThing, bootstrap.Active)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
|
||||
|
||||
nonExisting := config
|
||||
@@ -350,32 +353,32 @@ func TestUpdateConnections(t *testing.T) {
|
||||
token: validToken,
|
||||
id: "",
|
||||
connections: []string{"3"},
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections with invalid channels",
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"wrong"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update connections a config with wrong credentials",
|
||||
token: invalidToken,
|
||||
id: created.MFKey,
|
||||
connections: []string{"2", "3"},
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
|
||||
err := svc.UpdateConnections(context.Background(), tc.token, tc.id, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
@@ -389,12 +392,12 @@ func TestList(t *testing.T) {
|
||||
c.ExternalID = id.String()
|
||||
c.ExternalKey = id.String()
|
||||
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
|
||||
s, err := svc.Add(validToken, c)
|
||||
s, err := svc.Add(context.Background(), validToken, c)
|
||||
saved = append(saved, s)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
// Set one Thing to the different state
|
||||
err := svc.ChangeState(validToken, "42", bootstrap.Active)
|
||||
err := svc.ChangeState(context.Background(), validToken, "42", bootstrap.Active)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
|
||||
saved[41].State = bootstrap.Active
|
||||
|
||||
@@ -436,13 +439,13 @@ func TestList(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs unauthorized",
|
||||
desc: "list configs with invalid token",
|
||||
config: bootstrap.ConfigsPage{},
|
||||
filter: bootstrap.Filter{},
|
||||
token: invalidToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list last page",
|
||||
@@ -475,7 +478,7 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
result, err := svc.List(tc.token, tc.filter, tc.offset, tc.limit)
|
||||
result, err := svc.List(context.Background(), tc.token, tc.filter, tc.offset, tc.limit)
|
||||
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
|
||||
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
@@ -483,12 +486,12 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -501,7 +504,7 @@ func TestRemove(t *testing.T) {
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "remove an existing config",
|
||||
@@ -524,18 +527,18 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.token, tc.id)
|
||||
err := svc.Remove(context.Background(), tc.token, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
e, err := enc([]byte(saved.ExternalKey))
|
||||
@@ -554,7 +557,7 @@ func TestBootstrap(t *testing.T) {
|
||||
config: bootstrap.Config{},
|
||||
externalID: "invalid",
|
||||
externalKey: saved.ExternalKey,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
@@ -562,7 +565,7 @@ func TestBootstrap(t *testing.T) {
|
||||
config: bootstrap.Config{},
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: bootstrap.ErrExternalKey,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
@@ -584,19 +587,19 @@ func TestBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
config, err := svc.Bootstrap(tc.externalKey, tc.externalID, tc.encrypted)
|
||||
config, err := svc.Bootstrap(context.Background(), tc.externalKey, tc.externalID, tc.encrypted)
|
||||
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -611,14 +614,14 @@ func TestChangeState(t *testing.T) {
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "change state of non-existing config",
|
||||
state: bootstrap.Active,
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
@@ -644,18 +647,18 @@ func TestChangeState(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.token, tc.id, tc.state)
|
||||
err := svc.ChangeState(context.Background(), tc.token, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
_, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
ch := bootstrap.Channel{
|
||||
ID: channel.ID,
|
||||
@@ -681,18 +684,18 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateChannelHandler(tc.channel)
|
||||
err := svc.UpdateChannelHandler(context.Background(), tc.channel)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannelHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
_, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -713,18 +716,18 @@ func TestRemoveChannelHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveChannelHandler(tc.id)
|
||||
err := svc.RemoveChannelHandler(context.Background(), tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -745,18 +748,18 @@ func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveConfigHandler(tc.id)
|
||||
err := svc.RemoveConfigHandler(context.Background(), tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectThingsHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
users := mocks.NewAuthClient(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
saved, err := svc.Add(context.Background(), validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -780,7 +783,7 @@ func TestDisconnectThingsHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
|
||||
err := svc.DisconnectThingHandler(context.Background(), tc.channelID, tc.thingID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -2,8 +2,8 @@
|
||||
Issues certificates for things. `Certs` service can create certificates to be used when `Mainflux` is deployed to support mTLS.
|
||||
Certificate service can create certificates in two modes:
|
||||
1. Development mode - to be used when no PKI is deployed, this works similar to the [make thing_cert](../docker/ssl/Makefile)
|
||||
2. PKI mode - certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
|
||||
|
||||
2. PKI mode - certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
|
||||
|
||||
## Development mode
|
||||
If `MF_CERTS_VAULT_HOST` is empty than Development mode is on.
|
||||
|
||||
@@ -12,7 +12,7 @@ To issue a certificate:
|
||||
|
||||
TOK=`curl -s --insecure -S -X POST http://localhost/tokens -H 'Content-Type: application/json' -d '{"email":"edge@email.com","password":"12345678"}' | jq -r '.token'`
|
||||
|
||||
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "key_bits":2048, "key_type":"rsa"}'
|
||||
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "key_bits":2048, "key_type":"rsa"}'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -31,7 +31,7 @@ curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Co
|
||||
## PKI mode
|
||||
|
||||
When `MF_CERTS_VAULT_HOST` is set it is presumed that `Vault` is installed and `certs` service will issue certificates using `Vault` API.
|
||||
First you'll need to set up `Vault`.
|
||||
First you'll need to set up `Vault`.
|
||||
To setup `Vault` follow steps in [Build Your Own Certificate Authority (CA)](https://learn.hashicorp.com/tutorials/vault/pki-engine).
|
||||
|
||||
To setup certs service with `Vault` following environment variables must be set:
|
||||
@@ -49,5 +49,5 @@ Issuing certificate is same as in **Development** mode.
|
||||
In this mode certificates can also be revoked:
|
||||
|
||||
```bash
|
||||
curl -s -S -X DELETE http://localhost:8204/certs/revoke -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":"c30b8842-507c-4bcd-973c-74008cef3be5"}'
|
||||
curl -s -S -X DELETE http://localhost:8204/certs/revoke -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"thing_id":"c30b8842-507c-4bcd-973c-74008cef3be5"}'
|
||||
```
|
||||
|
||||
+58
-5
@@ -16,16 +16,17 @@ func issueCert(svc certs.Service) endpoint.Endpoint {
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.Valid, req.KeyBits, req.KeyType)
|
||||
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.TTL, req.KeyBits, req.KeyType)
|
||||
if err != nil {
|
||||
return certsRes{}, err
|
||||
}
|
||||
|
||||
return certsRes{
|
||||
CertSerial: res.Serial,
|
||||
ThingID: res.ThingID,
|
||||
CertKey: res.ClientKey,
|
||||
Cert: res.ClientCert,
|
||||
CACert: res.IssuingCA,
|
||||
Expiration: res.Expire,
|
||||
created: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -54,9 +55,8 @@ func listCerts(svc certs.Service) endpoint.Endpoint {
|
||||
view := certsRes{
|
||||
CertSerial: cert.Serial,
|
||||
ThingID: cert.ThingID,
|
||||
CertKey: cert.ClientKey,
|
||||
Cert: cert.ClientCert,
|
||||
CACert: cert.IssuingCA,
|
||||
Expiration: cert.Expire,
|
||||
}
|
||||
res.Certs = append(res.Certs, view)
|
||||
}
|
||||
@@ -64,6 +64,59 @@ func listCerts(svc certs.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func listSerials(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.ListSerials(ctx, req.token, req.thingID, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return certsPageRes{}, err
|
||||
}
|
||||
res := certsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Certs: []certsRes{},
|
||||
}
|
||||
|
||||
for _, cert := range page.Certs {
|
||||
cr := certsRes{
|
||||
CertSerial: cert.Serial,
|
||||
}
|
||||
res.Certs = append(res.Certs, cr)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := svc.ViewCert(ctx, req.token, req.serialID)
|
||||
if err != nil {
|
||||
return certsPageRes{}, err
|
||||
}
|
||||
|
||||
certRes := certsRes{
|
||||
CertSerial: cert.Serial,
|
||||
ThingID: cert.ThingID,
|
||||
Cert: cert.ClientCert,
|
||||
Expiration: cert.Expire,
|
||||
}
|
||||
|
||||
return certRes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(revokeReq)
|
||||
|
||||
+30
-2
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
@@ -24,7 +26,7 @@ func NewLoggingMiddleware(svc certs.Service, logger log.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (c certs.Cert, err error) {
|
||||
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, ttl string, keyBits int, keyType string) (c certs.Cert, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method issue_cert for token: %s and thing: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -34,7 +36,7 @@ func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, days
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
return lm.svc.IssueCert(ctx, token, thingID, ttl, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (cp certs.Page, err error) {
|
||||
@@ -50,6 +52,32 @@ func (lm *loggingMiddleware) ListCerts(ctx context.Context, token, thingID strin
|
||||
return lm.svc.ListCerts(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (cp certs.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_serials for token: %s and thing id: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListSerials(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewCert(ctx context.Context, token, serialID string) (c certs.Cert, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_cert for token: %s and serial id %s took %s to complete", token, serialID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewCert(ctx, token, serialID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RevokeCert(ctx context.Context, token, thingID string) (c certs.Revoke, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke_cert for token: %s and thing: %s took %s to complete", token, thingID, time.Since(begin))
|
||||
|
||||
+22
-2
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
@@ -28,13 +30,13 @@ func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metri
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (certs.Cert, error) {
|
||||
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, ttl string, keyBits int, keyType string) (certs.Cert, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_cert").Add(1)
|
||||
ms.latency.With("method", "issue_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
return ms.svc.IssueCert(ctx, token, thingID, ttl, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
@@ -46,6 +48,24 @@ func (ms *metricsMiddleware) ListCerts(ctx context.Context, token, thingID strin
|
||||
return ms.svc.ListCerts(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_serials").Add(1)
|
||||
ms.latency.With("method", "list_serials").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListSerials(ctx, token, thingID, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewCert(ctx context.Context, token, serialID string) (certs.Cert, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_cert").Add(1)
|
||||
ms.latency.With("method", "view_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewCert(ctx, token, serialID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RevokeCert(ctx context.Context, token, thingID string) (certs.Revoke, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_cert").Add(1)
|
||||
|
||||
+38
-9
@@ -3,7 +3,7 @@
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/certs"
|
||||
import "github.com/mainflux/mainflux/internal/apiutil"
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
@@ -12,13 +12,22 @@ type addCertsReq struct {
|
||||
ThingID string `json:"thing_id"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
Valid string `json:"valid"`
|
||||
TTL string `json:"ttl"`
|
||||
}
|
||||
|
||||
func (req addCertsReq) validate() error {
|
||||
if req.ThingID == "" && req.token == "" {
|
||||
return errUnauthorized
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ThingID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.TTL == "" || req.KeyType == "" || req.KeyBits == 0 {
|
||||
return apiutil.ErrMissingCertData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,22 +40,42 @@ type listReq struct {
|
||||
|
||||
func (req *listReq) validate() error {
|
||||
if req.token == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.limit == 0 || req.limit > maxLimitSize {
|
||||
return certs.ErrMalformedEntity
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewReq struct {
|
||||
serialID string
|
||||
token string
|
||||
}
|
||||
|
||||
func (req *viewReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.serialID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeReq struct {
|
||||
token string
|
||||
certID string
|
||||
}
|
||||
|
||||
func (req *revokeReq) validate() error {
|
||||
if req.token == "" || req.certID == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.certID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+12
-7
@@ -5,6 +5,7 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
@@ -19,15 +20,15 @@ type certsPageRes struct {
|
||||
}
|
||||
|
||||
type certsRes struct {
|
||||
ThingID string `json:"thing_id"`
|
||||
Cert string `json:"cert"`
|
||||
CertKey string `json:"cert_key"`
|
||||
CertSerial string `json:"cert_serial"`
|
||||
CACert string `json:"ca_cert"`
|
||||
ThingID string `json:"thing_id"`
|
||||
Cert string `json:"cert"`
|
||||
CertSerial string `json:"cert_serial"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res certsPageRes) Code() int {
|
||||
return http.StatusCreated
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res certsPageRes) Headers() map[string]string {
|
||||
@@ -39,7 +40,11 @@ func (res certsPageRes) Empty() bool {
|
||||
}
|
||||
|
||||
func (res certsRes) Code() int {
|
||||
return http.StatusCreated
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res certsRes) Headers() map[string]string {
|
||||
|
||||
+52
-31
@@ -6,14 +6,14 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/internal/httputil"
|
||||
"github.com/mainflux/mainflux/internal/apiutil"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -26,15 +26,10 @@ const (
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errUnauthorized = errors.New("missing or invalid credentials provided")
|
||||
errConflict = errors.New("entity already exists")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc certs.Service) http.Handler {
|
||||
func MakeHandler(svc certs.Service, logger logger.Logger) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
@@ -46,9 +41,9 @@ func MakeHandler(svc certs.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/certs/:thingId", kithttp.NewServer(
|
||||
listCerts(svc),
|
||||
decodeListCerts,
|
||||
r.Get("/certs/:certId", kithttp.NewServer(
|
||||
viewCert(svc),
|
||||
decodeViewCert,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
@@ -60,8 +55,15 @@ func MakeHandler(svc certs.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/serials/:thingId", kithttp.NewServer(
|
||||
listSerials(svc),
|
||||
decodeListCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.GetFunc("/version", mainflux.Version("certs"))
|
||||
r.GetFunc("/health", mainflux.Health("certs"))
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -85,16 +87,17 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
|
||||
}
|
||||
|
||||
func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := httputil.ReadUintQuery(r, limitKey, defLimit)
|
||||
l, err := apiutil.ReadUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o, err := httputil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
o, err := apiutil.ReadUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
thingID: bone.GetValue(r, "thingId"),
|
||||
limit: l,
|
||||
offset: o,
|
||||
@@ -102,12 +105,21 @@ func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeViewCert(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := viewReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
serialID: bone.GetValue(r, "certId"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errors.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addCertsReq{token: r.Header.Get("Authorization")}
|
||||
req := addCertsReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -117,7 +129,7 @@ func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
|
||||
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := revokeReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
certID: bone.GetValue(r, "certId"),
|
||||
}
|
||||
|
||||
@@ -125,23 +137,32 @@ func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case errors.ErrUnsupportedContentType:
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrAuthentication),
|
||||
err == apiutil.ErrBearerToken:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, errors.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case io.EOF, errors.ErrMalformedEntity,
|
||||
errors.ErrInvalidQueryParams:
|
||||
case errors.Contains(err, errors.ErrMalformedEntity),
|
||||
err == apiutil.ErrMissingID,
|
||||
err == apiutil.ErrMissingCertData,
|
||||
err == apiutil.ErrLimitSize:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errConflict:
|
||||
case errors.Contains(err, errors.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
|
||||
case errors.Contains(err, errors.ErrCreateEntity),
|
||||
errors.Contains(err, errors.ErrViewEntity),
|
||||
errors.Contains(err, errors.ErrRemoveEntity):
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -18,12 +18,15 @@ type Repository interface {
|
||||
// Save saves cert for thing into database
|
||||
Save(ctx context.Context, cert Cert) (string, error)
|
||||
|
||||
// RetrieveAll retrieve all issued certificates for given owner and thing id
|
||||
RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (Page, error)
|
||||
// RetrieveAll retrieve issued certificates for given owner ID
|
||||
RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// Remove certificate from DB for given thing
|
||||
Remove(ctx context.Context, thingID string) error
|
||||
// Remove removes certificate from DB for a given thing ID
|
||||
Remove(ctx context.Context, ownerID, thingID string) error
|
||||
|
||||
// RetrieveByThing certificate by given thing
|
||||
RetrieveByThing(ctx context.Context, thingID string) (Cert, error)
|
||||
// RetrieveByThing retrieves issued certificates for a given thing ID
|
||||
RetrieveByThing(ctx context.Context, ownerID, thingID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// RetrieveBySerial retrieves a certificate for a given serial ID
|
||||
RetrieveBySerial(ctx context.Context, ownerID, serialID string) (Cert, error)
|
||||
}
|
||||
|
||||
+72
-22
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var _ certs.Repository = (*certsRepoMock)(nil)
|
||||
@@ -15,44 +16,63 @@ var _ certs.Repository = (*certsRepoMock)(nil)
|
||||
type certsRepoMock struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
certs map[string]certs.Cert
|
||||
certsByThingID map[string]certs.Cert
|
||||
certsBySerial map[string]certs.Cert
|
||||
certsByThingID map[string]map[string][]certs.Cert
|
||||
}
|
||||
|
||||
// NewCertsRepository creates in-memory certs repository.
|
||||
func NewCertsRepository() certs.Repository {
|
||||
return &certsRepoMock{
|
||||
certs: make(map[string]certs.Cert),
|
||||
certsByThingID: make(map[string]certs.Cert),
|
||||
certsBySerial: make(map[string]certs.Cert),
|
||||
certsByThingID: make(map[string]map[string][]certs.Cert),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *certsRepoMock) Save(ctx context.Context, cert certs.Cert) (string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.certs[cert.Serial] = cert
|
||||
c.certsByThingID[cert.ThingID] = cert
|
||||
|
||||
crt := certs.Cert{
|
||||
OwnerID: cert.OwnerID,
|
||||
ThingID: cert.ThingID,
|
||||
Serial: cert.Serial,
|
||||
Expire: cert.Expire,
|
||||
}
|
||||
|
||||
_, ok := c.certsByThingID[cert.OwnerID][cert.ThingID]
|
||||
switch ok {
|
||||
case false:
|
||||
c.certsByThingID[cert.OwnerID] = map[string][]certs.Cert{
|
||||
cert.ThingID: []certs.Cert{crt},
|
||||
}
|
||||
default:
|
||||
c.certsByThingID[cert.OwnerID][cert.ThingID] = append(c.certsByThingID[cert.OwnerID][cert.ThingID], crt)
|
||||
}
|
||||
|
||||
c.certsBySerial[cert.Serial] = crt
|
||||
c.counter++
|
||||
return cert.Serial, nil
|
||||
}
|
||||
|
||||
func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if limit <= 0 {
|
||||
return certs.Page{}, nil
|
||||
}
|
||||
|
||||
first := offset + 1
|
||||
last := first + limit
|
||||
oc, ok := c.certsByThingID[ownerID]
|
||||
if !ok {
|
||||
return certs.Page{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
var crts []certs.Cert
|
||||
i := uint64(1)
|
||||
for _, v := range c.certs {
|
||||
if i >= first && i < last {
|
||||
crts = append(crts, v)
|
||||
for _, tc := range oc {
|
||||
for i, v := range tc {
|
||||
if uint64(i) >= offset && uint64(i) < offset+limit {
|
||||
crts = append(crts, v)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
page := certs.Page{
|
||||
@@ -64,24 +84,54 @@ func (c *certsRepoMock) RetrieveAll(ctx context.Context, ownerID, thingID string
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (c *certsRepoMock) Remove(ctx context.Context, serial string) error {
|
||||
func (c *certsRepoMock) Remove(ctx context.Context, ownerID, serial string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
crt, ok := c.certs[serial]
|
||||
crt, ok := c.certsBySerial[serial]
|
||||
if !ok {
|
||||
return certs.ErrNotFound
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
delete(c.certs, crt.Serial)
|
||||
delete(c.certsBySerial, crt.Serial)
|
||||
delete(c.certsByThingID, crt.ThingID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *certsRepoMock) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
|
||||
func (c *certsRepoMock) RetrieveByThing(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
crt, ok := c.certsByThingID[thingID]
|
||||
if !ok {
|
||||
return certs.Cert{}, certs.ErrNotFound
|
||||
if limit <= 0 {
|
||||
return certs.Page{}, nil
|
||||
}
|
||||
|
||||
cs, ok := c.certsByThingID[ownerID][thingID]
|
||||
if !ok {
|
||||
return certs.Page{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
var crts []certs.Cert
|
||||
for i, v := range cs {
|
||||
if uint64(i) >= offset && uint64(i) < offset+limit {
|
||||
crts = append(crts, v)
|
||||
}
|
||||
}
|
||||
|
||||
page := certs.Page{
|
||||
Certs: crts,
|
||||
Total: c.counter,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (c *certsRepoMock) RetrieveBySerial(ctx context.Context, ownerID, serialID string) (certs.Cert, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
crt, ok := c.certsBySerial[serialID]
|
||||
if !ok {
|
||||
return certs.Cert{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
return crt, nil
|
||||
}
|
||||
|
||||
+35
-13
@@ -14,6 +14,7 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/certs/pki"
|
||||
@@ -32,28 +33,27 @@ type agent struct {
|
||||
TLSCert tls.Certificate
|
||||
X509Cert *x509.Certificate
|
||||
RSABits int
|
||||
HoursValid string
|
||||
TTL string
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
certs map[string]pki.Cert
|
||||
}
|
||||
|
||||
func NewPkiAgent(tlsCert tls.Certificate, caCert *x509.Certificate, keyBits int, hoursValid string, timeout time.Duration) pki.Agent {
|
||||
func NewPkiAgent(tlsCert tls.Certificate, caCert *x509.Certificate, keyBits int, ttl string, timeout time.Duration) pki.Agent {
|
||||
return &agent{
|
||||
AuthTimeout: timeout,
|
||||
TLSCert: tlsCert,
|
||||
X509Cert: caCert,
|
||||
RSABits: keyBits,
|
||||
HoursValid: hoursValid,
|
||||
TTL: ttl,
|
||||
certs: make(map[string]pki.Cert),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) IssueCert(cn string, ttl, keyType string, keyBits int) (pki.Cert, error) {
|
||||
return a.certs(cn, ttl, keyBits)
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
func (a *agent) Revoke(serial string) (time.Time, error) {
|
||||
return time.Now(), nil
|
||||
}
|
||||
|
||||
func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
|
||||
if a.X509Cert == nil {
|
||||
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, pki.ErrMissingCACertificate)
|
||||
}
|
||||
@@ -64,12 +64,12 @@ func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
|
||||
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
|
||||
}
|
||||
|
||||
if daysValid == "" {
|
||||
daysValid = a.HoursValid
|
||||
if ttl == "" {
|
||||
ttl = a.TTL
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
validFor, err := time.ParseDuration(daysValid)
|
||||
validFor, err := time.ParseDuration(ttl)
|
||||
if err != nil {
|
||||
return pki.Cert{}, errors.Wrap(pki.ErrFailedCertCreation, err)
|
||||
}
|
||||
@@ -129,6 +129,12 @@ func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
|
||||
}
|
||||
buffKeyOut.Flush()
|
||||
key := keyOut.String()
|
||||
|
||||
a.certs[x509cert.SerialNumber.String()] = pki.Cert{
|
||||
ClientCert: cert,
|
||||
}
|
||||
a.counter++
|
||||
|
||||
return pki.Cert{
|
||||
ClientCert: cert,
|
||||
ClientKey: key,
|
||||
@@ -138,6 +144,22 @@ func (a *agent) certs(cn, daysValid string, keyBits int) (pki.Cert, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *agent) Read(serial string) (pki.Cert, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
crt, ok := a.certs[serial]
|
||||
if !ok {
|
||||
return pki.Cert{}, errors.ErrNotFound
|
||||
}
|
||||
|
||||
return crt, nil
|
||||
}
|
||||
|
||||
func (a *agent) Revoke(serial string) (time.Time, error) {
|
||||
return time.Now(), nil
|
||||
}
|
||||
|
||||
func publicKey(priv interface{}) (interface{}, error) {
|
||||
if priv == nil {
|
||||
return nil, errPrivateKeyEmpty
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Certs service
|
||||
description: HTTP API for Certs service
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/certs:
|
||||
post:
|
||||
summary: Creates a certificate for thing
|
||||
description: Creates a certificate for thing
|
||||
tags:
|
||||
- Thing to proxy
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'500':
|
||||
description: Unexpected server-side error ocurred.
|
||||
/certs/{thingId}:
|
||||
get:
|
||||
summary: Retrieves certificates
|
||||
description: |
|
||||
Retrieves a certificates for given thing ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/CertsRes"
|
||||
'404':
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/certs/{certId}:
|
||||
delete:
|
||||
summary: Revokes certificate
|
||||
description: |
|
||||
Revokes a certificates for given cert ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
'404':
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token. Used instead of credentials in env or config.toml.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
ThingID:
|
||||
name: thingID
|
||||
description: Thing ID
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
CertID:
|
||||
name: certID
|
||||
description: Serial of certificate
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Certs:
|
||||
type: object
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
issuing_ca:
|
||||
type: string
|
||||
description: CA Certificate that is used to issue client certs, usually intermediate.
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Revoke:
|
||||
type: object
|
||||
properties:
|
||||
revocation_time:
|
||||
type: string
|
||||
description: Certificate revocation time
|
||||
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- thing_id
|
||||
- days_valid
|
||||
- key_bits
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
days_valid:
|
||||
type: string
|
||||
key_bits:
|
||||
type: integer
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
CertsRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Certs"
|
||||
RevokeRes:
|
||||
description: Certificate revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revoke"
|
||||
+45
-17
@@ -17,14 +17,12 @@ import (
|
||||
|
||||
const (
|
||||
issue = "issue"
|
||||
cert = "cert"
|
||||
revoke = "revoke"
|
||||
apiVer = "v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotImplemented indicate that method called is not implemented
|
||||
ErrNotImplemented = errors.New("method not implemented for certs")
|
||||
|
||||
// ErrMissingCACertificate indicates missing CA certificate
|
||||
ErrMissingCACertificate = errors.New("missing CA certificate for certificate signing")
|
||||
|
||||
@@ -35,6 +33,7 @@ var (
|
||||
ErrFailedCertRevocation = errors.New("failed to revoke certificate")
|
||||
|
||||
errFailedVaultCertIssue = errors.New("failed to issue vault certificate")
|
||||
errFailedVaultRead = errors.New("failed to read vault certificate")
|
||||
errFailedCertDecoding = errors.New("failed to decode response from vault service")
|
||||
)
|
||||
|
||||
@@ -48,10 +47,14 @@ type Cert struct {
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
// Agent represents the Vault PKI interface.
|
||||
type Agent interface {
|
||||
// IssueCert issues certificate on PKI
|
||||
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
|
||||
|
||||
// Read retrieves certificate from PKI
|
||||
Read(serial string) (Cert, error)
|
||||
|
||||
// Revoke revokes certificate from PKI
|
||||
Revoke(serial string) (time.Time, error)
|
||||
}
|
||||
@@ -62,6 +65,7 @@ type pkiAgent struct {
|
||||
role string
|
||||
host string
|
||||
issueURL string
|
||||
readURL string
|
||||
revokeURL string
|
||||
client *api.Client
|
||||
}
|
||||
@@ -77,6 +81,7 @@ type certRevokeReq struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
}
|
||||
|
||||
// NewVaultClient instantiates a Vault client.
|
||||
func NewVaultClient(token, host, path, role string) (Agent, error) {
|
||||
conf := &api.Config{
|
||||
Address: host,
|
||||
@@ -94,6 +99,7 @@ func NewVaultClient(token, host, path, role string) (Agent, error) {
|
||||
path: path,
|
||||
client: client,
|
||||
issueURL: "/" + apiVer + "/" + path + "/" + issue + "/" + role,
|
||||
readURL: "/" + apiVer + "/" + path + "/" + cert + "/",
|
||||
revokeURL: "/" + apiVer + "/" + path + "/" + revoke,
|
||||
}
|
||||
return &p, nil
|
||||
@@ -129,22 +135,47 @@ func (p *pkiAgent) IssueCert(cn string, ttl, keyType string, keyBits int) (Cert,
|
||||
return Cert{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, _ := api.ParseSecret(resp.Body)
|
||||
cert := Cert{}
|
||||
s, err := api.ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
cert := Cert{}
|
||||
if err = mapstructure.Decode(s.Data, &cert); err != nil {
|
||||
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
|
||||
}
|
||||
|
||||
// Expire time calc must be revised value doesnt look correct
|
||||
exp, err := s.Data["expiration"].(json.Number).Float64()
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
expTime := time.Unix(0, int64(exp)*int64(time.Millisecond))
|
||||
cert.Expire = expTime
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) Read(serial string) (Cert, error) {
|
||||
r := p.client.NewRequest("GET", p.readURL+"/"+serial)
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
return Cert{}, errors.Wrap(errFailedVaultRead, err)
|
||||
}
|
||||
|
||||
s, err := api.ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
cert := Cert{}
|
||||
if err = mapstructure.Decode(s.Data, &cert); err != nil {
|
||||
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
|
||||
@@ -158,13 +189,10 @@ func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
@@ -183,6 +211,6 @@ func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Unix(0, int64(rev)*int64(time.Millisecond)), nil
|
||||
|
||||
return time.Unix(0, int64(rev)*int64(time.Millisecond)), nil
|
||||
}
|
||||
|
||||
+44
-35
@@ -14,17 +14,11 @@ import (
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
const duplicateErr = "unique_violation"
|
||||
|
||||
var (
|
||||
errSaveDB = errors.New("failed to save certificate to database")
|
||||
errRetrieveDB = errors.New("failed to retrieve certificate from db")
|
||||
errRemove = errors.New("failed to remove certificate from database")
|
||||
errInvalid = "invalid_text_representation"
|
||||
)
|
||||
var errInvalid = "invalid_text_representation"
|
||||
|
||||
var _ certs.Repository = (*certsRepository)(nil)
|
||||
|
||||
@@ -45,7 +39,7 @@ func NewRepository(db *sqlx.DB, log logger.Logger) certs.Repository {
|
||||
return &certsRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 ORDER BY expire LIMIT $2 OFFSET $3;`
|
||||
rows, err := cr.db.Query(q, ownerID, limit, offset)
|
||||
if err != nil {
|
||||
@@ -55,7 +49,6 @@ func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID, thingID stri
|
||||
defer rows.Close()
|
||||
|
||||
certificates := []certs.Cert{}
|
||||
|
||||
for rows.Next() {
|
||||
c := certs.Cert{}
|
||||
if err := rows.Scan(&c.ThingID, &c.OwnerID, &c.Serial, &c.Expire); err != nil {
|
||||
@@ -86,7 +79,7 @@ func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, er
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
return "", errors.Wrap(errors.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
dbcrt := toDBCert(cert)
|
||||
@@ -99,7 +92,7 @@ func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, er
|
||||
|
||||
cr.rollback("Failed to insert a Cert", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
return "", errors.Wrap(errors.ErrCreateEntity, e)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -109,52 +102,68 @@ func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, er
|
||||
return cert.Serial, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Remove(ctx context.Context, serial string) error {
|
||||
if _, err := cr.retrieveBySerial(ctx, serial); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
func (cr certsRepository) Remove(ctx context.Context, ownerID, serial string) error {
|
||||
if _, err := cr.RetrieveBySerial(ctx, ownerID, serial); err != nil {
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
q := `DELETE FROM certs WHERE serial = :serial`
|
||||
var c certs.Cert
|
||||
c.Serial = serial
|
||||
dbcrt := toDBCert(c)
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcrt); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
return errors.Wrap(errors.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE thing_id = $1`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, thingID).StructScan(&dbcrt); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return c, errors.Wrap(things.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(errRetrieveDB, err)
|
||||
func (cr certsRepository) RetrieveByThing(ctx context.Context, ownerID, thingID string, offset, limit uint64) (certs.Page, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 AND thing_id = $2 ORDER BY expire LIMIT $3 OFFSET $4;`
|
||||
rows, err := cr.db.Query(q, ownerID, thingID, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
defer rows.Close()
|
||||
|
||||
return c, nil
|
||||
certificates := []certs.Cert{}
|
||||
for rows.Next() {
|
||||
c := certs.Cert{}
|
||||
if err := rows.Scan(&c.ThingID, &c.OwnerID, &c.Serial, &c.Expire); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return certs.Page{}, err
|
||||
|
||||
}
|
||||
certificates = append(certificates, c)
|
||||
}
|
||||
|
||||
q = `SELECT COUNT(*) FROM certs WHERE owner_id = $1 AND thing_id = $2`
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q, ownerID, thingID).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count certs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
|
||||
return certs.Page{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Certs: certificates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) retrieveBySerial(ctx context.Context, serial string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE serial = $1`
|
||||
func (cr certsRepository) RetrieveBySerial(ctx context.Context, ownerID, serialID string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 AND serial = $2`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, serial).StructScan(&dbcrt); err != nil {
|
||||
if err := cr.db.QueryRowxContext(ctx, q, ownerID, serialID).StructScan(&dbcrt); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return c, errors.Wrap(things.ErrNotFound, err)
|
||||
return c, errors.Wrap(errors.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(errRetrieveDB, err)
|
||||
return c, errors.Wrap(errors.ErrViewEntity, err)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.8-alpine", cfg)
|
||||
container, err := pool.Run("postgres", "13.3-alpine", cfg)
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
|
||||
}
|
||||
|
||||
+81
-32
@@ -16,16 +16,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound = errors.New("non-existent entity")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrFailedCertCreation failed to create certificate
|
||||
ErrFailedCertCreation = errors.New("failed to create client certificate")
|
||||
|
||||
@@ -41,13 +31,19 @@ var _ Service = (*certsService)(nil)
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// IssueCert issues certificate for given thing id if access is granted with token
|
||||
IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (Cert, error)
|
||||
IssueCert(ctx context.Context, token, thingID, ttl string, keyBits int, keyType string) (Cert, error)
|
||||
|
||||
// ListCerts lists all certificates issued for given owner
|
||||
// ListCerts lists certificates issued for a given thing ID
|
||||
ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// RevokeCert revokes certificate for given thing
|
||||
RevokeCert(ctx context.Context, token, thingID string) (Revoke, error)
|
||||
// ListSerials lists certificate serial IDs issued for a given thing ID
|
||||
ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// ViewCert retrieves the certificate issued for a given serial ID
|
||||
ViewCert(ctx context.Context, token, serialID string) (Cert, error)
|
||||
|
||||
// RevokeCert revokes a certificate for a given serial ID
|
||||
RevokeCert(ctx context.Context, token, serialID string) (Revoke, error)
|
||||
}
|
||||
|
||||
// Config defines the service parameters
|
||||
@@ -58,8 +54,7 @@ type Config struct {
|
||||
HTTPPort string
|
||||
ServerCert string
|
||||
ServerKey string
|
||||
BaseURL string
|
||||
ThingsPrefix string
|
||||
CertsURL string
|
||||
JaegerURL string
|
||||
AuthURL string
|
||||
AuthTimeout time.Duration
|
||||
@@ -110,10 +105,10 @@ type Cert struct {
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
|
||||
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, ttl string, keyBits int, keyType string) (Cert, error) {
|
||||
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Cert{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
@@ -121,14 +116,14 @@ func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, da
|
||||
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
|
||||
cert, err := cs.pki.IssueCert(thing.Key, daysValid, keyType, keyBits)
|
||||
cert, err := cs.pki.IssueCert(thing.Key, ttl, keyType, keyBits)
|
||||
if err != nil {
|
||||
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
|
||||
c := Cert{
|
||||
ThingID: thingID,
|
||||
OwnerID: owner.GetEmail(),
|
||||
OwnerID: owner.GetId(),
|
||||
ClientCert: cert.ClientCert,
|
||||
IssuingCA: cert.IssuingCA,
|
||||
CAChain: cert.CAChain,
|
||||
@@ -144,36 +139,90 @@ func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, da
|
||||
|
||||
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
|
||||
var revoke Revoke
|
||||
_, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return revoke, err
|
||||
}
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
cert, err := cs.certsRepo.RetrieveByThing(ctx, thing.ID)
|
||||
// TODO: Replace offset and limit
|
||||
offset, limit := uint64(0), uint64(10000)
|
||||
cp, err := cs.certsRepo.RetrieveByThing(ctx, u.GetId(), thing.ID, offset, limit)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
revTime, err := cs.pki.Revoke(cert.Serial)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
revoke.RevocationTime = revTime
|
||||
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
|
||||
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
|
||||
for _, c := range cp.Certs {
|
||||
revTime, err := cs.pki.Revoke(c.Serial)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
revoke.RevocationTime = revTime
|
||||
if err = cs.certsRepo.Remove(context.Background(), u.GetId(), c.Serial); err != nil {
|
||||
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
|
||||
}
|
||||
}
|
||||
|
||||
return revoke, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListCerts(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Page{}, err
|
||||
}
|
||||
|
||||
return cs.certsRepo.RetrieveAll(ctx, u.GetEmail(), thingID, offset, limit)
|
||||
cp, err := cs.certsRepo.RetrieveByThing(ctx, u.GetId(), thingID, offset, limit)
|
||||
if err != nil {
|
||||
return Page{}, err
|
||||
}
|
||||
|
||||
for i, cert := range cp.Certs {
|
||||
vcert, err := cs.pki.Read(cert.Serial)
|
||||
if err != nil {
|
||||
return Page{}, err
|
||||
}
|
||||
cp.Certs[i].ClientCert = vcert.ClientCert
|
||||
cp.Certs[i].ClientKey = vcert.ClientKey
|
||||
}
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListSerials(ctx context.Context, token, thingID string, offset, limit uint64) (Page, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, err
|
||||
}
|
||||
|
||||
return cs.certsRepo.RetrieveByThing(ctx, u.GetId(), thingID, offset, limit)
|
||||
}
|
||||
|
||||
func (cs *certsService) ViewCert(ctx context.Context, token, serialID string) (Cert, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
cert, err := cs.certsRepo.RetrieveBySerial(ctx, u.GetId(), serialID)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
vcert, err := cs.pki.Read(serialID)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
c := Cert{
|
||||
ThingID: cert.ThingID,
|
||||
ClientCert: vcert.ClientCert,
|
||||
Serial: cert.Serial,
|
||||
Expire: cert.Expire,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
+189
-66
@@ -21,6 +21,7 @@ import (
|
||||
bsmocks "github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/certs/mocks"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
@@ -38,20 +39,19 @@ const (
|
||||
thingsNum = 1
|
||||
thingKey = "thingKey"
|
||||
thingID = "1"
|
||||
daysValid = "1h"
|
||||
ttl = "1h"
|
||||
keyBits = 2048
|
||||
key = "rsa"
|
||||
certNum = 10
|
||||
|
||||
cfgLogLevel = "error"
|
||||
cfgClientTLS = false
|
||||
cfgServerCert = ""
|
||||
cfgServerKey = ""
|
||||
cfgBaseURL = "http://localhost"
|
||||
cfgThingsPrefix = ""
|
||||
cfgJaegerURL = ""
|
||||
cfgAuthURL = "localhost:8181"
|
||||
cfgAuthTimeout = "1s"
|
||||
cfgLogLevel = "error"
|
||||
cfgClientTLS = false
|
||||
cfgServerCert = ""
|
||||
cfgServerKey = ""
|
||||
cfgCertsURL = "http://localhost"
|
||||
cfgJaegerURL = ""
|
||||
cfgAuthURL = "localhost:8181"
|
||||
cfgAuthTimeout = "1s"
|
||||
|
||||
caPath = "../docker/ssl/certs/ca.crt"
|
||||
caKeyPath = "../docker/ssl/certs/ca.key"
|
||||
@@ -60,12 +60,13 @@ const (
|
||||
)
|
||||
|
||||
func newService(tokens map[string]string) (certs.Service, error) {
|
||||
users := bsmocks.NewUsersService(map[string]string{token: email})
|
||||
server := newThingsServer(newThingsService(users))
|
||||
ac := bsmocks.NewAuthClient(map[string]string{token: email})
|
||||
server := newThingsServer(newThingsService(ac))
|
||||
|
||||
auth := thmocks.NewAuthService(tokens)
|
||||
policies := []thmocks.MockSubjectSet{{Object: "users", Relation: "member"}}
|
||||
auth := thmocks.NewAuthService(tokens, map[string][]thmocks.MockSubjectSet{email: policies})
|
||||
config := mfsdk.Config{
|
||||
BaseURL: server.URL,
|
||||
ThingsURL: server.URL,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
@@ -86,8 +87,7 @@ func newService(tokens map[string]string) (certs.Service, error) {
|
||||
ClientTLS: cfgClientTLS,
|
||||
ServerCert: cfgServerCert,
|
||||
ServerKey: cfgServerKey,
|
||||
BaseURL: cfgBaseURL,
|
||||
ThingsPrefix: cfgThingsPrefix,
|
||||
CertsURL: cfgCertsURL,
|
||||
JaegerURL: cfgJaegerURL,
|
||||
AuthURL: cfgAuthURL,
|
||||
SignTLSCert: tlsCert,
|
||||
@@ -120,63 +120,63 @@ func TestIssueCert(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
|
||||
|
||||
cases := []struct {
|
||||
token string
|
||||
desc string
|
||||
thingID string
|
||||
daysValid string
|
||||
key string
|
||||
keyBits int
|
||||
err error
|
||||
token string
|
||||
desc string
|
||||
thingID string
|
||||
ttl string
|
||||
key string
|
||||
keyBits int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue new cert",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
daysValid: daysValid,
|
||||
key: key,
|
||||
keyBits: 2048,
|
||||
err: nil,
|
||||
desc: "issue new cert",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
ttl: ttl,
|
||||
key: key,
|
||||
keyBits: 2048,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue new cert for non existing thing id",
|
||||
token: token,
|
||||
thingID: "2",
|
||||
daysValid: daysValid,
|
||||
key: key,
|
||||
keyBits: 2048,
|
||||
err: certs.ErrFailedCertCreation,
|
||||
desc: "issue new cert for non existing thing id",
|
||||
token: token,
|
||||
thingID: "2",
|
||||
ttl: ttl,
|
||||
key: key,
|
||||
keyBits: 2048,
|
||||
err: certs.ErrFailedCertCreation,
|
||||
},
|
||||
{
|
||||
desc: "issue new cert for non existing thing id",
|
||||
token: wrongValue,
|
||||
thingID: thingID,
|
||||
daysValid: daysValid,
|
||||
key: key,
|
||||
keyBits: 2048,
|
||||
err: certs.ErrUnauthorizedAccess,
|
||||
desc: "issue new cert for non existing thing id",
|
||||
token: wrongValue,
|
||||
thingID: thingID,
|
||||
ttl: ttl,
|
||||
key: key,
|
||||
keyBits: 2048,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "issue new cert for bad key bits",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
daysValid: daysValid,
|
||||
key: key,
|
||||
keyBits: -2,
|
||||
err: certs.ErrFailedCertCreation,
|
||||
desc: "issue new cert for bad key bits",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
ttl: ttl,
|
||||
key: key,
|
||||
keyBits: -2,
|
||||
err: certs.ErrFailedCertCreation,
|
||||
},
|
||||
{
|
||||
desc: "issue new cert for bad key bits",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
daysValid: daysValid,
|
||||
key: key,
|
||||
keyBits: -2,
|
||||
err: certs.ErrFailedCertCreation,
|
||||
desc: "issue new cert for bad key bits",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
ttl: ttl,
|
||||
key: key,
|
||||
keyBits: -2,
|
||||
err: certs.ErrFailedCertCreation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
c, err := svc.IssueCert(context.Background(), tc.token, tc.thingID, tc.daysValid, tc.keyBits, tc.key)
|
||||
c, err := svc.IssueCert(context.Background(), tc.token, tc.thingID, tc.ttl, tc.keyBits, tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
cert, _ := readCert([]byte(c.ClientCert))
|
||||
if cert != nil {
|
||||
@@ -190,7 +190,7 @@ func TestRevokeCert(t *testing.T) {
|
||||
svc, err := newService(map[string]string{token: email})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
|
||||
|
||||
_, err = svc.IssueCert(context.Background(), token, thingID, daysValid, keyBits, key)
|
||||
_, err = svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -209,7 +209,7 @@ func TestRevokeCert(t *testing.T) {
|
||||
desc: "revoke cert for invalid token",
|
||||
token: wrongValue,
|
||||
thingID: thingID,
|
||||
err: certs.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert for invalid thing id",
|
||||
@@ -231,7 +231,7 @@ func TestListCerts(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
|
||||
|
||||
for i := 0; i < certNum; i++ {
|
||||
_, err = svc.IssueCert(context.Background(), token, thingID, daysValid, keyBits, key)
|
||||
_, err = svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
|
||||
}
|
||||
|
||||
@@ -260,10 +260,10 @@ func TestListCerts(t *testing.T) {
|
||||
offset: 0,
|
||||
limit: certNum,
|
||||
size: 0,
|
||||
err: certs.ErrUnauthorizedAccess,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list half certs with invalid token",
|
||||
desc: "list half certs with valid token",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
offset: certNum / 2,
|
||||
@@ -272,7 +272,7 @@ func TestListCerts(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list last certs with invalid token",
|
||||
desc: "list last cert with valid token",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
offset: certNum - 1,
|
||||
@@ -288,11 +288,134 @@ func TestListCerts(t *testing.T) {
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSerials(t *testing.T) {
|
||||
svc, err := newService(map[string]string{token: email})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
|
||||
|
||||
var issuedCerts []certs.Cert
|
||||
for i := 0; i < certNum; i++ {
|
||||
cert, err := svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
|
||||
|
||||
crt := certs.Cert{
|
||||
OwnerID: cert.OwnerID,
|
||||
ThingID: cert.ThingID,
|
||||
Serial: cert.Serial,
|
||||
Expire: cert.Expire,
|
||||
}
|
||||
issuedCerts = append(issuedCerts, crt)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
token string
|
||||
desc string
|
||||
thingID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
certs []certs.Cert
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list all certs with valid token",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
offset: 0,
|
||||
limit: certNum,
|
||||
certs: issuedCerts,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list all certs with invalid token",
|
||||
token: wrongValue,
|
||||
thingID: thingID,
|
||||
offset: 0,
|
||||
limit: certNum,
|
||||
certs: nil,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list half certs with valid token",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
offset: certNum / 2,
|
||||
limit: certNum,
|
||||
certs: issuedCerts[certNum/2:],
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list last cert with valid token",
|
||||
token: token,
|
||||
thingID: thingID,
|
||||
offset: certNum - 1,
|
||||
limit: certNum,
|
||||
certs: []certs.Cert{issuedCerts[certNum-1]},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
page, err := svc.ListSerials(context.Background(), tc.token, tc.thingID, tc.offset, tc.limit)
|
||||
assert.Equal(t, tc.certs, page.Certs, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.certs, page.Certs))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewCert(t *testing.T) {
|
||||
svc, err := newService(map[string]string{token: email})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected service creation error: %s\n", err))
|
||||
|
||||
ic, err := svc.IssueCert(context.Background(), token, thingID, ttl, keyBits, key)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected cert creation error: %s\n", err))
|
||||
|
||||
cert := certs.Cert{
|
||||
ThingID: thingID,
|
||||
ClientCert: ic.ClientCert,
|
||||
Serial: ic.Serial,
|
||||
Expire: ic.Expire,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
token string
|
||||
desc string
|
||||
serialID string
|
||||
cert certs.Cert
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list cert with valid token and serial",
|
||||
token: token,
|
||||
serialID: cert.Serial,
|
||||
cert: cert,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list cert with invalid token",
|
||||
token: wrongValue,
|
||||
serialID: cert.Serial,
|
||||
cert: certs.Cert{},
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list cert with invalid serial",
|
||||
token: token,
|
||||
serialID: wrongValue,
|
||||
cert: certs.Cert{},
|
||||
err: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
cert, err := svc.ViewCert(context.Background(), tc.token, tc.serialID)
|
||||
assert.Equal(t, tc.cert, cert, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.cert, cert))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc)
|
||||
logger := logger.NewMock()
|
||||
mux := httpapi.MakeHandler(mocktracer.New(), svc, logger)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
|
||||
+17
-3
@@ -7,9 +7,9 @@ make cli
|
||||
|
||||
## Usage
|
||||
### Service
|
||||
#### Get the version of Mainflux services
|
||||
#### Get Mainflux Things services Health Check
|
||||
```bash
|
||||
mainflux-cli version
|
||||
mainflux-cli health
|
||||
```
|
||||
|
||||
### Users management
|
||||
@@ -239,4 +239,18 @@ mainflux-cli groups members <group_id> <user_auth_token>
|
||||
#### List groups that user belongs to
|
||||
```bash
|
||||
mainflux-cli groups membership <user_id> <user_auth_token>
|
||||
```
|
||||
```
|
||||
|
||||
### Keys management
|
||||
#### Issue a new Key
|
||||
```bash
|
||||
mainflux-cli keys issue <duration> <user_auth_token>
|
||||
```
|
||||
#### Remove API key from database
|
||||
```bash
|
||||
mainflux-cli keys revoke <key_id> <user_auth_token>
|
||||
```
|
||||
#### Retrieve API key with given id
|
||||
```bash
|
||||
mainflux-cli keys retrieve <key_id> <user_auth_token>
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user