mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 436db8877d | |||
| 4da66aecbf | |||
| 3c794d4a03 | |||
| 8dd59f8557 | |||
| bc41c32cf5 | |||
| 42e4e054c2 | |||
| 9ed5f8334f | |||
| 8ea26c5ab7 | |||
| 043d1e0aac | |||
| f10e49e6b5 | |||
| f18f2c1c98 | |||
| dfa6d8ba0d | |||
| e272d9f7ad | |||
| 26c944b5c3 | |||
| 3694a82de6 | |||
| 930f8beb29 | |||
| 9334568ba2 | |||
| b91024465c | |||
| be1361311d | |||
| c1088b9315 | |||
| 2453cd75ed | |||
| 7dc6b0d879 | |||
| 381ebb1e51 | |||
| 09d09c6ef5 | |||
| a7eee53dfb | |||
| e6b7fd818b | |||
| ecff066421 | |||
| 8d33285489 | |||
| ee5c76395a | |||
| 4a9c7403d8 | |||
| 381a15a695 | |||
| a7b81dc37a | |||
| a1643b1e7e | |||
| 340e685d70 | |||
| 97f8d65885 | |||
| 7f3e2c1b21 | |||
| 8b004b3daf | |||
| a7c3cfcf1c | |||
| f1ee9d0665 | |||
| ea3a891c91 | |||
| c26c7f34cd | |||
| a5fb55c328 |
@@ -29,7 +29,7 @@ MF_AUTHN_LOG_LEVEL=debug
|
||||
MF_AUTHN_HTTP_PORT=8189
|
||||
MF_AUTHN_GRPC_PORT=8181
|
||||
MF_AUTHN_GRPC_URL=authn:8181
|
||||
MF_AUTHN_GRPC_TIMEOUT=1
|
||||
MF_AUTHN_GRPC_TIMEOUT=1s
|
||||
MF_AUTHN_DB_PORT=5432
|
||||
MF_AUTHN_DB_USER=mainflux
|
||||
MF_AUTHN_DB_PASS=mainflux
|
||||
@@ -43,6 +43,8 @@ MF_USERS_DB_PORT=5432
|
||||
MF_USERS_DB_USER=mainflux
|
||||
MF_USERS_DB_PASS=mainflux
|
||||
MF_USERS_DB=users
|
||||
MF_USERS_ADMIN_EMAIL=admin@example.com
|
||||
MF_USERS_ADMIN_PASSWORD=12345678
|
||||
|
||||
### Email utility
|
||||
MF_EMAIL_DRIVER=smtp
|
||||
@@ -63,12 +65,11 @@ MF_THINGS_HTTP_PORT=8182
|
||||
MF_THINGS_AUTH_HTTP_PORT=8989
|
||||
MF_THINGS_AUTH_GRPC_PORT=8183
|
||||
MF_THINGS_AUTH_GRPC_URL=things:8183
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT=1
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT=1s
|
||||
MF_THINGS_DB_PORT=5432
|
||||
MF_THINGS_DB_USER=mainflux
|
||||
MF_THINGS_DB_PASS=mainflux
|
||||
MF_THINGS_DB=things
|
||||
MF_THINGS_SECRET=secret
|
||||
MF_THINGS_ES_URL=localhost:6379
|
||||
MF_THINGS_ES_PASS=
|
||||
MF_THINGS_ES_DB=0
|
||||
@@ -108,7 +109,6 @@ MF_PROVISION_CONFIG_FILE=/configs/config.toml
|
||||
MF_PROVISION_LOG_LEVEL=debug
|
||||
MF_PROVISION_HTTP_PORT=8190
|
||||
MF_PROVISION_ENV_CLIENTS_TLS=false
|
||||
MF_PROVISION_CA_CERTS=
|
||||
MF_PROVISION_SERVER_CERT=
|
||||
MF_PROVISION_SERVER_KEY=
|
||||
MF_PROVISION_MQTT_URL=tcp://localhost
|
||||
@@ -117,13 +117,44 @@ MF_PROVISION_THINGS_LOCATION=http://things:8182
|
||||
MF_PROVISION_USER=
|
||||
MF_PROVISION_PASS=
|
||||
MF_PROVISION_API_KEY=
|
||||
MF_PROVISION_CERTS_SVC_URL=http://localhost/certs
|
||||
MF_PROVISION_X509_PROVISIONING=false
|
||||
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things/configs
|
||||
MF_PROVISION_CERTS_SVC_URL=http://certs:8204
|
||||
MF_PROVISION_X509_PROVISIONING=true
|
||||
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things
|
||||
MF_PROVISION_BS_SVC_WHITELIST_URL=http://bootstrap:8202/things/state
|
||||
MF_PROVISION_BS_CONFIG_PROVISIONING=true
|
||||
MF_PROVISION_BS_AUTO_WHITELIST=true
|
||||
MF_PROVISION_BS_CONTENT=
|
||||
MF_PROVISION_CERTS_RSA_BITS=4096
|
||||
MF_PROVISION_CERTS_HOURS_VALID=2400h
|
||||
|
||||
# Certs
|
||||
MF_CERTS_LOG_LEVEL=debug
|
||||
MF_CERTS_HTTP_PORT=8204
|
||||
MF_CERTS_DB_HOST=certs-db
|
||||
MF_CERTS_DB_PORT=5432
|
||||
MF_CERTS_DB_USER=mainflux
|
||||
MF_CERTS_DB_PASS=mainflux
|
||||
MF_CERTS_DB=certs
|
||||
MF_CERTS_DB_SSL_MODE=
|
||||
MF_CERTS_DB_SSL_CERT=
|
||||
MF_CERTS_DB_SSL_KEY=
|
||||
MF_CERTS_DB_SSL_ROOT_CERT=
|
||||
MF_CERTS_ENCRYPT_KEY=
|
||||
MF_CERTS_CLIENT_TLS=
|
||||
MF_CERTS_CA_CERTS=
|
||||
MF_CERTS_SERVER_CERT=
|
||||
MF_CERTS_SERVER_KEY=
|
||||
MF_SDK_BASE_URL=http://172.17.0.1
|
||||
MF_SDK_THINGS_PREFIX=
|
||||
MF_CERTS_SIGN_CA_PATH=/etc/ssl/certs/ca.crt
|
||||
MF_CERTS_SIGN_CA_KEY_PATH=/etc/ssl/certs/ca.key
|
||||
MF_CERTS_SIGN_HOURS_VALID=2048h
|
||||
MF_CERTS_SIGN_RSA_BITS=2048
|
||||
MF_CERTS_VAULT_HOST=
|
||||
MF_CERTS_VAULT_PKI_PATH=pki_int
|
||||
MF_CERTS_VAULT_ROLE=agent
|
||||
MF_CERTS_VAULT_TOKEN=s.nArgw6xn3uIOfA7nfKk8LFaW
|
||||
|
||||
|
||||
### LoRa
|
||||
MF_LORA_ADAPTER_LOG_LEVEL=debug
|
||||
@@ -205,7 +236,7 @@ MF_POSTGRES_WRITER_PORT=9104
|
||||
MF_POSTGRES_WRITER_DB_PORT=5432
|
||||
MF_POSTGRES_WRITER_DB_USER=mainflux
|
||||
MF_POSTGRES_WRITER_DB_PASS=mainflux
|
||||
MF_POSTGRES_WRITER_DB=messages
|
||||
MF_POSTGRES_WRITER_DB=mainflux
|
||||
MF_POSTGRES_WRITER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_WRITER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_KEY=""
|
||||
@@ -220,7 +251,7 @@ MF_POSTGRES_READER_CA_CERTS=""
|
||||
MF_POSTGRES_READER_DB_PORT=5432
|
||||
MF_POSTGRES_READER_DB_USER=mainflux
|
||||
MF_POSTGRES_READER_DB_PASS=mainflux
|
||||
MF_POSTGRES_READER_DB=messages
|
||||
MF_POSTGRES_READER_DB=mainflux
|
||||
MF_POSTGRES_READER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_READER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_READER_DB_SSL_KEY=""
|
||||
@@ -238,7 +269,7 @@ MF_TWINS_SINGLE_USER_EMAIL=""
|
||||
MF_TWINS_SINGLE_USER_TOKEN=""
|
||||
MF_TWINS_CLIENT_TLS=""
|
||||
MF_TWINS_CA_CERTS=""
|
||||
MF_TWINS_MQTT_URL=tcp://mqtt-adapter:1883
|
||||
MF_TWINS_THING_ID=
|
||||
MF_TWINS_THING_KEY=
|
||||
MF_TWINS_CHANNEL_ID=
|
||||
MF_TWINS_CACHE_URL=es-redis:6379
|
||||
MF_TWINS_CACHE_PASS=
|
||||
MF_TWINS_CACHE_DB=0
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
|
||||
bootstrap opcua authn twins mqtt provision
|
||||
bootstrap opcua authn twins mqtt provision certs
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
@@ -22,7 +23,7 @@ define make_docker
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--tag=mainflux/$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
@@ -32,7 +33,7 @@ define make_docker_dev
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(svc) \
|
||||
--tag=mainflux/$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
@@ -44,23 +45,12 @@ clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
|
||||
cleandocker:
|
||||
# Stop all containers (if running)
|
||||
docker-compose -f docker/docker-compose.yml stop
|
||||
# Remove mainflux containers
|
||||
docker ps -f name=mainflux -aq | xargs -r docker rm
|
||||
|
||||
# Remove exited containers
|
||||
docker ps -f name=mainflux -f status=dead -f status=exited -aq | xargs -r docker rm -v
|
||||
|
||||
# Remove unused images
|
||||
docker images "mainflux\/*" -f dangling=true -q | xargs -r docker rmi
|
||||
|
||||
# Remove old mainflux images
|
||||
docker images -q mainflux\/* | xargs -r docker rmi
|
||||
# Stops containers and removes containers, networks, volumes, and images created by up
|
||||
docker-compose -f docker/docker-compose.yml down --rmi all -v --remove-orphans
|
||||
|
||||
ifdef pv
|
||||
# Remove unused volumes
|
||||
docker volume ls -f name=mainflux -f dangling=true -q | xargs -r docker volume rm
|
||||
docker volume ls -f name=$(MF_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
endif
|
||||
|
||||
install:
|
||||
@@ -71,7 +61,7 @@ test:
|
||||
|
||||
proto:
|
||||
protoc --gofast_out=plugins=grpc:. *.proto
|
||||
protoc --gofast_out=plugins=grpc:. messaging/*.proto
|
||||
protoc --gofast_out=plugins=grpc:. pkg/messaging/*.proto
|
||||
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
@@ -87,7 +77,7 @@ dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc:$(1); \
|
||||
docker push $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
done
|
||||
endef
|
||||
|
||||
@@ -102,7 +92,7 @@ release:
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker tag $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
done
|
||||
$(call docker_push,$(version))
|
||||
|
||||
|
||||
@@ -4,15 +4,14 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"golang.org/x/net/context"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
@@ -14,9 +15,9 @@ import (
|
||||
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -32,10 +33,10 @@ var svc authn.Service
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
return authn.New(repo, idp, t)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc authn.Service, port int) {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
httpapi "github.com/mainflux/mainflux/authn/api/http"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -63,9 +64,9 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, idp, t)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func newServer(svc authn.Service) *httptest.Server {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
type claims struct {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.IdentityProvider = (*identityProviderMock)(nil)
|
||||
|
||||
type identityProviderMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) ID() (string, error) {
|
||||
idp.mu.Lock()
|
||||
defer idp.mu.Unlock()
|
||||
|
||||
idp.counter++
|
||||
return fmt.Sprintf("%s%012d", "123e4567-e89b-12d3-a456-", idp.counter), nil
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() authn.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- authn
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyRequest"
|
||||
responses:
|
||||
201:
|
||||
description: Issued new key.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using already existing ID.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/keys/{id}:
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
secret:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
|
||||
description: API Key value.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ID:
|
||||
name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: e-mail
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
"github.com/mainflux/mainflux/authn/uuid"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -23,8 +23,7 @@ func TestKeySave(t *testing.T) {
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
id, _ := uuidProvider.New().ID()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
@@ -64,8 +63,7 @@ func TestKeyRetrieve(t *testing.T) {
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
id, _ := uuidProvider.New().ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
@@ -112,8 +110,7 @@ func TestKeyRemove(t *testing.T) {
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
id, _ := uuidProvider.New().ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// defers will not be run when using os.Exit
|
||||
// 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)
|
||||
|
||||
+10
-9
@@ -7,7 +7,8 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -60,17 +61,17 @@ type Service interface {
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
idp IdentityProvider
|
||||
tokenizer Tokenizer
|
||||
keys KeyRepository
|
||||
uuidProvider mainflux.UUIDProvider
|
||||
tokenizer Tokenizer
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, idp IdentityProvider, tokenizer Tokenizer) Service {
|
||||
func New(keys KeyRepository, up mainflux.UUIDProvider, tokenizer Tokenizer) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
idp: idp,
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
uuidProvider: up,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +157,7 @@ func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, er
|
||||
}
|
||||
key.Issuer = email
|
||||
|
||||
id, err := svc.idp.ID()
|
||||
id, err := svc.uuidProvider.ID()
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -23,9 +24,9 @@ const (
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, idp, t)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- name: key
|
||||
description: JSON-formatted document describing the new key.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/KeyRequest"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Issued new key.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using already existing ID.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/Key"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
definitions:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
secret:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
|
||||
description: API Key value.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires
|
||||
KeyRequest:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: e-mail
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package uuid provides a UUID identity provider.
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
)
|
||||
|
||||
// errGeneratingID indicates error in generating UUID
|
||||
var errGeneratingID = errors.New("failed to generate uuid")
|
||||
|
||||
var _ authn.IdentityProvider = (*uuidIdentityProvider)(nil)
|
||||
|
||||
type uuidIdentityProvider struct{}
|
||||
|
||||
// New instantiates a UUID identity provider.
|
||||
func New() authn.IdentityProvider {
|
||||
return &uuidIdentityProvider{}
|
||||
}
|
||||
|
||||
func (idp *uuidIdentityProvider) ID() (string, error) {
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errGeneratingID, err)
|
||||
}
|
||||
|
||||
return id.String(), nil
|
||||
}
|
||||
+1
-1
@@ -64,7 +64,7 @@ The service is configured using the environment variables presented in the follo
|
||||
| MF_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_AUTHN_GRPC_URL | AuthN service gRPC URL | localhost:8181 |
|
||||
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1 |
|
||||
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1s |
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
+27
-39
@@ -162,49 +162,37 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case req.filter.Unknown:
|
||||
res := listUnknownRes{}
|
||||
for _, cfg := range page.Configs {
|
||||
res.Configs = append(res.Configs, unknownRes{
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
|
||||
view := viewRes{
|
||||
MFThing: cfg.MFThing,
|
||||
MFKey: cfg.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
MFThing: cfg.MFThing,
|
||||
MFKey: cfg.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-111
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
bsapi "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
@@ -79,7 +79,7 @@ var (
|
||||
CACert: "newca",
|
||||
}
|
||||
|
||||
notFoundRes = toJSON(errorRes{bootstrap.ErrNotFound.Error()})
|
||||
bsErrorRes = toJSON(errorRes{bootstrap.ErrBootstrap.Error()})
|
||||
unauthRes = toJSON(errorRes{bootstrap.ErrUnauthorizedAccess.Error()})
|
||||
malformedRes = toJSON(errorRes{bootstrap.ErrMalformedEntity.Error()})
|
||||
extKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrExternalKeyNotFound.Error()})
|
||||
@@ -156,8 +156,8 @@ func dec(in []byte) ([]byte, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func newService(authn mainflux.AuthNServiceClient, unknown map[string]string, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(unknown)
|
||||
func newService(authn mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
data := toJSON(addReq)
|
||||
@@ -326,7 +326,7 @@ func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
c := newConfig([]bootstrap.Channel{})
|
||||
|
||||
@@ -423,7 +423,7 @@ func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -517,7 +517,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -612,7 +612,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -724,7 +724,7 @@ func TestList(t *testing.T) {
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/configs")
|
||||
|
||||
@@ -973,7 +973,7 @@ func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1031,108 +1031,11 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUnknown(t *testing.T) {
|
||||
unknownNum := 10
|
||||
unknown := make([]config, unknownNum)
|
||||
unknownConfigs := make(map[string]string, unknownNum)
|
||||
// Save some unknown elements.
|
||||
for i := 0; i < unknownNum; i++ {
|
||||
u := config{
|
||||
ExternalID: fmt.Sprintf("key-%s", strconv.Itoa(i)),
|
||||
ExternalKey: fmt.Sprintf("%s%s", addExternalKey, strconv.Itoa(i)),
|
||||
}
|
||||
unknownConfigs[u.ExternalID] = u.ExternalKey
|
||||
unknown[i] = u
|
||||
}
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, unknownConfigs, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/unknown/configs")
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
url string
|
||||
status int
|
||||
res []config
|
||||
}{
|
||||
{
|
||||
desc: "view unknown unauthorized",
|
||||
auth: invalidToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusForbidden,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with an empty token",
|
||||
auth: "",
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusForbidden,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with limit < 0",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, -5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with offset < 0",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, -3, 5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with invalid query params",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&key=%%", path, 0, -5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a list of unknown",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusOK,
|
||||
res: unknown[:5],
|
||||
},
|
||||
{
|
||||
desc: "view unknown with no page paremeters",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s", path),
|
||||
status: http.StatusOK,
|
||||
res: unknown[:10],
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
var body map[string][]config
|
||||
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.res, body["configs"], fmt.Sprintf("%s: expected response '%s' got '%s'", tc.desc, tc.res, body["configs"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, map[string]string{}, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1181,7 +1084,7 @@ func TestBootstrap(t *testing.T) {
|
||||
externalID: unknown,
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: notFoundRes,
|
||||
res: bsErrorRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
@@ -1260,7 +1163,7 @@ func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
|
||||
@@ -89,27 +89,6 @@ func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type unknownRes struct {
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
}
|
||||
|
||||
type listUnknownRes struct {
|
||||
Configs []unknownRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@@ -78,12 +78,6 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/unknown/configs", kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeUnknownRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
@@ -172,27 +166,6 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUnknownRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
offset, limit, err := parsePagePrams(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
|
||||
@@ -32,7 +32,6 @@ type Channel struct {
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
Unknown bool
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
@@ -85,12 +84,6 @@ type ConfigRepository interface {
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(owner string, ids []string) ([]Channel, error)
|
||||
|
||||
// SaveUnknown saves Thing which unsuccessfully bootstrapped.
|
||||
SaveUnknown(owner, id string) error
|
||||
|
||||
// RetrieveUnknown returns a subset of unsuccessfully bootstrapped Things.
|
||||
RetrieveUnknown(offset, limit uint64) ConfigsPage
|
||||
|
||||
// Methods RemoveThing, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
|
||||
@@ -24,15 +24,13 @@ type configRepositoryMock struct {
|
||||
counter uint64
|
||||
configs map[string]bootstrap.Config
|
||||
channels map[string]bootstrap.Channel
|
||||
unknown map[string]string
|
||||
}
|
||||
|
||||
// NewConfigsRepository creates in-memory config repository.
|
||||
func NewConfigsRepository(unknown map[string]string) bootstrap.ConfigRepository {
|
||||
func NewConfigsRepository() bootstrap.ConfigRepository {
|
||||
return &configRepositoryMock{
|
||||
configs: make(map[string]bootstrap.Config),
|
||||
channels: make(map[string]bootstrap.Channel),
|
||||
unknown: unknown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +59,6 @@ func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []str
|
||||
}
|
||||
|
||||
crm.configs[config.MFThing] = config
|
||||
delete(crm.unknown, config.ExternalID)
|
||||
|
||||
return config.MFThing, nil
|
||||
}
|
||||
@@ -237,59 +234,6 @@ func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.S
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
configs := []bootstrap.Config{}
|
||||
i := uint64(0)
|
||||
l := int(limit)
|
||||
var keys []string
|
||||
for k := range crm.unknown {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
if i >= offset && len(configs) < l {
|
||||
configs = append(configs, bootstrap.Config{
|
||||
ExternalID: k,
|
||||
ExternalKey: crm.unknown[k],
|
||||
})
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: uint64(len(crm.unknown)),
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveUnknown(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.unknown {
|
||||
if k == id && v == token {
|
||||
delete(crm.unknown, k)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) SaveUnknown(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.unknown[id] = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ListExisting(token string, connections []string) ([]bootstrap.Channel, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -171,11 +171,11 @@ func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, s
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64) (things.ChannelsPage, error) {
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64, bool) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64) (things.Page, error) {
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64, bool) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform things configuration.
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/things/configs:
|
||||
post:
|
||||
summary: Adds new config
|
||||
description: |
|
||||
Adds new config to the list of config owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
201:
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed configs
|
||||
description: |
|
||||
Retrieves a list of managed configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates config info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the owner, ID, external ID,
|
||||
external key, Mainflux Thing ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the Mainflux channels.
|
||||
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:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/connections/{configId}:
|
||||
put:
|
||||
summary: Updates channels the thing is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Thing is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/EncConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/state/{configId}:
|
||||
put:
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding Mainflux Thing to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: ID of the Channel.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to the Channel.
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Config"
|
||||
required:
|
||||
- configs
|
||||
BootstrapConfig:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
EncConfigAuth:
|
||||
name: configAuthorization
|
||||
description: |
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/State"
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ConfigCreateReq:
|
||||
description: JSON-formatted document describing the new config.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
thing_id:
|
||||
type: string
|
||||
description: ID of the corresponding Mainflux Thing.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigCertUpdateReq:
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the thing is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
ConfigStateUpdateReq:
|
||||
description: Update the state of the Config.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
|
||||
responses:
|
||||
ConfigCreateRes:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /things/configs/{configId}).
|
||||
ConfigListRes:
|
||||
description: Data retrieved. Configs from this list don't contain channels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigList"
|
||||
ConfigRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
BootstrapConfigRes:
|
||||
description: |
|
||||
Data retrieved. If secure, a response is encrypted using
|
||||
the secret key, so the response is in the binary form.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,8 +34,6 @@ var (
|
||||
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")
|
||||
errRemoveUnknown = errors.New("failed to remove from uknown configurations in database")
|
||||
errSaveUnknown = errors.New("failed to insert into uknown configurations in 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")
|
||||
@@ -91,14 +89,6 @@ func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (stri
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
q = "DELETE FROM unknown_configs WHERE external_id = :external_id AND external_key = :external_key"
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
cr.rollback("Failed to remove from unknown", tx, err)
|
||||
|
||||
return "", errors.Wrap(errRemoveUnknown, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
}
|
||||
@@ -393,55 +383,6 @@ func (cr configRepository) ListExisting(owner string, ids []string) ([]bootstrap
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) SaveUnknown(owner, id string) error {
|
||||
q := `INSERT INTO unknown_configs (external_id, external_key) VALUES ($1, $2)`
|
||||
|
||||
if _, err := cr.db.Exec(q, id, owner); err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errSaveUnknown, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
|
||||
q := `SELECT external_id, external_key FROM unknown_configs LIMIT $1 OFFSET $2`
|
||||
rows, err := cr.db.Query(q, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []bootstrap.Config{}
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{}
|
||||
if err := rows.Scan(&c.ExternalID, &c.ExternalKey); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM unknown_configs`)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count unknown configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: items,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveThing(id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1`
|
||||
_, err := cr.db.Exec(q, id)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -580,69 +580,6 @@ func TestListExisting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUnknown(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save unknown",
|
||||
externalID: "unknown",
|
||||
externalKey: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save invalid unknown",
|
||||
externalID: "unknown",
|
||||
externalKey: "",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.SaveUnknown(tc.externalKey, tc.externalID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveUnknown(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
id, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
repo.SaveUnknown(id.String(), id.String())
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all",
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset",
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveUnknown(tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveThing(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
|
||||
@@ -91,6 +91,15 @@ func migrateDB(db *sqlx.DB) error {
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_2",
|
||||
Up: []string{
|
||||
"DROP TABLE IF EXISTS unknown_configs",
|
||||
},
|
||||
Down: []string{
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// defers will not be run when using os.Exit
|
||||
// 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)
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/bootstrap/redis/producer"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -30,8 +30,6 @@ const (
|
||||
streamID = "mainflux.bootstrap"
|
||||
email = "user@example.com"
|
||||
validToken = "validToken"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
defaultTimout = 5
|
||||
|
||||
@@ -64,7 +62,7 @@ var (
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
configs := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
+7
-13
@@ -11,8 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,13 +36,15 @@ var (
|
||||
// ErrExternalKeyNotFound indicates a non-existent bootstrap configuration for given external key
|
||||
ErrExternalKeyNotFound = errors.New("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrSecureBootstrap indicates erron in getting bootstrap configuration for given encrypted 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")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
|
||||
errAddBootstrap = errors.New("failed to add bootstrap configuration")
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
@@ -272,10 +274,6 @@ func (bs bootstrapService) List(token string, filter Filter, offset, limit uint6
|
||||
return ConfigsPage{}, err
|
||||
}
|
||||
|
||||
if filter.Unknown {
|
||||
return bs.configs.RetrieveUnknown(offset, limit), nil
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
@@ -293,11 +291,7 @@ func (bs bootstrapService) Remove(token, id string) error {
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalID)
|
||||
if err != nil {
|
||||
if errors.Contains(err, ErrNotFound) {
|
||||
bs.configs.SaveUnknown(externalKey, externalID)
|
||||
return Config{}, ErrNotFound
|
||||
}
|
||||
return cfg, errors.Wrap(errBootstrap, err)
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
|
||||
if secure {
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -33,8 +33,6 @@ const (
|
||||
invalidToken = "invalidToken"
|
||||
email = "test@example.com"
|
||||
unknown = "unknown"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
)
|
||||
|
||||
@@ -56,7 +54,7 @@ var (
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
@@ -400,11 +398,6 @@ func TestList(t *testing.T) {
|
||||
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
|
||||
saved[41].State = bootstrap.Active
|
||||
|
||||
unknownConfig := bootstrap.Config{
|
||||
ExternalID: unknownID,
|
||||
ExternalKey: unknownKey,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.ConfigsPage
|
||||
@@ -479,20 +472,6 @@ func TestList(t *testing.T) {
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list unknown configs",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 20,
|
||||
Configs: []bootstrap.Config{unknownConfig},
|
||||
},
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -1,520 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform things configuration.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/things/configs:
|
||||
post:
|
||||
summary: Adds new config
|
||||
description: |
|
||||
Adds new config to the list of config owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the new config.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigReq"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /things/configs/{configId}).
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed configs
|
||||
description: |
|
||||
Retrieves a list of managed configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
- $ref: "#/parameters/State"
|
||||
- $ref: "#/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
description: |
|
||||
Data retrieved. Configs from this list don't contain channels.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/ConfigAuth"
|
||||
- $ref: "#/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/BootstrapRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config. Thing which attempted
|
||||
to bootstrap is saved as an unknown Thing and can be listed and
|
||||
added to the service later.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/EncConfigAuth"
|
||||
- $ref: "#/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
description: |
|
||||
Data retrieved. In this case, Bootstrap response is encrypted using
|
||||
the secret key, so an actual response is in the binary format.
|
||||
schema:
|
||||
$ref: "#/definitions/BootstrapRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config. Thing which attempted
|
||||
to bootstrap is saved as an unknown Thing and can be listed and
|
||||
added to the service later.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels)
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates config info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the owner, ID, external ID,
|
||||
external key, Mainflux Thing ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the Mainflux channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateCertReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/connections/{configId}:
|
||||
put:
|
||||
summary: Updates channels the thing is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Thing is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: channels
|
||||
description: Array if IDs the thing is be connected to.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateConnReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/state/{configId}:
|
||||
put:
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding Mainflux Thing to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: state
|
||||
description: New state of the Config.
|
||||
in: body
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
type: integer
|
||||
enum:
|
||||
- inactive
|
||||
- active
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/unknown/configs:
|
||||
get:
|
||||
summary: Get a list of unsuccessfully bootstrapped Things
|
||||
description: |
|
||||
Retrieves a list of unknown configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
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
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
type: integer
|
||||
enum:
|
||||
- inactive
|
||||
- active
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
|
||||
definitions:
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/definitions/ConfigRes"
|
||||
required:
|
||||
- configs
|
||||
State:
|
||||
type: integer
|
||||
enum:
|
||||
- active
|
||||
- inactive
|
||||
ConfigRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: ID of the Channel.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to the Channel.
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
state:
|
||||
$ref: '#/definitions/State'
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
BootstrapRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certrificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
|
||||
ConfigReq:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
thing_id:
|
||||
type: string
|
||||
description: ID of the corresponding Mainflux Thing.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigUpdateConnReq:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
ConfigUpdateCertReq:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
@@ -0,0 +1,53 @@
|
||||
# Certs Service
|
||||
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.
|
||||
|
||||
## Development mode
|
||||
If `MF_CERTS_VAULT_HOST` is empty than Development mode is on.
|
||||
|
||||
To issue a certificate:
|
||||
```bash
|
||||
|
||||
TOK=`curl -s --insecure -S -X POST http://localhost/tokens -H 'Content-Type: application/json' -d '{"email":"edge@email.com","password":"12345678"}' | jq -r '.token'`
|
||||
|
||||
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "rsa_bits":2048, "key_type":"rsa"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ThingID": "",
|
||||
"ClientCert": "-----BEGIN CERTIFICATE-----\nMIIDmTCCAoGgAwIBAgIRANmkAPbTR1UYeYO0Id/4+8gwDQYJKoZIhvcNAQELBQAw\nVzESMBAGA1UEAwwJbG9jYWxob3N0MREwDwYDVQQKDAhNYWluZmx1eDEMMAoGA1UE\nCwwDSW9UMSAwHgYJKoZIhvcNAQkBFhFpbmZvQG1haW5mbHV4LmNvbTAeFw0yMDA2\nMzAxNDIxMDlaFw0yMDA5MjMyMjIxMDlaMFUxETAPBgNVBAoTCE1haW5mbHV4MREw\nDwYDVQQLEwhtYWluZmx1eDEtMCsGA1UEAxMkYjAwZDBhNzktYjQ2YS00NTk3LTli\nNGYtMjhkZGJhNTBjYTYyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\ntgS2fLUWG3CCQz/l6VRQRJfRvWmdxK0mW6zIXGeeOILYZeaLiuiUnohwMJ4RiMqT\nuJbInAIuO/Tt5osfrCFFzPEOLYJ5nZBBaJfTIAxqf84Ou1oeMRll4wpzgeKx0rJO\nXMAARwn1bT9n3uky5QQGSLy4PyyILzSXH/1yCQQctdQB/Ar/UI1TaYoYlGzh7dHT\nWpcxq1HYgCyAtcrQrGD0rEwUn82UBCrnya+bygNqu0oDzIFQwa1G8jxSgXk0mFS1\nWrk7rBipsvp8HQhdnvbEVz4k4AAKcQxesH4DkRx/EXmU2UvN3XysvcJ2bL+UzMNI\njNhAe0pgPbB82F6zkYZ/XQIDAQABo2IwYDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0l\nBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQHBAUBAgMEBjAfBgNVHSME\nGDAWgBRs4xR91qEjNRGmw391xS7x6Tc+8jANBgkqhkiG9w0BAQsFAAOCAQEAW/dS\nV4vNLTZwBnPVHUX35pRFxPKvscY+vnnpgyDtITgZHYe0KL+Bs3IHuywtqaezU5x1\nkZo+frE1OcpRvp7HJtDiT06yz+18qOYZMappCWCeAFWtZkMhlvnm3TqTkgui6Xgl\nGj5xnPb15AOlsDE2dkv5S6kEwJGHdVX6AOWfB4ubUq5S9e4ABYzXGUty6Hw/ZUmJ\nhCTRVJ7cQJVTJsl1o7CYT8JBvUUG75LirtoFE4M4JwsfsKZXzrQffTf1ynqI3dN/\nHWySEbvTSWcRcA3MSmOTxGt5/zwCglHDlWPKMrXtjTW7NPuGL5/P9HSB9HGVVeET\nDUMdvYwgj0cUCEu3LA==\n-----END CERTIFICATE-----\n",
|
||||
"IssuingCA": "",
|
||||
"CAChain": null,
|
||||
"ClientKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtgS2fLUWG3CCQz/l6VRQRJfRvWmdxK0mW6zIXGeeOILYZeaL\niuiUnohwMJ4RiMqTuJbInAIuO/Tt5osfrCFFzPEOLYJ5nZBBaJfTIAxqf84Ou1oe\nMRll4wpzgeKx0rJOXMAARwn1bT9n3uky5QQGSLy4PyyILzSXH/1yCQQctdQB/Ar/\nUI1TaYoYlGzh7dHTWpcxq1HYgCyAtcrQrGD0rEwUn82UBCrnya+bygNqu0oDzIFQ\nwa1G8jxSgXk0mFS1Wrk7rBipsvp8HQhdnvbEVz4k4AAKcQxesH4DkRx/EXmU2UvN\n3XysvcJ2bL+UzMNIjNhAe0pgPbB82F6zkYZ/XQIDAQABAoIBAALoal3tqq+/iWU3\npR2oKiweXMxw3oNg3McEKKNJSH7QoFJob3xFoPIzbc9pBxCvY9LEHepYIpL0o8RW\nHqhqU6olg7t4ZSb+Qf1Ax6+wYxctnJCjrO3N4RHSfevqSjr6fEQBEUARSal4JNmr\n0hNUkCEjWrIvrPFMHsn1C5hXR3okJQpGsad4oCGZDp2eZ/NDyvmLBLci9/5CJdRv\n6roOF5ShWweKcz1+pfy666Q8RiUI7H1zXjPaL4yqkv8eg/WPOO0dYF2Ri2Grk9OY\n1qTM0W1vi9zfncinZ0DpgtwMTFQezGwhUyJHSYHmjVBA4AaYIyOQAI/2dl5fXM+O\n9JfXpOUCgYEA10xAtMc/8KOLbHCprpc4pbtOqfchq/M04qPKxQNAjqvLodrWZZgF\nexa+B3eWWn5MxmQMx18AjBCPwbNDK8Rkd9VqzdWempaSblgZ7y1a0rRNTXzN5DFP\noiuRQV4wszCuj5XSdPn+lxApaI/4+TQ0oweIZCpGW39XKePPoB5WZiMCgYEA2G3W\niJncRpmxWwrRPi1W26E9tWOT5s9wYgXWMc+PAVUd/qdDRuMBHpu861Qoghp/MJog\nBYqt2rQqU0OxvIXlXPrXPHXrCLOFwybRCBVREZrg4BZNnjyDTLOu9C+0M3J9ImCh\n3vniYqb7S0gRmoDM0R3Zu4+ajfP2QOGLXw1qHH8CgYEAl0EQ7HBW8V5UYzi7XNcM\nixKOb0YZt83DR74+hC6GujTjeLBfkzw8DX+qvWA8lxLIKVC80YxivAQemryv4h21\nX6Llx/nd1UkXUsI+ZhP9DK5y6I9XroseIRZuk/fyStFWsbVWB6xiOgq2rKkJBzqw\nCCEQpx40E6/gsqNDiIAHvvUCgYBkkjXc6FJ55DWMLuyozfzMtpKsVYeG++InSrsM\nDn1PizQS/7q9mAMPLCOP312rh5CPDy/OI3FCbfI1GwHerwG0QUP/bnQ3aOTBmKoN\n7YnsemIA/5w16bzBycWE5x3/wjXv4aOWr9vJJ/siMm0rtKp4ijyBcevKBxHpeGWB\nWAR1FQKBgGIqAxGnBpip9E24gH894BaGHHMpQCwAxARev6sHKUy27eFUd6ipoTva\n4Wv36iz3gxU4R5B0gyfnxBNiUab/z90cb5+6+FYO13kqjxRRZWffohk5nHlmFN9K\nea7KQHTfTdRhOLUzW2yVqLi9pzfTfA6Yqf3U1YD3bgnWrp1VQnjo\n-----END RSA PRIVATE KEY-----\n",
|
||||
"PrivateKeyType": "",
|
||||
"Serial": "",
|
||||
"Expire": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 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`.
|
||||
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:
|
||||
|
||||
```
|
||||
MF_CERTS_VAULT_HOST=vault-domain.com
|
||||
MF_CERTS_VAULT_PKI_PATH=<vault_pki_path>
|
||||
MF_CERTS_VAULT_ROLE=<vault_role>
|
||||
MF_CERTS_VAULT_TOKEN=<vault_acces_token>
|
||||
```
|
||||
|
||||
For lab purposes you can use docker-compose and script for setting up PKI in [https://github.com/mteodor/vault](https://github.com/mteodor/vault)
|
||||
|
||||
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"}'
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of certs service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
)
|
||||
|
||||
func issueCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addCertsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.Valid, req.KeyBits, req.KeyType)
|
||||
if err != nil {
|
||||
return certsResponse{Error: err.Error()}, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listCerts(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.ListCerts(ctx, req.token, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return certsPageRes{
|
||||
Error: err.Error(),
|
||||
}, err
|
||||
}
|
||||
res := certsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Certs: []certsResponse{},
|
||||
}
|
||||
|
||||
for _, cert := range page.Certs {
|
||||
view := certsResponse{
|
||||
Serial: cert.Serial,
|
||||
ThingID: cert.ThingID,
|
||||
}
|
||||
res.Certs = append(res.Certs, view)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(revokeReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.RevokeCert(ctx, req.token, req.ThingID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
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) {
|
||||
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 {
|
||||
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.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (cp certs.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_certs for token: %s took %s to complete", token, time.Since(begin))
|
||||
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.ListCerts(ctx, token, offset, limit)
|
||||
}
|
||||
|
||||
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))
|
||||
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.RevokeCert(ctx, token, thingID)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metrics.Histogram) certs.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, daysValid 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)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (certs.Page, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_certs").Add(1)
|
||||
ms.latency.With("method", "list_certs").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListCerts(ctx, token, offset, limit)
|
||||
}
|
||||
|
||||
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)
|
||||
ms.latency.With("method", "revoke_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RevokeCert(ctx, token, thingID)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/certs"
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addCertsReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
Valid string `json:"valid"`
|
||||
}
|
||||
|
||||
func (req addCertsReq) validate() error {
|
||||
if req.ThingID == "" && req.token == "" {
|
||||
return errUnauthorized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
token string
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req *listReq) validate() error {
|
||||
if req.token == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.limit == 0 || req.limit > maxLimitSize {
|
||||
return certs.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
}
|
||||
|
||||
func (req *revokeReq) validate() error {
|
||||
if req.token == "" || req.ThingID == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
}
|
||||
|
||||
type certsPageRes struct {
|
||||
pageRes
|
||||
Certs []certsResponse `json:"certs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type certsResponse struct {
|
||||
ClientCert map[string]string `json:"client_cert"`
|
||||
ClientKey map[string]string `json:"client_key"`
|
||||
Serial string `json:"serial"`
|
||||
ThingID string `json:"thing_id"`
|
||||
CACert string `json:"ca_cert"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (res certsPageRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (res certsResponse) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certsResponse) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certsResponse) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
offset = "offset"
|
||||
limit = "limit"
|
||||
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errUnauthorized = errors.New("missing or invalid credentials provided")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errMalformedEntity = errors.New("malformed entity")
|
||||
errConflict = errors.New("entity already exists")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc certs.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/certs", kithttp.NewServer(
|
||||
issueCert(svc),
|
||||
decodeCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/certs", kithttp.NewServer(
|
||||
listCerts(svc),
|
||||
decodeListCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/certs/revoke", kithttp.NewServer(
|
||||
revokeCert(svc),
|
||||
decodeRevokeCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.GetFunc("/version", mainflux.Version("certs"))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
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 decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := readUintQuery(r, limit, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o, err := readUintQuery(r, offset, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := listReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
limit: l,
|
||||
offset: o,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
strval := vals[0]
|
||||
val, err := strconv.ParseUint(strval, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addCertsReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := revokeReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case io.EOF, errMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import "context"
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list
|
||||
type Page struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Certs []Cert
|
||||
}
|
||||
|
||||
// Repository specifies a Config persistence API.
|
||||
type Repository interface {
|
||||
// Save saves cert for thing into database
|
||||
Save(ctx context.Context, cert Cert) (string, error)
|
||||
|
||||
// RetrieveAll retrieve all issued certificates for given owner
|
||||
RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// Remove certificate from DB for given thing
|
||||
Remove(ctx context.Context, thingID string) error
|
||||
|
||||
// RetrieveByThing certificate by given thing
|
||||
RetrieveByThing(ctx context.Context, thingID string) (Cert, error)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package certs contains the domain concept definitions needed to support
|
||||
// Mainflux certs service functionality.
|
||||
package certs
|
||||
@@ -0,0 +1,144 @@
|
||||
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/revoke:
|
||||
delete:
|
||||
summary: Revokes certificate
|
||||
description: |
|
||||
Revokes a certificates for given thing ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
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
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Certs:
|
||||
type: object
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
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
|
||||
- rsa_bits
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
days_valid:
|
||||
type: string
|
||||
rsa_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"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import "time"
|
||||
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
type Agent interface {
|
||||
// IssueCert issues certificate on PKI
|
||||
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
|
||||
// Revoke revokes certificate from PKI
|
||||
Revoke(serial string) (Revoke, error)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
issue = "issue"
|
||||
revoke = "revoke"
|
||||
apiVer = "v1"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedVaultCertIssue = errors.New("failed to issue vault certificate")
|
||||
errFailedCertDecoding = errors.New("failed to decode response from vault service")
|
||||
)
|
||||
|
||||
type pkiAgent struct {
|
||||
token string
|
||||
path string
|
||||
role string
|
||||
host string
|
||||
issueURL string
|
||||
revokeURL string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
type certReq struct {
|
||||
CommonName string `json:"common_name"`
|
||||
TTL string `json:"ttl"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
}
|
||||
|
||||
type certRevokeReq struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
}
|
||||
|
||||
func NewVaultClient(token, host, path, role string) (Agent, error) {
|
||||
conf := &api.Config{
|
||||
Address: host,
|
||||
}
|
||||
|
||||
client, err := api.NewClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.SetToken(token)
|
||||
p := pkiAgent{
|
||||
token: token,
|
||||
host: host,
|
||||
role: role,
|
||||
path: path,
|
||||
client: client,
|
||||
issueURL: "/" + apiVer + "/" + path + "/" + issue + "/" + role,
|
||||
revokeURL: "/" + apiVer + "/" + path + "/" + revoke,
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error) {
|
||||
cReq := certReq{
|
||||
CommonName: cn,
|
||||
TTL: ttl,
|
||||
KeyBits: keyBits,
|
||||
KeyType: keyType,
|
||||
}
|
||||
|
||||
r := p.client.NewRequest("POST", p.issueURL)
|
||||
if err := r.SetJSONBody(cReq); err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
return Cert{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, _ := api.ParseSecret(resp.Body)
|
||||
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) Revoke(serial string) (Revoke, error) {
|
||||
cReq := certRevokeReq{
|
||||
SerialNumber: serial,
|
||||
}
|
||||
|
||||
r := p.client.NewRequest("POST", p.revokeURL)
|
||||
if err := r.SetJSONBody(cReq); err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
return Revoke{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, err := api.ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
rev, err := s.Data["revocation_time"].(json.Number).Float64()
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
revTime := time.Unix(0, int64(rev)*int64(time.Millisecond))
|
||||
return Revoke{
|
||||
RevocationTime: revTime,
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"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 _ certs.Repository = (*certsRepository)(nil)
|
||||
|
||||
type Cert struct {
|
||||
ThingID string
|
||||
Serial string
|
||||
Expire time.Time
|
||||
}
|
||||
|
||||
type certsRepository struct {
|
||||
db *sqlx.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewRepository instantiates a PostgreSQL implementation of certs
|
||||
// repository.
|
||||
func NewRepository(db *sqlx.DB, log logger.Logger) certs.Repository {
|
||||
return &certsRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
|
||||
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 {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
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 {
|
||||
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`
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q, ownerID).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) Save(ctx context.Context, cert certs.Cert) (string, error) {
|
||||
q := `INSERT INTO certs (thing_id, owner_id, serial, expire) VALUES (:thing_id, :owner_id, :serial, :expire)`
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
}
|
||||
|
||||
dbcrt := toDBCert(cert)
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcrt); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = errors.New("error conflict")
|
||||
}
|
||||
|
||||
cr.rollback("Failed to insert a Cert", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, 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`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, serial).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)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) rollback(content string, tx *sqlx.Tx, err error) {
|
||||
cr.log.Error(fmt.Sprintf("%s %s", content, err))
|
||||
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
type dbCert struct {
|
||||
ThingID string `db:"thing_id"`
|
||||
Serial string `db:"serial"`
|
||||
Expire time.Time `db:"expire"`
|
||||
OwnerID string `db:"owner_id"`
|
||||
}
|
||||
|
||||
func toDBCert(c certs.Cert) dbCert {
|
||||
return dbCert{
|
||||
ThingID: c.ThingID,
|
||||
OwnerID: c.OwnerID,
|
||||
Serial: c.Serial,
|
||||
Expire: c.Expire,
|
||||
}
|
||||
}
|
||||
|
||||
func toCert(cdb dbCert) certs.Cert {
|
||||
var c certs.Cert
|
||||
c.OwnerID = cdb.OwnerID
|
||||
c.ThingID = cdb.ThingID
|
||||
c.Serial = cdb.Serial
|
||||
c.Expire = cdb.Expire
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
const primaryKey = "primary_key"
|
||||
|
||||
// ErrMigrate indicates error during database migrations.
|
||||
var ErrMigrate = errors.New("error executing database migrations")
|
||||
|
||||
// Config defines the options that are used when connecting to a PostgreSQL instance
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Name string
|
||||
SSLMode string
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
SSLRootCert string
|
||||
}
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance and applies any
|
||||
// unapplied database migrations. A non-nil error is returned to indicate
|
||||
// failure.
|
||||
func Connect(cfg Config) (*sqlx.DB, error) {
|
||||
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
|
||||
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
mErr, ok := err.(*migrate.TxError)
|
||||
if ok && mErr.Migration.Id == primaryKey {
|
||||
return db, ErrMigrate
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sqlx.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "certs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS certs (
|
||||
thing_id TEXT NOT NULL,
|
||||
owner_id TEXT NOT NULL,
|
||||
expire TIMESTAMPTZ NOT NULL,
|
||||
serial TEXT NOT NULL,
|
||||
PRIMARY KEY (thing_id, owner_id)
|
||||
);`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE IF EXISTS certs;",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/certs/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = logger.New(os.Stdout, logger.Info.String())
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err = sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/certs/pki"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
errFailedKeyCreation = errors.New("failed to create client private key")
|
||||
errFailedDateSetting = errors.New("failed to set date for certificate")
|
||||
errKeyBitsValueWrong = errors.New("missing RSA bits for certificate creation")
|
||||
errMissingCACertificate = errors.New("missing CA certificate for certificate signing")
|
||||
errFailedSerialGeneration = errors.New("failed to generate certificate serial")
|
||||
errFailedPemKeyWrite = errors.New("failed to write PEM key")
|
||||
errFailedPemDataWrite = errors.New("failed to write pem data for certificate")
|
||||
errPrivateKeyUnsupportedType = errors.New("private key type is unsupported")
|
||||
errPrivateKeyEmpty = errors.New("private key is empty")
|
||||
errFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
|
||||
errFailedCertCreation = errors.New("failed to create client certificate")
|
||||
errFailedCertRevocation = errors.New("failed to revoke certificate")
|
||||
)
|
||||
|
||||
var _ Service = (*certsService)(nil)
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// 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)
|
||||
|
||||
// ListCerts lists all certificates issued for given owner
|
||||
ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error)
|
||||
|
||||
// RevokeCert revokes certificate for given thing
|
||||
RevokeCert(ctx context.Context, token, thingID string) (Revoke, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
LogLevel string
|
||||
ClientTLS bool
|
||||
CaCerts string
|
||||
HTTPPort string
|
||||
ServerCert string
|
||||
ServerKey string
|
||||
BaseURL string
|
||||
ThingsPrefix string
|
||||
JaegerURL string
|
||||
AuthnURL string
|
||||
AuthnTimeout time.Duration
|
||||
SignTLSCert tls.Certificate
|
||||
SignX509Cert *x509.Certificate
|
||||
SignRSABits int
|
||||
SignHoursValid string
|
||||
PKIHost string
|
||||
PKIPath string
|
||||
PKIRole string
|
||||
PKIToken string
|
||||
}
|
||||
|
||||
type certsService struct {
|
||||
auth mainflux.AuthNServiceClient
|
||||
certsRepo Repository
|
||||
sdk mfsdk.SDK
|
||||
conf Config
|
||||
pki pki.Agent
|
||||
}
|
||||
|
||||
// New returns new Certs service.
|
||||
func New(auth mainflux.AuthNServiceClient, certs Repository, sdk mfsdk.SDK, config Config, pki pki.Agent) Service {
|
||||
return &certsService{
|
||||
certsRepo: certs,
|
||||
sdk: sdk,
|
||||
auth: auth,
|
||||
conf: config,
|
||||
pki: pki,
|
||||
}
|
||||
}
|
||||
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
OwnerID string `json:"owner_id" mapstructure:"owner_id"`
|
||||
ThingID string `json:"thing_id" mapstructure:"thing_id"`
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
|
||||
var c Cert
|
||||
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return c, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
// If PKIHost is not set we don't use 3rd party PKI service.
|
||||
if cs.conf.PKIHost == "" {
|
||||
c.ClientCert, c.ClientKey, err = cs.certs(thing.Key, daysValid, keyBits)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
cert, err := cs.pki.IssueCert(thingID, daysValid, keyType, keyBits)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
c.ThingID = thingID
|
||||
c.OwnerID = owner.GetValue()
|
||||
c.ClientCert = cert.ClientCert
|
||||
c.IssuingCA = cert.IssuingCA
|
||||
c.CAChain = cert.CAChain
|
||||
c.ClientKey = cert.ClientKey
|
||||
c.PrivateKeyType = cert.PrivateKeyType
|
||||
c.Serial = cert.Serial
|
||||
c.Expire = cert.Expire
|
||||
|
||||
_, err = cs.certsRepo.Save(context.Background(), c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
|
||||
var revoke Revoke
|
||||
_, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrUnauthorizedAccess, 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)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
r, err := cs.pki.Revoke(cert.Serial)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
revoke.RevocationTime = r.RevocationTime
|
||||
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
|
||||
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
|
||||
}
|
||||
return revoke, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return cs.certsRepo.RetrieveAll(ctx, u.GetValue(), offset, limit)
|
||||
}
|
||||
|
||||
func (cs *certsService) certs(thingKey, daysValid string, keyBits int) (string, string, error) {
|
||||
if cs.conf.SignX509Cert == nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, errMissingCACertificate)
|
||||
}
|
||||
if keyBits == 0 {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, errKeyBitsValueWrong)
|
||||
}
|
||||
var priv interface{}
|
||||
priv, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedKeyCreation, err)
|
||||
}
|
||||
|
||||
if daysValid == "" {
|
||||
daysValid = cs.conf.SignHoursValid
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
validFor, err := time.ParseDuration(daysValid)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedDateSetting, err)
|
||||
}
|
||||
notAfter := notBefore.Add(validFor)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedSerialGeneration, err)
|
||||
}
|
||||
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Mainflux"},
|
||||
CommonName: thingKey,
|
||||
OrganizationalUnit: []string{"mainflux"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||
}
|
||||
|
||||
pubKey, err := publicKey(priv)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, cs.conf.SignX509Cert, pubKey, cs.conf.SignTLSCert.PrivateKey)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
var bw, keyOut bytes.Buffer
|
||||
buffWriter := bufio.NewWriter(&bw)
|
||||
buffKeyOut := bufio.NewWriter(&keyOut)
|
||||
|
||||
if err := pem.Encode(buffWriter, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemDataWrite, err)
|
||||
}
|
||||
buffWriter.Flush()
|
||||
cert := bw.String()
|
||||
|
||||
block, err := pemBlockForKey(priv)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
||||
}
|
||||
if err := pem.Encode(buffKeyOut, block); err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
||||
}
|
||||
buffKeyOut.Flush()
|
||||
key := keyOut.String()
|
||||
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
func publicKey(priv interface{}) (interface{}, error) {
|
||||
if priv == nil {
|
||||
return nil, errPrivateKeyEmpty
|
||||
}
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return nil, errPrivateKeyUnsupportedType
|
||||
}
|
||||
}
|
||||
|
||||
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
b, err := x509.MarshalECPrivateKey(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
@@ -202,3 +202,41 @@ mainflux-cli bootstrap remove <thing_id> <user_auth_token>
|
||||
```bash
|
||||
mainflux-cli bootstrap bootstrap <external_id> <external_key>
|
||||
```
|
||||
|
||||
### Groups
|
||||
#### Create new group
|
||||
```bash
|
||||
mainflux-cli groups create '{"name":"<group_name>","parent_id":"<parent_group_id>","description":"<description>","metadata":{"key":"value",...}}' <user_auth_token>
|
||||
```
|
||||
#### Delete group
|
||||
```bash
|
||||
mainflux-cli groups delete <group_id> <user_auth_token>
|
||||
```
|
||||
#### Get group with id
|
||||
```bash
|
||||
mainflux-cli groups get <group_id> <user_auth_token>
|
||||
```
|
||||
#### List all groups
|
||||
```bash
|
||||
mainflux-cli groups get all <user_auth_token>
|
||||
```
|
||||
#### List children groups for some group
|
||||
```bash
|
||||
mainflux-cli groups get children <parent_group_id> <user_auth_token>
|
||||
```
|
||||
#### Assign user to a group
|
||||
```bash
|
||||
mainflux-cli groups assign <user_id> <group_id> <user_auth_token>
|
||||
```
|
||||
#### Unassign user from group
|
||||
```bash
|
||||
mainflux-cli groups unassign <user_id> <group_id> <user_auth_token>
|
||||
```
|
||||
#### List users for a group
|
||||
```bash
|
||||
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>
|
||||
```
|
||||
+1
-1
@@ -6,7 +6,7 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdCerts = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "issue <thing_id> <keybits> <keytype> <hoursvalid> <user_auth_token>",
|
||||
Long: `Issues new certificate for a thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 5 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
thingID := args[0]
|
||||
keyBits, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
logError(errors.New("invalid format for keybits"))
|
||||
return
|
||||
}
|
||||
|
||||
keyType := args[2]
|
||||
valid := args[3]
|
||||
token := args[4]
|
||||
|
||||
c, err := sdk.IssueCert(thingID, keyBits, keyType, valid, token)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewCertsCmd returns certificate command.
|
||||
func NewCertsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "cert",
|
||||
Short: "Certificate management",
|
||||
Long: `Certificate management: create certificates for things"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("cert issue <thing_id> <keybits> <keytype> <hoursvalid> <user_auth_token>")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdCerts {
|
||||
cmd.AddCommand(&cmdCerts[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
+24
-5
@@ -6,7 +6,7 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -111,14 +111,33 @@ var cmdChannels = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "connections",
|
||||
Short: "connections <channel_id> <user_auth_token>",
|
||||
Long: `List of Things connected to Channel`,
|
||||
Long: `List of Things connected to a Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit), false)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "not-connected",
|
||||
Short: "not-connected <channel_id> <user_auth_token>",
|
||||
Long: `List of Things not connected to a Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit), false)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
@@ -134,9 +153,9 @@ func NewChannelsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "channels",
|
||||
Short: "Channels management",
|
||||
Long: `Channels management: create, get, update or delete Channel and get list of Things connected to Channel`,
|
||||
Long: `Channels management: create, get, update or delete Channel and get list of Things connected or not connected to a Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("channels [create | get | update | delete | connections]")
|
||||
logUsage("channels [create | get | update | delete | connections | not-connected]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdGroups = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create <JSON_group> <user_auth_token>",
|
||||
Long: `Creates new group
|
||||
JSON_group:
|
||||
{
|
||||
"Name":<group_name>,
|
||||
"Description":<description>,
|
||||
"ParentID":<parent_id>,
|
||||
"Metadata":<metadata>,
|
||||
}
|
||||
Name - is unique group name
|
||||
ParentID - ID of a group that is a parent to the creating group
|
||||
Metadata - JSON structured string`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
var group mfxsdk.Group
|
||||
if err := json.Unmarshal([]byte(args[0]), &group); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
id, err := sdk.CreateGroup(group, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get [all | children <group_id> | group_id] <user_auth_token>",
|
||||
Long: `Get all users groups, group children or group by id.
|
||||
all - lists all groups
|
||||
children <group_id> - lists all children groups of <group_id>
|
||||
<group_id> - shows group with provided group ID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if args[0] == "all" {
|
||||
l, err := sdk.Groups(args[1], uint64(Offset), uint64(Limit), "")
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
if args[0] == "children" {
|
||||
l, err := sdk.Groups(args[2], uint64(Offset), uint64(Limit), args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
t, err := sdk.Group(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(t)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "assign",
|
||||
Short: "assign <user_id> <group_id> <user_auth_token>",
|
||||
Long: `Assign user to a group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if err := sdk.Assign(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "unassign",
|
||||
Short: "unassign <user_id> <group_id> <user_auth_token>",
|
||||
Long: `Unassign user from a group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if err := sdk.Unassign(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete <group_id> <user_auth_token>",
|
||||
Long: `Delete users group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if err := sdk.DeleteGroup(args[0], args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "members",
|
||||
Short: "members <group_id> <user_auth_token>",
|
||||
Long: `Lists all user members of a group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
up, err := sdk.Members(args[0], args[1], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(up)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "membership",
|
||||
Short: "membership <user_id> <user_auth_token>",
|
||||
Long: `List user groups membership`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
up, err := sdk.Memberships(args[0], args[1], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(up)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewGroupsCmd returns users command.
|
||||
func NewGroupsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "groups",
|
||||
Short: "Groups management",
|
||||
Long: `Groups management: create groups and assigns user to groups"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("Usage: Groups [create | get | delete | assign | unassign | members | membership]")
|
||||
},
|
||||
}
|
||||
for i := range cmdGroups {
|
||||
cmd.AddCommand(&cmdGroups[i])
|
||||
}
|
||||
return &cmd
|
||||
}
|
||||
+2
-2
@@ -13,7 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -124,7 +124,7 @@ var cmdProvision = []cobra.Command{
|
||||
Email: un,
|
||||
Password: "12345678",
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
if _, err := sdk.CreateUser(user); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
package cli
|
||||
|
||||
import mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
import mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
|
||||
// Keep SDK handle in global var
|
||||
var sdk mfxsdk.SDK
|
||||
|
||||
+23
-4
@@ -6,7 +6,7 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -157,7 +157,26 @@ var cmdThings = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit), true)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "not-connected",
|
||||
Short: "not-connected <thing_id> <user_auth_token>",
|
||||
Long: `List of Channels not connected to a Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit), false)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
@@ -173,9 +192,9 @@ func NewThingsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "things",
|
||||
Short: "Things management",
|
||||
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected to Thing`,
|
||||
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected or disconnected from a Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("things [create | get | update | delete | connect | disconnect | connections]")
|
||||
logUsage("things [create | get | update | delete | connect | disconnect | connections | not-connected]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -6,7 +6,7 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -25,12 +25,13 @@ var cmdUsers = []cobra.Command{
|
||||
Email: args[0],
|
||||
Password: args[1],
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
id, err := sdk.CreateUser(user)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
|
||||
+3
-3
@@ -21,8 +21,8 @@ import (
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
"github.com/mainflux/mainflux/authn/tracing"
|
||||
mfidp "github.com/mainflux/mainflux/authn/uuid"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
@@ -179,9 +179,9 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, secret string, logger lo
|
||||
database := postgres.NewDatabase(db)
|
||||
repo := tracing.New(postgres.New(database), tracer)
|
||||
|
||||
idp := mfidp.New()
|
||||
up := uuidProvider.New()
|
||||
t := jwt.New(secret)
|
||||
svc := authn.New(repo, idp, t)
|
||||
svc := authn.New(repo, up, t)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
api "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
@@ -65,7 +65,7 @@ const (
|
||||
defESConsumerName = "bootstrap"
|
||||
defJaegerURL = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1" // in seconds
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_BOOTSTRAP_LOG_LEVEL"
|
||||
envDBHost = "MF_BOOTSTRAP_DB_HOST"
|
||||
@@ -178,7 +178,7 @@ func loadConfig() config {
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthnTimeout, defAuthnTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func loadConfig() config {
|
||||
esConsumerName: mainflux.Env(envESConsumerName, defESConsumerName),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: time.Duration(timeout) * time.Second,
|
||||
authnTimeout: authnTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const (
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "messages"
|
||||
defKeyspace = "mainflux"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBPort = "9042"
|
||||
@@ -47,7 +47,7 @@ const (
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_CASSANDRA_READER_LOG_LEVEL"
|
||||
envPort = "MF_CASSANDRA_READER_PORT"
|
||||
@@ -131,7 +131,7 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func loadConfig() config {
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(timeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/cassandra"
|
||||
@@ -33,7 +33,7 @@ const (
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "messages"
|
||||
defKeyspace = "mainflux"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBPort = "9042"
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/certs/api"
|
||||
vault "github.com/mainflux/mainflux/certs/pki"
|
||||
"github.com/mainflux/mainflux/certs/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDB = "certs"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8204"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defBaseURL = "http://localhost"
|
||||
defThingsPrefix = ""
|
||||
defJaegerURL = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
defSignCAPath = "ca.crt"
|
||||
defSignCAKeyPath = "ca.key"
|
||||
defSignHoursValid = "2048h"
|
||||
defSignRSABits = ""
|
||||
|
||||
defVaultHost = ""
|
||||
defVaultRole = "mainflux"
|
||||
defVaultToken = ""
|
||||
defVaultPKIPath = "pki_int"
|
||||
|
||||
envPort = "MF_CERTS_HTTP_PORT"
|
||||
envLogLevel = "MF_CERTS_LOG_LEVEL"
|
||||
envDBHost = "MF_CERTS_DB_HOST"
|
||||
envDBPort = "MF_CERTS_DB_PORT"
|
||||
envDBUser = "MF_CERTS_DB_USER"
|
||||
envDBPass = "MF_CERTS_DB_PASS"
|
||||
envDB = "MF_CERTS_DB"
|
||||
envDBSSLMode = "MF_CERTS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_CERTS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_CERTS_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_CERTS_DB_SSL_ROOT_CERT"
|
||||
envEncryptKey = "MF_CERTS_ENCRYPT_KEY"
|
||||
envClientTLS = "MF_CERTS_CLIENT_TLS"
|
||||
envCACerts = "MF_CERTS_CA_CERTS"
|
||||
envServerCert = "MF_CERTS_SERVER_CERT"
|
||||
envServerKey = "MF_CERTS_SERVER_KEY"
|
||||
envBaseURL = "MF_SDK_BASE_URL"
|
||||
envThingsPrefix = "MF_SDK_THINGS_PREFIX"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envAuthnURL = "MF_AUTHN_GRPC_URL"
|
||||
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"
|
||||
|
||||
envSignCAPath = "MF_CERTS_SIGN_CA_PATH"
|
||||
envSignCAKey = "MF_CERTS_SIGN_CA_KEY_PATH"
|
||||
envSignHoursValid = "MF_CERTS_SIGN_HOURS_VALID"
|
||||
envSignRSABits = "MF_CERTS_SIGN_RSA_BITS"
|
||||
|
||||
envVaultHost = "MF_CERTS_VAULT_HOST"
|
||||
envVaultPKIPath = "MF_CERTS_VAULT_PKI_PATH"
|
||||
envVaultRole = "MF_CERTS_VAULT_ROLE"
|
||||
envVaultToken = "MF_CERTS_VAULT_TOKEN"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedCertLoading = errors.New("failed to load certificate")
|
||||
errFailedCertDecode = errors.New("failed to decode certificate")
|
||||
errMissingCACertificate = errors.New("missing CA")
|
||||
errPrivateKeyEmpty = errors.New("private key empty")
|
||||
errPrivateKeyUnsupportedType = errors.New("private key unsupported type")
|
||||
errCertsRemove = errors.New("failed to remove certificate")
|
||||
errCACertificateDoesntExist = errors.New("CA certificate doesnt exist")
|
||||
errCAKeyDoesntExist = errors.New("CA certificate key doesnt exist")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
clientTLS bool
|
||||
encKey []byte
|
||||
caCerts string
|
||||
httpPort string
|
||||
serverCert string
|
||||
serverKey string
|
||||
baseURL string
|
||||
thingsPrefix string
|
||||
jaegerURL string
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
// Sign and issue certificates
|
||||
// without 3rd party PKI
|
||||
signCAPath string
|
||||
signCAKeyPath string
|
||||
signRSABits int
|
||||
signHoursValid string
|
||||
// 3rd party PKI API access settings
|
||||
pkiPath string
|
||||
pkiToken string
|
||||
pkiHost string
|
||||
pkiRole string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := mflog.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
tlsCert, caCert, err := loadCertificates(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to load CA certificates for issuing client certs")
|
||||
}
|
||||
|
||||
pkiClient, err := vault.NewVaultClient(cfg.pkiToken, cfg.pkiHost, cfg.pkiPath, cfg.pkiRole)
|
||||
if err != nil {
|
||||
logger.Error("Failed to init vault client")
|
||||
}
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger)
|
||||
defer authCloser.Close()
|
||||
|
||||
authConn := connectToAuth(cfg, logger)
|
||||
defer authConn.Close()
|
||||
|
||||
auth := authapi.NewClient(authTracer, authConn, cfg.authnTimeout)
|
||||
|
||||
svc := newService(auth, db, logger, nil, tlsCert, caCert, cfg, pkiClient)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Certs service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
tls = false
|
||||
}
|
||||
dbConfig := postgres.Config{
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
|
||||
signRSABits, err := strconv.Atoi(mainflux.Env(envSignRSABits, defSignRSABits))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envSignRSABits, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
httpPort: mainflux.Env(envPort, defPort),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
baseURL: mainflux.Env(envBaseURL, defBaseURL),
|
||||
thingsPrefix: mainflux.Env(envThingsPrefix, defThingsPrefix),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
|
||||
signCAKeyPath: mainflux.Env(envSignCAKey, defSignCAKeyPath),
|
||||
signCAPath: mainflux.Env(envSignCAPath, defSignCAPath),
|
||||
signHoursValid: mainflux.Env(envSignHoursValid, defSignHoursValid),
|
||||
signRSABits: signRSABits,
|
||||
|
||||
pkiToken: mainflux.Env(envVaultToken, defVaultToken),
|
||||
pkiPath: mainflux.Env(envVaultPKIPath, defVaultPKIPath),
|
||||
pkiRole: mainflux.Env(envVaultRole, defVaultRole),
|
||||
pkiHost: mainflux.Env(envVaultHost, defVaultHost),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func connectToRedis(redisURL, redisPass, redisDB string, logger mflog.Logger) *redis.Client {
|
||||
db, err := strconv.Atoi(redisDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: redisURL,
|
||||
Password: redisPass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sqlx.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create tls credentials: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.authnURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to authn service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer, io.Closer) {
|
||||
if url == "" {
|
||||
return opentracing.NoopTracer{}, ioutil.NopCloser(nil)
|
||||
}
|
||||
|
||||
tracer, closer, err := jconfig.Configuration{
|
||||
ServiceName: svcName,
|
||||
Sampler: &jconfig.SamplerConfig{
|
||||
Type: "const",
|
||||
Param: 1,
|
||||
},
|
||||
Reporter: &jconfig.ReporterConfig{
|
||||
LocalAgentHostPort: url,
|
||||
LogSpans: true,
|
||||
},
|
||||
}.NewTracer()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to init Jaeger client: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, db *sqlx.DB, logger mflog.Logger, esClient *redis.Client, tlsCert tls.Certificate, x509Cert *x509.Certificate, cfg config, pkiAgent vault.Agent) certs.Service {
|
||||
certsRepo := postgres.NewRepository(db, logger)
|
||||
|
||||
certsConfig := certs.Config{
|
||||
LogLevel: cfg.logLevel,
|
||||
ClientTLS: cfg.clientTLS,
|
||||
CaCerts: cfg.caCerts,
|
||||
HTTPPort: cfg.httpPort,
|
||||
ServerCert: cfg.serverCert,
|
||||
ServerKey: cfg.serverKey,
|
||||
BaseURL: cfg.baseURL,
|
||||
ThingsPrefix: cfg.thingsPrefix,
|
||||
JaegerURL: cfg.jaegerURL,
|
||||
AuthnURL: cfg.authnURL,
|
||||
AuthnTimeout: cfg.authnTimeout,
|
||||
SignTLSCert: tlsCert,
|
||||
SignX509Cert: x509Cert,
|
||||
SignHoursValid: cfg.signHoursValid,
|
||||
SignRSABits: cfg.signRSABits,
|
||||
PKIToken: cfg.pkiToken,
|
||||
PKIHost: cfg.pkiHost,
|
||||
PKIPath: cfg.pkiPath,
|
||||
PKIRole: cfg.pkiRole,
|
||||
}
|
||||
|
||||
config := mfsdk.Config{
|
||||
BaseURL: cfg.baseURL,
|
||||
ThingsPrefix: cfg.thingsPrefix,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
|
||||
svc := certs.New(auth, certsRepo, sdk, certsConfig, pkiAgent)
|
||||
svc = api.NewLoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "certs",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "certs",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc certs.Service, cfg config, logger mflog.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Certs service started using https on port %s with cert %s key %s",
|
||||
cfg.httpPort, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, api.MakeHandler(svc))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Certs service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func loadCertificates(conf config) (tls.Certificate, *x509.Certificate, error) {
|
||||
var tlsCert tls.Certificate
|
||||
var caCert *x509.Certificate
|
||||
|
||||
if conf.signCAPath == "" || conf.signCAKeyPath == "" {
|
||||
return tlsCert, caCert, nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(conf.signCAPath); os.IsNotExist(err) {
|
||||
return tlsCert, caCert, errCACertificateDoesntExist
|
||||
}
|
||||
|
||||
if _, err := os.Stat(conf.signCAKeyPath); os.IsNotExist(err) {
|
||||
return tlsCert, caCert, errCAKeyDoesntExist
|
||||
}
|
||||
|
||||
tlsCert, err := tls.LoadX509KeyPair(conf.signCAPath, conf.signCAKeyPath)
|
||||
if err != nil {
|
||||
return tlsCert, caCert, errors.Wrap(errFailedCertLoading, err)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(conf.signCAPath)
|
||||
if err != nil {
|
||||
return tlsCert, caCert, errors.Wrap(errFailedCertLoading, err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(b)
|
||||
if block == nil {
|
||||
log.Fatalf("No PEM data found, failed to decode CA")
|
||||
}
|
||||
|
||||
caCert, err = x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return tlsCert, caCert, errors.Wrap(errFailedCertDecode, err)
|
||||
}
|
||||
|
||||
return tlsCert, caCert, nil
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
+15
-1
@@ -7,7 +7,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/mainflux/mainflux/cli"
|
||||
"github.com/mainflux/mainflux/sdk/go"
|
||||
sdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -17,8 +17,10 @@ func main() {
|
||||
BaseURL: "http://localhost",
|
||||
ReaderURL: "http://localhost:8905",
|
||||
BootstrapURL: "http://localhost:8202",
|
||||
CertsURL: "http://localhost:8204",
|
||||
ReaderPrefix: "",
|
||||
UsersPrefix: "",
|
||||
GroupsPrefix: "",
|
||||
ThingsPrefix: "",
|
||||
HTTPAdapterPrefix: "http",
|
||||
BootstrapPrefix: "things",
|
||||
@@ -40,19 +42,23 @@ func main() {
|
||||
versionCmd := cli.NewVersionCmd()
|
||||
usersCmd := cli.NewUsersCmd()
|
||||
thingsCmd := cli.NewThingsCmd()
|
||||
groupsCmd := cli.NewGroupsCmd()
|
||||
channelsCmd := cli.NewChannelsCmd()
|
||||
messagesCmd := cli.NewMessagesCmd()
|
||||
provisionCmd := cli.NewProvisionCmd()
|
||||
bootstrapCmd := cli.NewBootstrapCmd()
|
||||
certsCmd := cli.NewCertsCmd()
|
||||
|
||||
// Root Commands
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(usersCmd)
|
||||
rootCmd.AddCommand(groupsCmd)
|
||||
rootCmd.AddCommand(thingsCmd)
|
||||
rootCmd.AddCommand(channelsCmd)
|
||||
rootCmd.AddCommand(messagesCmd)
|
||||
rootCmd.AddCommand(provisionCmd)
|
||||
rootCmd.AddCommand(bootstrapCmd)
|
||||
rootCmd.AddCommand(certsCmd)
|
||||
|
||||
// Root Flags
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
@@ -79,6 +85,14 @@ func main() {
|
||||
"Mainflux things service prefix",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&sdkConf.GroupsPrefix,
|
||||
"groups-prefix",
|
||||
"g",
|
||||
sdkConf.GroupsPrefix,
|
||||
"Mainflux groups service prefix",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&sdkConf.HTTPAdapterPrefix,
|
||||
"http-prefix",
|
||||
|
||||
+13
-28
@@ -15,15 +15,15 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/api"
|
||||
logger "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
broker "github.com/nats-io/nats.go"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
gocoap "github.com/plgd-dev/go-coap/v2"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
@@ -36,17 +36,15 @@ const (
|
||||
defLogLevel = "error"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPingPeriod = "12"
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envPort = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_COAP_ADAPTER_LOG_LEVEL"
|
||||
envClientTLS = "MF_COAP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_COAP_ADAPTER_CA_CERTS"
|
||||
envPingPeriod = "MF_COAP_ADAPTER_PING_PERIOD"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
@@ -58,7 +56,6 @@ type config struct {
|
||||
logLevel string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
pingPeriod time.Duration
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
@@ -78,17 +75,15 @@ func main() {
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
respChan := make(chan string, 10000)
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, "", logger)
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
defer pubSub.Close()
|
||||
defer nc.Close()
|
||||
|
||||
svc := coap.New(pubSub, logger, cc, respChan)
|
||||
svc := coap.New(tc, nc)
|
||||
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
|
||||
@@ -111,7 +106,7 @@ func main() {
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(cfg.port, logger, errs)
|
||||
go startCOAPServer(cfg, svc, cc, respChan, logger, errs)
|
||||
go startCOAPServer(cfg, svc, nil, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
@@ -129,16 +124,7 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
pp, err := strconv.ParseInt(mainflux.Env(envPingPeriod, defPingPeriod), 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envPingPeriod)
|
||||
}
|
||||
|
||||
if pp < 1 || pp > 24 {
|
||||
log.Fatalf("Value of %s must be between 1 and 24", envPingPeriod)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
@@ -149,10 +135,9 @@ func loadConfig() config {
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
pingPeriod: time.Duration(pp),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(timeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +195,8 @@ func startHTTPServer(port string, logger logger.Logger, errs chan error) {
|
||||
errs <- http.ListenAndServe(p, api.MakeHTTPHandler())
|
||||
}
|
||||
|
||||
func startCOAPServer(cfg config, svc coap.Service, auth mainflux.ThingsServiceClient, respChan chan<- string, l logger.Logger, errs chan error) {
|
||||
func startCOAPServer(cfg config, svc coap.Service, auth mainflux.ThingsServiceClient, l logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
l.Info(fmt.Sprintf("CoAP adapter service started, exposed port %s", cfg.port))
|
||||
errs <- gocoap.ListenAndServe("udp", p, api.MakeCOAPHandler(svc, auth, l, respChan, cfg.pingPeriod))
|
||||
errs <- gocoap.ListenAndServe("udp", p, api.MakeCoAPHandler(svc, l))
|
||||
}
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ import (
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_HTTP_ADAPTER_LOG_LEVEL"
|
||||
envClientTLS = "MF_HTTP_ADAPTER_CLIENT_TLS"
|
||||
@@ -129,7 +129,7 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func loadConfig() config {
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(timeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDB = "messages"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_INFLUX_READER_LOG_LEVEL"
|
||||
envPort = "MF_INFLUX_READER_PORT"
|
||||
@@ -118,7 +118,7 @@ func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(timeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
|
||||
clientCfg := influxdata.HTTPConfig{
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
influxdata "github.com/influxdata/influxdb/client/v2"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/influxdb"
|
||||
@@ -29,7 +29,7 @@ const (
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDB = "messages"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/mainflux/mainflux/lora"
|
||||
"github.com/mainflux/mainflux/lora/api"
|
||||
"github.com/mainflux/mainflux/lora/mqtt"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux/lora/redis"
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDB = "messages"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defClientTLS = "false"
|
||||
@@ -44,7 +44,7 @@ const (
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_MONGO_READER_LOG_LEVEL"
|
||||
envPort = "MF_MONGO_READER_PORT"
|
||||
@@ -56,7 +56,7 @@ const (
|
||||
envServerCert = "MF_MONGO_READER_SERVER_CERT"
|
||||
envServerKey = "MF_MONGO_READER_SERVER_KEY"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_ThINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ func loadConfigs() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func loadConfigs() config {
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(timeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/mongodb"
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
defLogLevel = "error"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defPort = "8180"
|
||||
defDB = "messages"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defSubjectsCfgPath = "/config/subjects.toml"
|
||||
|
||||
+43
-40
@@ -15,11 +15,12 @@ import (
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
mqttpub "github.com/mainflux/mainflux/messaging/mqtt"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/mqtt"
|
||||
mqttredis "github.com/mainflux/mainflux/mqtt/redis"
|
||||
"github.com/mainflux/mainflux/pkg/auth"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
mqttpub "github.com/mainflux/mainflux/pkg/messaging/mqtt"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
mp "github.com/mainflux/mproxy/pkg/mqtt"
|
||||
"github.com/mainflux/mproxy/pkg/session"
|
||||
@@ -35,35 +36,28 @@ const (
|
||||
defLogLevel = "error"
|
||||
envLogLevel = "MF_MQTT_ADAPTER_LOG_LEVEL"
|
||||
// MQTT
|
||||
defMQTTHost = "0.0.0.0"
|
||||
defMQTTPort = "1883"
|
||||
defMQTTTargetHost = "0.0.0.0"
|
||||
defMQTTTargetPort = "1883"
|
||||
defMQTTForwarderTimeout = "30" // in seconds
|
||||
|
||||
envMQTTHost = "MF_MQTT_ADAPTER_MQTT_HOST"
|
||||
defMQTTForwarderTimeout = "30s" // 30 seconds
|
||||
envMQTTPort = "MF_MQTT_ADAPTER_MQTT_PORT"
|
||||
envMQTTTargetHost = "MF_MQTT_ADAPTER_MQTT_TARGET_HOST"
|
||||
envMQTTTargetPort = "MF_MQTT_ADAPTER_MQTT_TARGET_PORT"
|
||||
envMQTTForwarderTimeout = "MF_MQTT_ADAPTER_FORWARDER_TIMEOUT"
|
||||
// HTTP
|
||||
defHTTPHost = "0.0.0.0"
|
||||
defHTTPPort = "8080"
|
||||
defHTTPScheme = "ws"
|
||||
defHTTPTargetHost = "localhost"
|
||||
defHTTPTargetPort = "8080"
|
||||
defHTTPTargetPath = "/mqtt"
|
||||
envHTTPHost = "MF_MQTT_ADAPTER_WS_HOST"
|
||||
envHTTPPort = "MF_MQTT_ADAPTER_WS_PORT"
|
||||
envHTTPScheme = "MF_MQTT_ADAPTER_WS_SCHEMA"
|
||||
envHTTPTargetHost = "MF_MQTT_ADAPTER_WS_TARGET_HOST"
|
||||
envHTTPTargetPort = "MF_MQTT_ADAPTER_WS_TARGET_PORT"
|
||||
envHTTPTargetPath = "MF_MQTT_ADAPTER_WS_TARGET_PATH"
|
||||
// Things
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMMEOUT"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
// Nats
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
@@ -85,17 +79,21 @@ const (
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
// Auth cache
|
||||
envAuthCacheURL = "MF_AUTH_CACHE_URL"
|
||||
envAuthCachePass = "MF_AUTH_CACHE_PASS"
|
||||
envAuthCacheDB = "MF_AUTH_CACHE_DB"
|
||||
defAuthcacheURL = "localhost:6379"
|
||||
defAuthCachePass = ""
|
||||
defAuthCacheDB = "0"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
mqttHost string
|
||||
mqttPort string
|
||||
mqttTargetHost string
|
||||
mqttTargetPort string
|
||||
mqttForwarderTimeout time.Duration
|
||||
httpHost string
|
||||
httpPort string
|
||||
httpScheme string
|
||||
httpTargetHost string
|
||||
httpTargetPort string
|
||||
httpTargetPath string
|
||||
@@ -111,6 +109,9 @@ type config struct {
|
||||
esURL string
|
||||
esPass string
|
||||
esDB string
|
||||
authURL string
|
||||
authPass string
|
||||
authDB string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -124,16 +125,8 @@ func main() {
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
tracer, closer := initJaeger("mproxy", cfg.jaegerURL, logger)
|
||||
defer closer.Close()
|
||||
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
rc := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer rc.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
ec := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer ec.Close()
|
||||
|
||||
nps, err := nats.NewPubSub(cfg.natsURL, "mqtt", logger)
|
||||
if err != nil {
|
||||
@@ -141,11 +134,13 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nps.Close()
|
||||
|
||||
mp, err := mqttpub.NewPublisher(fmt.Sprintf("%s:%s", cfg.mqttTargetHost, cfg.mqttTargetPort), cfg.mqttForwarderTimeout)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create MQTT publisher: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fwd := mqtt.NewForwarder(nats.SubjectAllChannels, logger)
|
||||
if err := fwd.Forward(nps, mp); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to forward NATS messages: %s", err))
|
||||
@@ -159,10 +154,19 @@ func main() {
|
||||
}
|
||||
defer np.Close()
|
||||
|
||||
es := mqttredis.NewEventStore(rc, cfg.instance)
|
||||
es := mqttredis.NewEventStore(ec, cfg.instance)
|
||||
|
||||
ac := connectToRedis(cfg.authURL, cfg.authPass, cfg.authDB, logger)
|
||||
defer ac.Close()
|
||||
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
authClient := auth.New(ac, tc)
|
||||
|
||||
// Event handler for MQTT hooks
|
||||
h := mqtt.NewHandler([]messaging.Publisher{np}, cc, es, logger, tracer)
|
||||
h := mqtt.NewHandler([]messaging.Publisher{np}, es, logger, authClient)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
@@ -188,31 +192,28 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
authTimeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
mqttTimeout, err := strconv.ParseInt(mainflux.Env(envMQTTForwarderTimeout, defMQTTForwarderTimeout), 10, 64)
|
||||
mqttTimeout, err := time.ParseDuration(mainflux.Env(envMQTTForwarderTimeout, defMQTTForwarderTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
mqttHost: mainflux.Env(envMQTTHost, defMQTTHost),
|
||||
mqttPort: mainflux.Env(envMQTTPort, defMQTTPort),
|
||||
mqttTargetHost: mainflux.Env(envMQTTTargetHost, defMQTTTargetHost),
|
||||
mqttTargetPort: mainflux.Env(envMQTTTargetPort, defMQTTTargetPort),
|
||||
mqttForwarderTimeout: time.Duration(mqttTimeout) * time.Second,
|
||||
httpHost: mainflux.Env(envHTTPHost, defHTTPHost),
|
||||
mqttForwarderTimeout: mqttTimeout,
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
httpScheme: mainflux.Env(envHTTPScheme, defHTTPScheme),
|
||||
httpTargetHost: mainflux.Env(envHTTPTargetHost, defHTTPTargetHost),
|
||||
httpTargetPort: mainflux.Env(envHTTPTargetPort, defHTTPTargetPort),
|
||||
httpTargetPath: mainflux.Env(envHTTPTargetPath, defHTTPTargetPath),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(authTimeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
thingsURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
@@ -222,6 +223,9 @@ func loadConfig() config {
|
||||
esURL: mainflux.Env(envESURL, defESURL),
|
||||
esPass: mainflux.Env(envESPass, defESPass),
|
||||
esDB: mainflux.Env(envESDB, defESDB),
|
||||
authURL: mainflux.Env(envAuthCacheURL, defAuthcacheURL),
|
||||
authPass: mainflux.Env(envAuthCachePass, defAuthCachePass),
|
||||
authDB: mainflux.Env(envAuthCacheDB, defAuthCacheDB),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,17 +292,16 @@ func connectToRedis(redisURL, redisPass, redisDB string, logger mflog.Logger) *r
|
||||
}
|
||||
|
||||
func proxyMQTT(cfg config, logger mflog.Logger, handler session.Handler, errs chan error) {
|
||||
address := fmt.Sprintf("%s:%s", cfg.mqttHost, cfg.mqttPort)
|
||||
address := fmt.Sprintf(":%s", cfg.mqttPort)
|
||||
target := fmt.Sprintf("%s:%s", cfg.mqttTargetHost, cfg.mqttTargetPort)
|
||||
mp := mp.New(address, target, handler, logger)
|
||||
|
||||
errs <- mp.Proxy()
|
||||
errs <- mp.Listen()
|
||||
}
|
||||
func proxyWS(cfg config, logger mflog.Logger, handler session.Handler, errs chan error) {
|
||||
target := fmt.Sprintf("%s:%s", cfg.httpTargetHost, cfg.httpTargetPort)
|
||||
wp := ws.New(target, cfg.httpTargetPath, cfg.httpScheme, handler, logger)
|
||||
wp := ws.New(target, cfg.httpTargetPath, "ws", handler, logger)
|
||||
http.Handle("/mqtt", wp.Handler())
|
||||
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
errs <- http.ListenAndServe(p, nil)
|
||||
errs <- wp.Listen(cfg.httpPort)
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,12 +16,12 @@ import (
|
||||
r "github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/opcua"
|
||||
"github.com/mainflux/mainflux/opcua/api"
|
||||
"github.com/mainflux/mainflux/opcua/db"
|
||||
"github.com/mainflux/mainflux/opcua/gopcua"
|
||||
"github.com/mainflux/mainflux/opcua/redis"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
@@ -42,14 +42,14 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDB = "messages"
|
||||
defDB = "mainflux"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1" // in seconds
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_POSTGRES_READER_LOG_LEVEL"
|
||||
envPort = "MF_POSTGRES_READER_PORT"
|
||||
@@ -133,7 +133,7 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func loadConfig() config {
|
||||
dbConfig: dbConfig,
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: time.Duration(timeout) * time.Second,
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/postgres"
|
||||
@@ -34,7 +34,7 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDB = "messages"
|
||||
defDB = "mainflux"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
|
||||
+39
-16
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -11,18 +12,17 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/errors"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfSDK "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/provision"
|
||||
"github.com/mainflux/mainflux/provision/api"
|
||||
mfSDK "github.com/mainflux/mainflux/sdk/go"
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "debug"
|
||||
defConfigFile = "config.toml"
|
||||
defTLS = "false"
|
||||
defCACerts = ""
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defThingsLocation = "http://localhost"
|
||||
@@ -34,17 +34,18 @@ const (
|
||||
defMfAPIKey = ""
|
||||
defMfBSURL = "http://localhost:8202/things/configs"
|
||||
defMfWhiteListURL = "http://localhost:8202/things/state"
|
||||
defMfCertsURL = "http://localhost/certs"
|
||||
defMfCertsURL = "http://localhost:8204"
|
||||
defProvisionCerts = "false"
|
||||
defProvisionBS = "true"
|
||||
defBSAutoWhitelist = "true"
|
||||
defBSContent = ""
|
||||
defCertsHoursValid = "2400h"
|
||||
defCertsKeyBits = "4096"
|
||||
|
||||
envConfigFile = "MF_PROVISION_CONFIG_FILE"
|
||||
envLogLevel = "MF_PROVISION_LOG_LEVEL"
|
||||
envHTTPPort = "MF_PROVISION_HTTP_PORT"
|
||||
envTLS = "MF_PROVISION_ENV_CLIENTS_TLS"
|
||||
envCACerts = "MF_PROVISION_CA_CERTS"
|
||||
envServerCert = "MF_PROVISION_SERVER_CERT"
|
||||
envServerKey = "MF_PROVISION_SERVER_KEY"
|
||||
envMQTTURL = "MF_PROVISION_MQTT_URL"
|
||||
@@ -60,15 +61,19 @@ const (
|
||||
envProvisionBS = "MF_PROVISION_BS_CONFIG_PROVISIONING"
|
||||
envBSAutoWhiteList = "MF_PROVISION_BS_AUTO_WHITELIST"
|
||||
envBSContent = "MF_PROVISION_BS_CONTENT"
|
||||
envCertsHoursValid = "MF_PROVISION_CERTS_HOURS_VALID"
|
||||
envCertsKeyBits = "MF_PROVISION_CERTS_RSA_BITS"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingConfigFile = errors.New("missing config file setting")
|
||||
errFailedToLoadConfigFile = errors.New("failed to load config from file")
|
||||
errFailedToGetAutoWhiteList = errors.New("failed to get auto whitelist setting")
|
||||
errFailGettingCertSettings = errors.New("failed to get certificate file setting")
|
||||
errFailGettingTLSConf = errors.New("failed to get TLS setting")
|
||||
errFailGettingProvBS = errors.New("failed to get BS url setting")
|
||||
errMissingConfigFile = errors.New("missing config file setting")
|
||||
errFailLoadingConfigFile = errors.New("failed to load config from file")
|
||||
errFailGettingAutoWhiteList = errors.New("failed to get auto whitelist setting")
|
||||
errFailGettingCertSettings = errors.New("failed to get certificate file setting")
|
||||
errFailGettingTLSConf = errors.New("failed to get TLS setting")
|
||||
errFailGettingProvBS = errors.New("failed to get BS url setting")
|
||||
errFailSettingKeyBits = errors.New("failed to set rsa number of bits")
|
||||
errFailedToReadBootstrapContent = errors.New("failed to read bootstrap content from envs")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -86,7 +91,7 @@ func main() {
|
||||
// Merge environment variables and file settings.
|
||||
mergeConfigs(&cfgFromFile, &cfg)
|
||||
cfg = cfgFromFile
|
||||
logger.Info("Continue with settings from file:" + cfg.File)
|
||||
logger.Info("Continue with settings from file: " + cfg.File)
|
||||
}
|
||||
|
||||
SDKCfg := mfSDK.Config{
|
||||
@@ -135,7 +140,7 @@ func loadConfigFromFile(file string) (provision.Config, error) {
|
||||
}
|
||||
c, err := provision.Read(file)
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailedToLoadConfigFile, err)
|
||||
return provision.Config{}, errors.Wrap(errFailLoadingConfigFile, err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@@ -156,16 +161,26 @@ func loadConfig() (provision.Config, error) {
|
||||
|
||||
autoWhiteList, err := strconv.ParseBool(mainflux.Env(envBSAutoWhiteList, defBSAutoWhitelist))
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailedToGetAutoWhiteList, fmt.Errorf(" for %s", envBSAutoWhiteList))
|
||||
return provision.Config{}, errors.Wrap(errFailGettingAutoWhiteList, fmt.Errorf(" for %s", envBSAutoWhiteList))
|
||||
}
|
||||
if autoWhiteList && !provisionBS {
|
||||
return provision.Config{}, errors.New("Can't auto whitelist if auto config save is off")
|
||||
}
|
||||
keyBits, err := strconv.Atoi(mainflux.Env(envCertsKeyBits, defCertsKeyBits))
|
||||
if err != nil && provisionX509 == true {
|
||||
return provision.Config{}, errFailSettingKeyBits
|
||||
}
|
||||
|
||||
var content map[string]interface{}
|
||||
if c := mainflux.Env(envBSContent, defBSContent); c != "" {
|
||||
if err = json.Unmarshal([]byte(c), content); err != nil {
|
||||
return provision.Config{}, errFailedToReadBootstrapContent
|
||||
}
|
||||
}
|
||||
|
||||
cfg := provision.Config{
|
||||
Server: provision.ServiceConf{
|
||||
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
CACerts: mainflux.Env(envCACerts, defCACerts),
|
||||
ServerCert: mainflux.Env(envServerCert, defServerCert),
|
||||
ServerKey: mainflux.Env(envServerKey, defServerKey),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
@@ -179,11 +194,15 @@ func loadConfig() (provision.Config, error) {
|
||||
UsersLocation: mainflux.Env(envUsersLocation, defUsersLocation),
|
||||
TLS: tls,
|
||||
},
|
||||
Certs: provision.Certs{
|
||||
HoursValid: mainflux.Env(envCertsHoursValid, defCertsHoursValid),
|
||||
KeyBits: keyBits,
|
||||
},
|
||||
Bootstrap: provision.Bootstrap{
|
||||
X509Provision: provisionX509,
|
||||
Provision: provisionBS,
|
||||
AutoWhiteList: autoWhiteList,
|
||||
Content: mainflux.Env(envBSContent, defBSContent),
|
||||
Content: content,
|
||||
},
|
||||
|
||||
// This is default conf for provision if there is no config file
|
||||
@@ -227,6 +246,10 @@ func mergeConfigs(dst, src interface{}) interface{} {
|
||||
if dField.Interface() == false {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
case reflect.Int:
|
||||
if dField.Interface() == 0 {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
case reflect.String:
|
||||
if dField.Interface() == "" {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
|
||||
+6
-6
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
"github.com/mainflux/mainflux/things/api"
|
||||
authgrpcapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
@@ -35,7 +36,6 @@ import (
|
||||
"github.com/mainflux/mainflux/things/postgres"
|
||||
rediscache "github.com/mainflux/mainflux/things/redis"
|
||||
localusers "github.com/mainflux/mainflux/things/users"
|
||||
"github.com/mainflux/mainflux/things/uuid"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
@@ -69,7 +69,7 @@ const (
|
||||
defSingleUserToken = ""
|
||||
defJaegerURL = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1" // in seconds
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_THINGS_LOG_LEVEL"
|
||||
envDBHost = "MF_THINGS_DB_HOST"
|
||||
@@ -179,7 +179,7 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthnTimeout, defAuthnTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
@@ -216,7 +216,7 @@ func loadConfig() config {
|
||||
singleUserToken: mainflux.Env(envSingleUserToken, defSingleUserToken),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: time.Duration(timeout) * time.Second,
|
||||
authnTimeout: authnTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,9 +315,9 @@ func newService(auth mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, c
|
||||
|
||||
thingCache := rediscache.NewThingCache(cacheClient)
|
||||
thingCache = tracing.ThingCacheMiddleware(cacheTracer, thingCache)
|
||||
idp := uuid.New()
|
||||
up := uuidProvider.New()
|
||||
|
||||
svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idp)
|
||||
svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, up)
|
||||
svc = rediscache.NewEventStoreMiddleware(svc, esClient)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
|
||||
+46
-14
@@ -16,18 +16,20 @@ import (
|
||||
"time"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
"github.com/mainflux/mainflux/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
localusers "github.com/mainflux/mainflux/things/users"
|
||||
"github.com/mainflux/mainflux/twins"
|
||||
"github.com/mainflux/mainflux/twins/api"
|
||||
twapi "github.com/mainflux/mainflux/twins/api/http"
|
||||
twmongodb "github.com/mainflux/mainflux/twins/mongodb"
|
||||
rediscache "github.com/mainflux/mainflux/twins/redis"
|
||||
"github.com/mainflux/mainflux/twins/tracing"
|
||||
"github.com/mainflux/mainflux/twins/uuid"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
@@ -48,6 +50,9 @@ const (
|
||||
defDB = "mainflux-twins"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defCacheURL = "localhost:6379"
|
||||
defCachePass = ""
|
||||
defCacheDB = "0"
|
||||
defSingleUserEmail = ""
|
||||
defSingleUserToken = ""
|
||||
defClientTLS = "false"
|
||||
@@ -55,7 +60,7 @@ const (
|
||||
defChannelID = ""
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1" // in seconds
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_TWINS_LOG_LEVEL"
|
||||
envHTTPPort = "MF_TWINS_HTTP_PORT"
|
||||
@@ -65,6 +70,9 @@ const (
|
||||
envDB = "MF_TWINS_DB"
|
||||
envDBHost = "MF_TWINS_DB_HOST"
|
||||
envDBPort = "MF_TWINS_DB_PORT"
|
||||
envCacheURL = "MF_TWINS_CACHE_URL"
|
||||
envCachePass = "MF_TWINS_CACHE_PASS"
|
||||
envCacheDB = "MF_TWINS_CACHE_DB"
|
||||
envSingleUserEmail = "MF_TWINS_SINGLE_USER_EMAIL"
|
||||
envSingleUserToken = "MF_TWINS_SINGLE_USER_TOKEN"
|
||||
envClientTLS = "MF_TWINS_CLIENT_TLS"
|
||||
@@ -82,6 +90,9 @@ type config struct {
|
||||
serverCert string
|
||||
serverKey string
|
||||
dbCfg twmongodb.Config
|
||||
cacheURL string
|
||||
cachePass string
|
||||
cacheDB string
|
||||
singleUserEmail string
|
||||
singleUserToken string
|
||||
clientTLS bool
|
||||
@@ -101,20 +112,22 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
cacheClient := connectToRedis(cfg.cacheURL, cfg.cachePass, cfg.cacheDB, logger)
|
||||
cacheTracer, cacheCloser := initJaeger("twins_cache", cfg.jaegerURL, logger)
|
||||
defer cacheCloser.Close()
|
||||
|
||||
db, err := twmongodb.Connect(cfg.dbCfg, logger)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
dbTracer, dbCloser := initJaeger("twins_db", cfg.jaegerURL, logger)
|
||||
defer dbCloser.Close()
|
||||
|
||||
authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger)
|
||||
defer authCloser.Close()
|
||||
|
||||
auth, _ := createAuthClient(cfg, authTracer, logger)
|
||||
|
||||
dbTracer, dbCloser := initJaeger("twins_db", cfg.jaegerURL, logger)
|
||||
defer dbCloser.Close()
|
||||
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, queue, logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
@@ -122,7 +135,7 @@ func main() {
|
||||
}
|
||||
defer pubSub.Close()
|
||||
|
||||
svc := newService(pubSub, cfg.channelID, auth, dbTracer, db, logger)
|
||||
svc := newService(pubSub, cfg.channelID, auth, dbTracer, db, cacheTracer, cacheClient, logger)
|
||||
|
||||
tracer, closer := initJaeger("twins", cfg.jaegerURL, logger)
|
||||
defer closer.Close()
|
||||
@@ -145,7 +158,7 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthnTimeout, defAuthnTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
@@ -163,6 +176,9 @@ func loadConfig() config {
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
dbCfg: dbCfg,
|
||||
cacheURL: mainflux.Env(envCacheURL, defCacheURL),
|
||||
cachePass: mainflux.Env(envCachePass, defCachePass),
|
||||
cacheDB: mainflux.Env(envCacheDB, defCacheDB),
|
||||
singleUserEmail: mainflux.Env(envSingleUserEmail, defSingleUserEmail),
|
||||
singleUserToken: mainflux.Env(envSingleUserToken, defSingleUserToken),
|
||||
clientTLS: tls,
|
||||
@@ -170,7 +186,7 @@ func loadConfig() config {
|
||||
channelID: mainflux.Env(envChannelID, defChannelID),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: time.Duration(timeout) * time.Second,
|
||||
authnTimeout: authnTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,16 +248,32 @@ func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(ps messaging.PubSub, chanID string, users mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, db *mongo.Database, logger logger.Logger) twins.Service {
|
||||
func connectToRedis(cacheURL, cachePass, cacheDB string, logger logger.Logger) *redis.Client {
|
||||
db, err := strconv.Atoi(cacheDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to cache: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: cacheURL,
|
||||
Password: cachePass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func newService(ps messaging.PubSub, chanID string, users mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, db *mongo.Database, cacheTracer opentracing.Tracer, cacheClient *redis.Client, logger logger.Logger) twins.Service {
|
||||
twinRepo := twmongodb.NewTwinRepository(db)
|
||||
twinRepo = tracing.TwinRepositoryMiddleware(dbTracer, twinRepo)
|
||||
|
||||
stateRepo := twmongodb.NewStateRepository(db)
|
||||
stateRepo = tracing.StateRepositoryMiddleware(dbTracer, stateRepo)
|
||||
|
||||
idp := uuid.New()
|
||||
up := uuidProvider.New()
|
||||
twinCache := rediscache.NewTwinCache(cacheClient)
|
||||
twinCache = tracing.TwinCacheMiddleware(cacheTracer, twinCache)
|
||||
|
||||
svc := twins.New(ps, users, twinRepo, stateRepo, idp, chanID, logger)
|
||||
svc := twins.New(ps, users, twinRepo, twinCache, stateRepo, up, chanID, logger)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
|
||||
+64
-34
@@ -4,6 +4,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/internal/email"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/emailer"
|
||||
"github.com/mainflux/mainflux/users/tracing"
|
||||
"google.golang.org/grpc"
|
||||
@@ -28,7 +30,6 @@ import (
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/users/api"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/postgres"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
@@ -51,7 +52,6 @@ const (
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
|
||||
defEmailLogLevel = "debug"
|
||||
defEmailDriver = "smtp"
|
||||
defEmailHost = "localhost"
|
||||
defEmailPort = "25"
|
||||
@@ -60,13 +60,16 @@ const (
|
||||
defEmailFromAddress = ""
|
||||
defEmailFromName = ""
|
||||
defEmailTemplate = "email.tmpl"
|
||||
defAdminEmail = ""
|
||||
defAdminPassword = ""
|
||||
defAdminGroup = "mainflux"
|
||||
|
||||
defTokenResetEndpoint = "/reset-request" // URL where user lands after click on the reset link from email
|
||||
|
||||
defAuthnTLS = "false"
|
||||
defAuthnCACerts = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1" // in seconds
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_USERS_LOG_LEVEL"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
@@ -83,6 +86,9 @@ const (
|
||||
envServerKey = "MF_USERS_SERVER_KEY"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
|
||||
envAdminEmail = "MF_USERS_ADMIN_EMAIL"
|
||||
envAdminPassword = "MF_USERS_ADMIN_PASSWORD"
|
||||
|
||||
envEmailDriver = "MF_EMAIL_DRIVER"
|
||||
envEmailHost = "MF_EMAIL_HOST"
|
||||
envEmailPort = "MF_EMAIL_PORT"
|
||||
@@ -102,18 +108,20 @@ const (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
emailConf email.Config
|
||||
httpPort string
|
||||
serverCert string
|
||||
serverKey string
|
||||
jaegerURL string
|
||||
resetURL string
|
||||
authnTLS bool
|
||||
authnCACerts string
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
emailConf email.Config
|
||||
httpPort string
|
||||
serverCert string
|
||||
serverKey string
|
||||
jaegerURL string
|
||||
resetURL string
|
||||
authnTLS bool
|
||||
authnCACerts string
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
adminEmail string
|
||||
adminPassword string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -123,7 +131,6 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
@@ -157,7 +164,7 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthnTimeout, defAuthnTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
@@ -191,18 +198,20 @@ func loadConfig() config {
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
emailConf: emailConf,
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
resetURL: mainflux.Env(envTokenResetEndpoint, defTokenResetEndpoint),
|
||||
authnTLS: tls,
|
||||
authnCACerts: mainflux.Env(envAuthnCACerts, defAuthnCACerts),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: time.Duration(timeout) * time.Second,
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
emailConf: emailConf,
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
resetURL: mainflux.Env(envTokenResetEndpoint, defTokenResetEndpoint),
|
||||
authnTLS: tls,
|
||||
authnCACerts: mainflux.Env(envAuthnCACerts, defAuthnCACerts),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
adminEmail: mainflux.Env(envAdminEmail, defAdminEmail),
|
||||
adminPassword: mainflux.Env(envAdminPassword, defAdminPassword),
|
||||
}
|
||||
|
||||
}
|
||||
@@ -230,14 +239,12 @@ func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer,
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sqlx.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -268,14 +275,16 @@ func connectToAuthn(cfg config, tracer opentracing.Tracer, logger logger.Logger)
|
||||
|
||||
func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServiceClient, c config, logger logger.Logger) users.Service {
|
||||
database := postgres.NewDatabase(db)
|
||||
repo := tracing.UserRepositoryMiddleware(postgres.New(database), tracer)
|
||||
hasher := bcrypt.New()
|
||||
userRepo := tracing.UserRepositoryMiddleware(postgres.NewUserRepo(database), tracer)
|
||||
groupRepo := tracing.GroupRepositoryMiddleware(postgres.NewGroupRepo(database), tracer)
|
||||
|
||||
emailer, err := emailer.New(c.resetURL, &c.emailConf)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to configure e-mailing util: %s", err.Error()))
|
||||
}
|
||||
|
||||
svc := users.New(repo, hasher, auth, emailer)
|
||||
svc := users.New(userRepo, groupRepo, hasher, auth, emailer)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -292,10 +301,31 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServi
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
if err := createAdmin(svc, userRepo, groupRepo, c); err != nil {
|
||||
logger.Error("failed to create admin user: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
func createAdmin(svc users.Service, userRepo users.UserRepository, groupRepo users.GroupRepository, c config) error {
|
||||
user := users.User{
|
||||
Email: c.adminEmail,
|
||||
Password: c.adminPassword,
|
||||
}
|
||||
|
||||
if _, err := userRepo.RetrieveByEmail(context.Background(), user.Email); err == nil {
|
||||
// Exiting if user already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := svc.Register(context.Background(), user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startHTTPServer(tracer opentracing.Tracer, svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
if certFile != "" || keyFile != "" {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ default values.
|
||||
| MF_COAP_ADAPTER_PING_PERIOD | Hours between 1 and 24 to ping client with ACK message | 12 |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_THINGS_AUTH_GRPC_URL | Things service Auth gRPC URL | localhost:8181 |
|
||||
| MF_THINGS_AUTH_GRPC_TIMEOUT | Things service Auth gRPC request timeout in seconds | 1 |
|
||||
| MF_THINGS_AUTH_GRPC_TIMEOUT | Things service Auth gRPC request timeout in seconds | 1s |
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
+120
-91
@@ -2,142 +2,171 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package coap contains the domain concept definitions needed to support
|
||||
// Mainflux coap adapter service functionality. All constant values are taken
|
||||
// Mainflux CoAP adapter service functionality. All constant values are taken
|
||||
// from RFC, and could be adjusted based on specific use case.
|
||||
package coap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
broker "github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
)
|
||||
|
||||
const (
|
||||
chanID = "id"
|
||||
keyHeader = "key"
|
||||
const chansPrefix = "channels"
|
||||
|
||||
// AckRandomFactor is default ACK coefficient.
|
||||
AckRandomFactor = 1.5
|
||||
// AckTimeout is the amount of time to wait for a response.
|
||||
AckTimeout = 2000 * time.Millisecond
|
||||
// MaxRetransmit is the maximum number of times a message will be retransmitted.
|
||||
MaxRetransmit = 4
|
||||
// Exported errors
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized access")
|
||||
ErrUnsubscribe = errors.New("unable to unsubscribe")
|
||||
)
|
||||
|
||||
// Service specifies coap service API.
|
||||
// Service specifies CoAP service API.
|
||||
type Service interface {
|
||||
// Publish Messssage
|
||||
Publish(msg messaging.Message) error
|
||||
Publish(ctx context.Context, key string, msg messaging.Message) error
|
||||
|
||||
// Subscribes to channel with specified id, subtopic and adds subscription to
|
||||
// service map of subscriptions under given ID.
|
||||
Subscribe(chanID, subtopic, obsID string, obs *Observer) error
|
||||
Subscribe(ctx context.Context, key, chanID, subtopic string, c Client) error
|
||||
|
||||
// Unsubscribe method is used to stop observing resource.
|
||||
Unsubscribe(obsID string)
|
||||
Unsubscribe(ctx context.Context, key, chanID, subptopic, token string) error
|
||||
}
|
||||
|
||||
var _ Service = (*adapterService)(nil)
|
||||
|
||||
// Observers is a map of maps,
|
||||
type adapterService struct {
|
||||
auth mainflux.ThingsServiceClient
|
||||
ps messaging.PubSub
|
||||
log logger.Logger
|
||||
obs map[string]*Observer
|
||||
obsLock sync.Mutex
|
||||
auth mainflux.ThingsServiceClient
|
||||
conn *broker.Conn
|
||||
observers map[string]observers
|
||||
obsLock sync.Mutex
|
||||
}
|
||||
|
||||
// New instantiates the CoAP adapter implementation.
|
||||
func New(ps messaging.PubSub, log logger.Logger, auth mainflux.ThingsServiceClient, responses <-chan string) Service {
|
||||
func New(auth mainflux.ThingsServiceClient, nc *broker.Conn) Service {
|
||||
as := &adapterService{
|
||||
auth: auth,
|
||||
ps: ps,
|
||||
log: log,
|
||||
obs: make(map[string]*Observer),
|
||||
obsLock: sync.Mutex{},
|
||||
auth: auth,
|
||||
conn: nc,
|
||||
observers: make(map[string]observers),
|
||||
obsLock: sync.Mutex{},
|
||||
}
|
||||
|
||||
go as.listenResponses(responses)
|
||||
return as
|
||||
}
|
||||
|
||||
func (svc *adapterService) get(obsID string) (*Observer, bool) {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
val, ok := svc.obs[obsID]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (svc *adapterService) put(obsID string, o *Observer) {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
val, ok := svc.obs[obsID]
|
||||
if ok {
|
||||
close(val.Cancel)
|
||||
func (svc *adapterService) Publish(ctx context.Context, key string, msg messaging.Message) error {
|
||||
ar := &mainflux.AccessByKeyReq{
|
||||
Token: key,
|
||||
ChanID: msg.Channel,
|
||||
}
|
||||
|
||||
svc.obs[obsID] = o
|
||||
}
|
||||
|
||||
func (svc *adapterService) remove(obsID string) {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
val, ok := svc.obs[obsID]
|
||||
if ok {
|
||||
close(val.Cancel)
|
||||
delete(svc.obs, obsID)
|
||||
thid, err := svc.auth.CanAccessByKey(ctx, ar)
|
||||
if err != nil {
|
||||
return errors.Wrap(ErrUnauthorized, err)
|
||||
}
|
||||
}
|
||||
msg.Publisher = thid.GetValue()
|
||||
|
||||
// ListenResponses method handles ACK messages received from client.
|
||||
func (svc *adapterService) listenResponses(responses <-chan string) {
|
||||
for {
|
||||
id := <-responses
|
||||
|
||||
val, ok := svc.get(id)
|
||||
if ok {
|
||||
val.StoreExpired(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *adapterService) Publish(msg messaging.Message) error {
|
||||
return svc.ps.Publish(msg.Channel, msg)
|
||||
}
|
||||
|
||||
func (svc *adapterService) Subscribe(chanID, subtopic, obsID string, o *Observer) error {
|
||||
subject := chanID
|
||||
if subtopic != "" {
|
||||
subject = fmt.Sprintf("%s.%s", chanID, subtopic)
|
||||
}
|
||||
|
||||
err := svc.ps.Subscribe(subject, func(msg messaging.Message) error {
|
||||
o.Messages <- msg
|
||||
return nil
|
||||
})
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s.%s", chansPrefix, msg.Channel)
|
||||
if msg.Subtopic != "" {
|
||||
subject = fmt.Sprintf("%s.%s", subject, msg.Subtopic)
|
||||
}
|
||||
|
||||
return svc.conn.Publish(subject, data)
|
||||
}
|
||||
|
||||
func (svc *adapterService) Subscribe(ctx context.Context, key, chanID, subtopic string, c Client) error {
|
||||
ar := &mainflux.AccessByKeyReq{
|
||||
Token: key,
|
||||
ChanID: chanID,
|
||||
}
|
||||
if _, err := svc.auth.CanAccessByKey(ctx, ar); err != nil {
|
||||
return errors.Wrap(ErrUnauthorized, err)
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s.%s", chansPrefix, chanID)
|
||||
if subtopic != "" {
|
||||
subject = fmt.Sprintf("%s.%s", subject, subtopic)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-o.Cancel
|
||||
if err := svc.ps.Unsubscribe(subject); err != nil {
|
||||
svc.log.Error(fmt.Sprintf("Failed to unsubscribe from %s.%s due to %s", chanID, subtopic, err))
|
||||
}
|
||||
<-c.Done()
|
||||
svc.remove(subject, c.Token())
|
||||
}()
|
||||
|
||||
// Put method removes Observer if already exists.
|
||||
svc.put(obsID, o)
|
||||
obs, err := NewObserver(subject, c, svc.conn)
|
||||
if err != nil {
|
||||
c.Cancel()
|
||||
return err
|
||||
}
|
||||
return svc.put(subject, c.Token(), obs)
|
||||
}
|
||||
|
||||
func (svc *adapterService) Unsubscribe(ctx context.Context, key, chanID, subtopic, token string) error {
|
||||
ar := &mainflux.AccessByKeyReq{
|
||||
Token: key,
|
||||
ChanID: chanID,
|
||||
}
|
||||
if _, err := svc.auth.CanAccessByKey(ctx, ar); err != nil {
|
||||
return errors.Wrap(ErrUnauthorized, err)
|
||||
}
|
||||
subject := fmt.Sprintf("%s.%s", chansPrefix, chanID)
|
||||
if subtopic != "" {
|
||||
subject = fmt.Sprintf("%s.%s", subject, subtopic)
|
||||
}
|
||||
|
||||
return svc.remove(subject, token)
|
||||
}
|
||||
|
||||
func (svc *adapterService) put(endpoint, token string, o Observer) error {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
obs, ok := svc.observers[endpoint]
|
||||
// If there are no observers, create map and assign it to the endpoint.
|
||||
if !ok {
|
||||
obs = observers{token: o}
|
||||
svc.observers[endpoint] = obs
|
||||
return nil
|
||||
}
|
||||
// If observer exists, cancel subscription and replace it.
|
||||
if sub, ok := obs[token]; ok {
|
||||
if err := sub.Cancel(); err != nil {
|
||||
return errors.Wrap(ErrUnsubscribe, err)
|
||||
}
|
||||
}
|
||||
obs[token] = o
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Unsubscribe(obsID string) {
|
||||
svc.remove(obsID)
|
||||
func (svc *adapterService) remove(endpoint, token string) error {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
obs, ok := svc.observers[endpoint]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if current, ok := obs[token]; ok {
|
||||
if err := current.Cancel(); err != nil {
|
||||
return errors.Wrap(ErrUnsubscribe, err)
|
||||
}
|
||||
}
|
||||
delete(obs, token)
|
||||
// If there are no observers left for the endpint, remove the map.
|
||||
if len(obs) == 0 {
|
||||
delete(svc.observers, endpoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+11
-10
@@ -6,12 +6,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*loggingMiddleware)(nil)
|
||||
@@ -26,13 +27,13 @@ func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg messaging.Message) (err error) {
|
||||
func (lm *loggingMiddleware) Publish(ctx context.Context, key string, msg messaging.Message) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
destChannel := msg.Channel
|
||||
if msg.Subtopic != "" {
|
||||
destChannel = fmt.Sprintf("%s.%s", destChannel, msg.Subtopic)
|
||||
}
|
||||
message := fmt.Sprintf("Method publish to channel %s took %s to complete", destChannel, time.Since(begin))
|
||||
message := fmt.Sprintf("Method publish to %s took %s to complete", destChannel, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -40,16 +41,16 @@ func (lm *loggingMiddleware) Publish(msg messaging.Message) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Publish(msg)
|
||||
return lm.svc.Publish(ctx, key, msg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Subscribe(chanID, subtopic, obsID string, o *coap.Observer) (err error) {
|
||||
func (lm *loggingMiddleware) Subscribe(ctx context.Context, key, chanID, subtopic string, c coap.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
destChannel := chanID
|
||||
if subtopic != "" {
|
||||
destChannel = fmt.Sprintf("%s.%s", destChannel, subtopic)
|
||||
}
|
||||
message := fmt.Sprintf("Method subscribe to channel %s for client %s took %s to complete", destChannel, obsID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method subscribe to %s for client %s took %s to complete", destChannel, c.Token(), time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -57,14 +58,14 @@ func (lm *loggingMiddleware) Subscribe(chanID, subtopic, obsID string, o *coap.O
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Subscribe(chanID, subtopic, obsID, o)
|
||||
return lm.svc.Subscribe(ctx, key, chanID, subtopic, c)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unsubscribe(obsID string) {
|
||||
func (lm *loggingMiddleware) Unsubscribe(ctx context.Context, key, chanID, subtopic, token string) error {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unsubscribe for the client %s took %s to complete without errors.", obsID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method unsubscribe for the client %s from the channel %s and subtopic %s took %s to complete without errors.", token, chanID, subtopic, time.Since(begin))
|
||||
lm.logger.Info(fmt.Sprintf(message))
|
||||
}(time.Now())
|
||||
|
||||
lm.svc.Unsubscribe(obsID)
|
||||
return lm.svc.Unsubscribe(ctx, key, chanID, subtopic, token)
|
||||
}
|
||||
|
||||
+8
-7
@@ -6,11 +6,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*metricsMiddleware)(nil)
|
||||
@@ -30,29 +31,29 @@ func MetricsMiddleware(svc coap.Service, counter metrics.Counter, latency metric
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Publish(msg messaging.Message) error {
|
||||
func (mm *metricsMiddleware) Publish(ctx context.Context, key string, msg messaging.Message) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "publish").Add(1)
|
||||
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Publish(msg)
|
||||
return mm.svc.Publish(ctx, key, msg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Subscribe(chanID, subtopic, clientID string, o *coap.Observer) error {
|
||||
func (mm *metricsMiddleware) Subscribe(ctx context.Context, key, chanID, subtopic string, c coap.Client) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "subscribe").Add(1)
|
||||
mm.latency.With("method", "subscribe").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Subscribe(chanID, subtopic, clientID, o)
|
||||
return mm.svc.Subscribe(ctx, key, chanID, subtopic, c)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
|
||||
func (mm *metricsMiddleware) Unsubscribe(ctx context.Context, key, chanID, subtopic, token string) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "unsubscribe").Add(1)
|
||||
mm.latency.With("method", "unsubscribe").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
mm.svc.Unsubscribe(clientID)
|
||||
return mm.svc.Unsubscribe(ctx, key, chanID, subtopic, token)
|
||||
}
|
||||
|
||||
+130
-302
@@ -4,50 +4,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
"github.com/plgd-dev/go-coap/v2/message"
|
||||
"github.com/plgd-dev/go-coap/v2/message/codes"
|
||||
"github.com/plgd-dev/go-coap/v2/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
protocol = "coap"
|
||||
senMLJSON gocoap.MediaType = 110
|
||||
senMLCBOR gocoap.MediaType = 112
|
||||
protocol = "coap"
|
||||
authQuery = "auth"
|
||||
)
|
||||
|
||||
var channelPartRegExp = regexp.MustCompile(`^ch/([\w\-]+)/msg(/[^?]*)?(\?.*)?$`)
|
||||
|
||||
var errMalformedSubtopic = errors.New("malformed subtopic")
|
||||
|
||||
var (
|
||||
errBadRequest = errors.New("bad request")
|
||||
errBadOption = errors.New("bad option")
|
||||
errMalformedSubtopic = errors.New("malformed subtopic")
|
||||
channelRegExp = regexp.MustCompile(`^/?channels/([\w\-]+)/messages(/[^?]*)?(\?.*)?$`)
|
||||
logger log.Logger
|
||||
service coap.Service
|
||||
)
|
||||
|
||||
var (
|
||||
auth mainflux.ThingsServiceClient
|
||||
logger log.Logger
|
||||
pingPeriod time.Duration
|
||||
)
|
||||
|
||||
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
|
||||
|
||||
//MakeHTTPHandler creates handler for version endpoint.
|
||||
func MakeHTTPHandler() http.Handler {
|
||||
b := bone.New()
|
||||
@@ -57,112 +49,139 @@ func MakeHTTPHandler() http.Handler {
|
||||
return b
|
||||
}
|
||||
|
||||
// MakeCOAPHandler creates handler for CoAP messages.
|
||||
func MakeCOAPHandler(svc coap.Service, tc mainflux.ThingsServiceClient, l log.Logger, responses chan<- string, pp time.Duration) gocoap.Handler {
|
||||
auth = tc
|
||||
// MakeCoAPHandler creates handler for CoAP messages.
|
||||
func MakeCoAPHandler(svc coap.Service, l log.Logger) mux.HandlerFunc {
|
||||
logger = l
|
||||
pingPeriod = pp
|
||||
return mux(svc, responses)
|
||||
service = svc
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func mux(svc coap.Service, responses chan<- string) gocoap.Handler {
|
||||
return gocoap.FuncHandler(func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
path := msg.PathString()
|
||||
if !channelRegExp.Match([]byte(path)) {
|
||||
logger.Info(fmt.Sprintf("path %s not found", path))
|
||||
return &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
Code: gocoap.NotFound,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
}
|
||||
}
|
||||
// Allow "/" to be a part of the path.
|
||||
if strings.HasPrefix(path, "/") {
|
||||
msg.SetPathString(path[1:])
|
||||
}
|
||||
switch msg.Code {
|
||||
case gocoap.GET:
|
||||
return observe(svc, responses)(conn, addr, msg)
|
||||
default:
|
||||
return receive(svc, msg)
|
||||
}
|
||||
})
|
||||
func sendResp(w mux.ResponseWriter, resp *message.Message) {
|
||||
if err := w.Client().WriteMessage(resp); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Can't set response: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func id(msg *gocoap.Message) string {
|
||||
vars := strings.Split(msg.PathString(), "/")
|
||||
func handler(w mux.ResponseWriter, m *mux.Message) {
|
||||
resp := message.Message{
|
||||
Code: codes.Content,
|
||||
Token: m.Token,
|
||||
Context: m.Context,
|
||||
Options: make(message.Options, 0, 16),
|
||||
}
|
||||
defer sendResp(w, &resp)
|
||||
if m.Options == nil {
|
||||
logger.Warn("Nil options")
|
||||
resp.Code = codes.BadOption
|
||||
return
|
||||
}
|
||||
msg, err := decodeMessage(m)
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("Error decoding message: %s", err))
|
||||
resp.Code = codes.BadRequest
|
||||
return
|
||||
}
|
||||
key, err := parseKey(m)
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("Error parsing auth: %s", err))
|
||||
resp.Code = codes.Unauthorized
|
||||
return
|
||||
}
|
||||
switch m.Code {
|
||||
case codes.GET:
|
||||
var obs uint32
|
||||
obs, err = m.Options.Observe()
|
||||
if err != nil {
|
||||
resp.Code = codes.BadOption
|
||||
logger.Warn(fmt.Sprintf("Error reading observe option: %s", err))
|
||||
return
|
||||
}
|
||||
if obs == 0 {
|
||||
c := coap.NewClient(w.Client(), m.Token, logger)
|
||||
err = service.Subscribe(context.Background(), key, msg.Channel, msg.Subtopic, c)
|
||||
break
|
||||
}
|
||||
service.Unsubscribe(context.Background(), key, msg.Channel, msg.Subtopic, m.Token.String())
|
||||
case codes.POST:
|
||||
err = service.Publish(context.Background(), key, msg)
|
||||
default:
|
||||
resp.Code = codes.NotFound
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Contains(err, coap.ErrUnauthorized):
|
||||
resp.Code = codes.Unauthorized
|
||||
return
|
||||
case errors.Contains(err, coap.ErrUnsubscribe):
|
||||
resp.Code = codes.InternalServerError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeMessage(msg *mux.Message) (messaging.Message, error) {
|
||||
path, err := msg.Options.Path()
|
||||
if err != nil {
|
||||
return messaging.Message{}, err
|
||||
}
|
||||
channelParts := channelPartRegExp.FindStringSubmatch(path)
|
||||
if len(channelParts) < 2 {
|
||||
return messaging.Message{}, errMalformedSubtopic
|
||||
}
|
||||
|
||||
st, err := parseSubtopic(channelParts[2])
|
||||
if err != nil {
|
||||
return messaging.Message{}, err
|
||||
}
|
||||
ret := messaging.Message{
|
||||
Protocol: protocol,
|
||||
Channel: parseID(path),
|
||||
Subtopic: st,
|
||||
Payload: []byte{},
|
||||
Created: time.Now().UnixNano(),
|
||||
}
|
||||
|
||||
if msg.Body != nil {
|
||||
buff, err := ioutil.ReadAll(msg.Body)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
ret.Payload = buff
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func parseID(path string) string {
|
||||
vars := strings.Split(path, "/")
|
||||
if len(vars) > 1 {
|
||||
return vars[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func subtopic(msg *gocoap.Message) string {
|
||||
path := msg.PathString()
|
||||
pos := 0
|
||||
for i, c := range path {
|
||||
if c == '/' {
|
||||
pos++
|
||||
}
|
||||
if pos == 3 {
|
||||
return path[i:]
|
||||
}
|
||||
func parseKey(msg *mux.Message) (string, error) {
|
||||
auth, err := msg.Options.GetString(message.URIQuery)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ""
|
||||
vars := strings.Split(auth, "=")
|
||||
if len(vars) != 2 || vars[0] != authQuery {
|
||||
return "", coap.ErrUnauthorized
|
||||
}
|
||||
return vars[1], nil
|
||||
}
|
||||
|
||||
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (string, error) {
|
||||
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
|
||||
query := msg.Option(gocoap.URIQuery)
|
||||
queryStr, ok := query.(string)
|
||||
if !ok {
|
||||
res.Code = gocoap.BadRequest
|
||||
return "", errBadRequest
|
||||
}
|
||||
|
||||
params, err := url.ParseQuery(queryStr)
|
||||
if err != nil {
|
||||
res.Code = gocoap.BadRequest
|
||||
return "", errBadRequest
|
||||
}
|
||||
|
||||
auths, ok := params["authorization"]
|
||||
if !ok || len(auths) != 1 {
|
||||
res.Code = gocoap.BadRequest
|
||||
return "", errBadRequest
|
||||
}
|
||||
|
||||
key := auths[0]
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := auth.CanAccessByKey(ctx, &mainflux.AccessByKeyReq{Token: key, ChanID: cid})
|
||||
if err != nil {
|
||||
e, ok := status.FromError(err)
|
||||
if ok {
|
||||
switch e.Code() {
|
||||
case codes.PermissionDenied:
|
||||
res.Code = gocoap.Forbidden
|
||||
default:
|
||||
res.Code = gocoap.ServiceUnavailable
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
|
||||
return id.GetValue(), nil
|
||||
}
|
||||
|
||||
func fmtSubtopic(msg *gocoap.Message) (string, error) {
|
||||
subtopic := subtopic(msg)
|
||||
func parseSubtopic(subtopic string) (string, error) {
|
||||
if subtopic == "" {
|
||||
return subtopic, nil
|
||||
}
|
||||
|
||||
subtopic = strings.Replace(subtopic, "/", ".", -1)
|
||||
subtopic, err := url.QueryUnescape(subtopic)
|
||||
if err != nil {
|
||||
return "", errMalformedSubtopic
|
||||
}
|
||||
subtopic = strings.ReplaceAll(subtopic, "/", ".")
|
||||
|
||||
elems := strings.Split(subtopic, ".")
|
||||
filteredElems := []string{}
|
||||
@@ -179,196 +198,5 @@ func fmtSubtopic(msg *gocoap.Message) (string, error) {
|
||||
}
|
||||
|
||||
subtopic = strings.Join(filteredElems, ".")
|
||||
|
||||
return subtopic, nil
|
||||
}
|
||||
|
||||
func receive(svc coap.Service, msg *gocoap.Message) *gocoap.Message {
|
||||
// By default message is NonConfirmable, so
|
||||
// NonConfirmable response is sent back.
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
// According to https://tools.ietf.org/html/rfc7252#page-47: If the POST
|
||||
// succeeds but does not result in a new resource being created on the
|
||||
// server, the response SHOULD have a 2.04 (Changed) Response Code.
|
||||
Code: gocoap.Changed,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
|
||||
if msg.IsConfirmable() {
|
||||
res.Type = gocoap.Acknowledgement
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
if len(msg.Payload) == 0 {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
chanID := id(msg)
|
||||
if chanID == "" {
|
||||
res.Code = gocoap.NotFound
|
||||
return res
|
||||
}
|
||||
|
||||
subtopic, err := fmtSubtopic(msg)
|
||||
if err != nil {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
|
||||
publisher, err := authorize(msg, res, chanID)
|
||||
if err != nil {
|
||||
res.Code = gocoap.Forbidden
|
||||
return res
|
||||
}
|
||||
|
||||
m := messaging.Message{
|
||||
Channel: chanID,
|
||||
Subtopic: subtopic,
|
||||
Publisher: publisher,
|
||||
Protocol: protocol,
|
||||
Payload: msg.Payload,
|
||||
Created: time.Now().UnixNano(),
|
||||
}
|
||||
|
||||
if err := svc.Publish(m); err != nil {
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func observe(svc coap.Service, responses chan<- string) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
|
||||
chanID := id(msg)
|
||||
if chanID == "" {
|
||||
res.Code = gocoap.NotFound
|
||||
return res
|
||||
}
|
||||
|
||||
subtopic, err := fmtSubtopic(msg)
|
||||
if err != nil {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
|
||||
publisher, err := authorize(msg, res, chanID)
|
||||
if err != nil {
|
||||
res.Code = gocoap.Forbidden
|
||||
logger.Warn(fmt.Sprintf("Failed to authorize: %s", err))
|
||||
return res
|
||||
}
|
||||
|
||||
obsID := fmt.Sprintf("%x-%s-%s", msg.Token, publisher, chanID)
|
||||
|
||||
if msg.Type == gocoap.Acknowledgement {
|
||||
responses <- obsID
|
||||
return nil
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); (ok && value == 1) || msg.Type == gocoap.Reset {
|
||||
svc.Unsubscribe(obsID)
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
|
||||
res.AddOption(gocoap.Observe, 1)
|
||||
o := coap.NewObserver()
|
||||
if err := svc.Subscribe(chanID, subtopic, obsID, o); err != nil {
|
||||
logger.Warn(err.Error())
|
||||
res.Code = gocoap.InternalServerError
|
||||
return res
|
||||
}
|
||||
|
||||
go handleMessage(conn, addr, o, msg)
|
||||
go ping(svc, obsID, conn, addr, o, msg)
|
||||
go cancel(o)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func cancel(observer *coap.Observer) {
|
||||
<-observer.Cancel
|
||||
close(observer.Messages)
|
||||
observer.StoreExpired(true)
|
||||
}
|
||||
|
||||
func handleMessage(conn *net.UDPConn, addr *net.UDPAddr, o *coap.Observer, msg *gocoap.Message) {
|
||||
notifyMsg := *msg
|
||||
notifyMsg.Type = gocoap.NonConfirmable
|
||||
notifyMsg.Code = gocoap.Content
|
||||
notifyMsg.RemoveOption(gocoap.URIQuery)
|
||||
for {
|
||||
msg, ok := <-o.Messages
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
notifyMsg.Payload = msg.Payload
|
||||
notifyMsg.MessageID = o.LoadMessageID()
|
||||
buff := new(bytes.Buffer)
|
||||
observe := uint64(notifyMsg.MessageID)
|
||||
if err := binary.Write(buff, binary.BigEndian, observe); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to generate Observe option value: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
observeVal := buff.Bytes()
|
||||
notifyMsg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
|
||||
|
||||
if err := gocoap.Transmit(conn, addr, notifyMsg); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to send message to observer: %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ping(svc coap.Service, obsID string, conn *net.UDPConn, addr *net.UDPAddr, o *coap.Observer, msg *gocoap.Message) {
|
||||
pingMsg := *msg
|
||||
pingMsg.Payload = []byte{}
|
||||
pingMsg.Type = gocoap.Confirmable
|
||||
pingMsg.RemoveOption(gocoap.URIQuery)
|
||||
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
|
||||
// 24 hours. Deafault value of pingPeriod is 12.
|
||||
t := time.NewTicker(pingPeriod * time.Hour)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-t.C:
|
||||
if !ok || o.LoadExpired() {
|
||||
return
|
||||
}
|
||||
|
||||
o.StoreExpired(true)
|
||||
timeout := float64(coap.AckTimeout)
|
||||
logger.Info(fmt.Sprintf("Ping client %s.", obsID))
|
||||
for i := 0; i < coap.MaxRetransmit; i++ {
|
||||
pingMsg.MessageID = o.LoadMessageID()
|
||||
gocoap.Transmit(conn, addr, pingMsg)
|
||||
time.Sleep(time.Duration(timeout * coap.AckRandomFactor))
|
||||
if !o.LoadExpired() {
|
||||
break
|
||||
}
|
||||
timeout = 2 * timeout
|
||||
}
|
||||
|
||||
if o.LoadExpired() {
|
||||
svc.Unsubscribe(obsID)
|
||||
return
|
||||
}
|
||||
case <-o.Cancel:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import "strings"
|
||||
|
||||
func authKey(opt interface{}) (string, error) {
|
||||
val, ok := opt.(string)
|
||||
if !ok {
|
||||
return "", errBadRequest
|
||||
}
|
||||
|
||||
arr := strings.Split(val, "=")
|
||||
if len(arr) != 2 || strings.ToLower(arr[0]) != "authorization" {
|
||||
return "", errBadOption
|
||||
}
|
||||
|
||||
return arr[1], nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package coap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
"github.com/plgd-dev/go-coap/v2/message"
|
||||
"github.com/plgd-dev/go-coap/v2/message/codes"
|
||||
mux "github.com/plgd-dev/go-coap/v2/mux"
|
||||
)
|
||||
|
||||
// Client wraps CoAP client.
|
||||
type Client interface {
|
||||
// In CoAP terminology, Token similar to the Session ID.
|
||||
Token() string
|
||||
SendMessage(m messaging.Message) error
|
||||
Cancel() error
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
type observers map[string]Observer
|
||||
|
||||
// ErrOption indicates an error when adding an option.
|
||||
var ErrOption = errors.New("unable to set option")
|
||||
|
||||
type client struct {
|
||||
client mux.Client
|
||||
token message.Token
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewClient instantiates a new Observer.
|
||||
func NewClient(mc mux.Client, token message.Token, l logger.Logger) Client {
|
||||
return &client{
|
||||
client: mc,
|
||||
token: token,
|
||||
logger: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Done() <-chan struct{} {
|
||||
return c.client.Context().Done()
|
||||
}
|
||||
|
||||
func (c *client) Cancel() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *client) Token() string {
|
||||
return c.token.String()
|
||||
}
|
||||
|
||||
func (c *client) SendMessage(msg messaging.Message) error {
|
||||
m := message.Message{
|
||||
Code: codes.Content,
|
||||
Token: c.token,
|
||||
Context: c.client.Context(),
|
||||
Body: bytes.NewReader(msg.Payload),
|
||||
}
|
||||
var opts message.Options
|
||||
var buff []byte
|
||||
|
||||
opts, n, err := opts.SetContentFormat(buff, message.TextPlain)
|
||||
if err == message.ErrTooSmall {
|
||||
buff = append(buff, make([]byte, n)...)
|
||||
opts, n, err = opts.SetContentFormat(buff, message.TextPlain)
|
||||
}
|
||||
if err != nil {
|
||||
c.logger.Error(fmt.Sprintf("Can't set content format: %s.", err))
|
||||
return errors.Wrap(ErrOption, err)
|
||||
}
|
||||
m.Options = opts
|
||||
if err := c.client.WriteMessage(&m); err != nil {
|
||||
c.logger.Error(fmt.Sprintf("Error sending message: %s.", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+31
-52
@@ -4,65 +4,44 @@
|
||||
package coap
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/messaging"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
broker "github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// Observer is used to handle CoAP subscription.
|
||||
type Observer struct {
|
||||
// Expired flag is used to mark that ticker sent a
|
||||
// CON message, but response is not received yet.
|
||||
// The flag changes its value once ACK message is
|
||||
// received from the client. If Expired is true
|
||||
// when ticker is triggered, Observer should be canceled
|
||||
// and removed from the Service map.
|
||||
expired bool
|
||||
|
||||
// Message ID for notification messages.
|
||||
msgID uint16
|
||||
|
||||
expiredLock, msgIDLock sync.Mutex
|
||||
|
||||
// Messages is used to receive messages from NATS.
|
||||
Messages chan messaging.Message
|
||||
|
||||
// Cancel channel is used to cancel observing resource.
|
||||
// Cancel channel should not be used to send or receive any
|
||||
// data, it's purpose is to be closed once Observer canceled.
|
||||
Cancel chan bool
|
||||
// Observer represents an internal observer used to handle CoAP observe messages.
|
||||
type Observer interface {
|
||||
Cancel() error
|
||||
}
|
||||
|
||||
// NewObserver instantiates a new Observer.
|
||||
func NewObserver() *Observer {
|
||||
return &Observer{
|
||||
Messages: make(chan messaging.Message),
|
||||
Cancel: make(chan bool),
|
||||
// NewObserver returns a new Observer instance.
|
||||
func NewObserver(subject string, c Client, conn *broker.Conn) (Observer, error) {
|
||||
sub, err := conn.Subscribe(subject, func(m *broker.Msg) {
|
||||
var msg messaging.Message
|
||||
if err := proto.Unmarshal(m.Data, &msg); err != nil {
|
||||
return
|
||||
}
|
||||
// There is no error handling, but the client takes care to log the error.
|
||||
c.SendMessage(msg)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &observer{
|
||||
client: c,
|
||||
sub: sub,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// LoadExpired reads Expired flag in thread-safe way.
|
||||
func (o *Observer) LoadExpired() bool {
|
||||
o.expiredLock.Lock()
|
||||
defer o.expiredLock.Unlock()
|
||||
|
||||
return o.expired
|
||||
type observer struct {
|
||||
client Client
|
||||
sub *broker.Subscription
|
||||
}
|
||||
|
||||
// StoreExpired stores Expired flag in thread-safe way.
|
||||
func (o *Observer) StoreExpired(val bool) {
|
||||
o.expiredLock.Lock()
|
||||
defer o.expiredLock.Unlock()
|
||||
|
||||
o.expired = val
|
||||
}
|
||||
|
||||
// LoadMessageID reads MessageID and increments
|
||||
// its value in thread-safe way.
|
||||
func (o *Observer) LoadMessageID() uint16 {
|
||||
o.msgIDLock.Lock()
|
||||
defer o.msgIDLock.Unlock()
|
||||
|
||||
o.msgID++
|
||||
return o.msgID
|
||||
func (o *observer) Cancel() error {
|
||||
if err := o.sub.Unsubscribe(); err != nil && err != broker.ErrConnectionClosed {
|
||||
return err
|
||||
}
|
||||
return o.client.Cancel()
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.13-alpine AS builder
|
||||
FROM golang:1.14.4-alpine AS builder
|
||||
ARG SVC
|
||||
ARG GOARCH
|
||||
ARG GOARM
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# This docker-compose file contains optional certs services. Since it's optional, this file is
|
||||
# dependent of docker-compose file from <project_root>/docker. In order to run this services, execute command:
|
||||
# docker-compose -f docker/docker-compose.yml -f docker/addons/certs/docker-compose.yml up
|
||||
# from project root.
|
||||
|
||||
version: "3.7"
|
||||
|
||||
networks:
|
||||
docker_mainflux-base-net:
|
||||
external: true
|
||||
|
||||
|
||||
volumes:
|
||||
mainflux-certs-db-volume:
|
||||
|
||||
services:
|
||||
certs-db:
|
||||
image: postgres:10.2-alpine
|
||||
container_name: mainflux-certs-db
|
||||
restart: on-failure
|
||||
environment:
|
||||
POSTGRES_USER: ${MF_CERTS_DB_USER}
|
||||
POSTGRES_PASSWORD: ${MF_CERTS_DB_PASS}
|
||||
POSTGRES_DB: ${MF_CERTS_DB}
|
||||
networks:
|
||||
- docker_mainflux-base-net
|
||||
volumes:
|
||||
- mainflux-certs-db-volume:/var/lib/postgresql/data
|
||||
|
||||
certs:
|
||||
image: mainflux/certs:latest
|
||||
container_name: mainflux-certs
|
||||
depends_on:
|
||||
- certs-db
|
||||
restart: on-failure
|
||||
networks:
|
||||
- docker_mainflux-base-net
|
||||
ports:
|
||||
- ${MF_CERTS_HTTP_PORT}:${MF_CERTS_HTTP_PORT}
|
||||
environment:
|
||||
MF_CERTS_LOG_LEVEL: ${MF_CERTS_LOG_LEVEL}
|
||||
MF_CERTS_DB_HOST: certs-db
|
||||
MF_CERTS_DB_PORT: ${MF_CERTS_DB_PORT}
|
||||
MF_CERTS_DB_PASS: ${MF_CERTS_DB_PASS}
|
||||
MF_CERTS_DB_USER: ${MF_CERTS_DB_USER}
|
||||
MF_CERTS_DB: ${MF_CERTS_DB}
|
||||
MF_CERTS_DB_SSL_MODE: ${MF_CERTS_DB_SSL_MODE}
|
||||
MF_CERTS_DB_SSL_CERT: ${MF_CERTS_DB_SSL_CERT}
|
||||
MF_CERTS_DB_SSL_KEY: ${MF_CERTS_DB_SSL_KEY}
|
||||
MF_CERTS_DB_SSL_ROOT_CERT: ${MF_CERTS_DB_SSL_ROOT_CERT}
|
||||
MF_CERTS_ENCRYPT_KEY: ${MF_CERTS_ENCRYPT_KEY}
|
||||
MF_CERTS_CLIENT_TLS: ${MF_CERTS_CLIENT_TLS}
|
||||
MF_CERTS_CA_CERTS: ${MF_CERTS_CA_CERTS}
|
||||
MF_CERTS_HTTP_PORT: ${MF_CERTS_HTTP_PORT}
|
||||
MF_CERTS_SERVER_CERT: ${MF_CERTS_SERVER_CERT}
|
||||
MF_CERTS_SERVER_KEY: ${MF_CERTS_SERVER_KEY}
|
||||
MF_SDK_BASE_URL: ${MF_SDK_BASE_URL}
|
||||
MF_SDK_THINGS_PREFIX: ${MF_SDK_THINGS_PREFIX}
|
||||
MF_JAEGER_URL: ${MF_JAEGER_URL}
|
||||
MF_AUTHN_GRPC_URL: ${MF_AUTHN_GRPC_URL}
|
||||
MF_AUTHN_GRPC_TIMEOUT: ${MF_AUTHN_GRPC_TIMEOUT}
|
||||
MF_CERTS_SIGN_CA_PATH: ${MF_CERTS_SIGN_CA_PATH}
|
||||
MF_CERTS_SIGN_CA_KEY_PATH: ${MF_CERTS_SIGN_CA_KEY_PATH}
|
||||
MF_CERTS_SIGN_HOURS_VALID: ${MF_CERTS_SIGN_HOURS_VALID}
|
||||
MF_CERTS_SIGN_RSA_BITS: ${MF_CERTS_SIGN_RSA_BITS}
|
||||
MF_CERTS_VAULT_TOKEN: ${MF_CERTS_VAULT_TOKEN}
|
||||
MF_CERTS_VAULT_HOST: ${MF_CERTS_VAULT_HOST}
|
||||
MF_CERTS_VAULT_PKI_PATH: ${MF_CERTS_VAULT_PKI_PATH}
|
||||
MF_CERTS_VAULT_ROLE: ${MF_CERTS_VAULT_ROLE}
|
||||
volumes:
|
||||
- ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key
|
||||
- ../../ssl/certs/ca.crt:/etc/ssl/certs/ca.crt
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
[bootstrap]
|
||||
content = "{\"log_level\":\"info\",\"http_port\":\"9003\",\"mqtt_url\":\"localhost:1883\",\"edgex_url\":\"http://localhost:48090/api/v1/\",\"nats_url\":\"localhost:4222\"}"
|
||||
[bootstrap.content]
|
||||
[bootstrap.content.agent.edgex]
|
||||
url = "http://localhost:48090/api/v1/"
|
||||
|
||||
[bootstrap.content.agent.log]
|
||||
level = "info"
|
||||
|
||||
[bootstrap.content.agent.mqtt]
|
||||
mtls = false
|
||||
qos = 0
|
||||
retain = false
|
||||
skip_tls_ver = true
|
||||
url = "localhost:1883"
|
||||
|
||||
[bootstrap.content.agent.server]
|
||||
nats_url = "localhost:4222"
|
||||
port = "9000"
|
||||
|
||||
[bootstrap.content.agent.heartbeat]
|
||||
interval = "30s"
|
||||
|
||||
[bootstrap.content.agent.terminal]
|
||||
session_timeout = "30s"
|
||||
|
||||
|
||||
[bootstrap.content.export.exp]
|
||||
log_level = "debug"
|
||||
nats = "nats://localhost:4222"
|
||||
port = "8172"
|
||||
cache_url = "localhost:6379"
|
||||
cache_pass = ""
|
||||
cache_db = "0"
|
||||
|
||||
[bootstrap.content.export.mqtt]
|
||||
ca_path = "ca.crt"
|
||||
cert_path = "thing.crt"
|
||||
channel = ""
|
||||
host = "tcp://localhost:1883"
|
||||
mtls = false
|
||||
password = ""
|
||||
priv_key_path = "thing.key"
|
||||
qos = 0
|
||||
retain = false
|
||||
skip_tls_ver = false
|
||||
username = ""
|
||||
|
||||
[[bootstrap.content.export.routes]]
|
||||
mqtt_topic = ""
|
||||
nats_topic = ">"
|
||||
subtopic = ""
|
||||
type = "plain"
|
||||
workers = 10
|
||||
|
||||
[[things]]
|
||||
name = "thing"
|
||||
|
||||
@@ -27,7 +27,6 @@ services:
|
||||
MF_PROVISION_CONFIG_FILE: ${MF_PROVISION_CONFIG_FILE}
|
||||
MF_PROVISION_LOG_LEVEL: ${MF_PROVISION_LOG_LEVEL}
|
||||
MF_PROVISION_ENV_CLIENTS_TLS: ${MF_PROVISION_ENV_CLIENTS_TLS}
|
||||
MF_PROVISION_CA_CERTS: ${MF_PROVISION_CA_CERTS}
|
||||
MF_PROVISION_SERVER_CERT: ${MF_PROVISION_SERVER_CERT}
|
||||
MF_PROVISION_SERVER_KEY: ${MF_PROVISION_SERVER_KEY}
|
||||
MF_PROVISION_MQTT_URL: ${MF_PROVISION_MQTT_URL}
|
||||
@@ -43,5 +42,9 @@ services:
|
||||
MF_PROVISION_BS_CONFIG_PROVISIONING: ${MF_PROVISION_BS_CONFIG_PROVISIONING}
|
||||
MF_PROVISION_BS_AUTO_WHITELIST: ${MF_PROVISION_BS_AUTO_WHITELIST}
|
||||
MF_PROVISION_BS_CONTENT: ${MF_PROVISION_BS_CONTENT}
|
||||
MF_PROVISION_CERTS_RSA_BITS: ${MF_PROVISION_CERTS_RSA_BITS}
|
||||
MF_PROVISION_CERTS_HOURS_VALID: ${MF_PROVISION_CERTS_HOURS_VALID}
|
||||
volumes:
|
||||
- ./configs:/configs
|
||||
- ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key
|
||||
- ../../ssl/certs/ca.crt:/etc/ssl/certs/ca.crt
|
||||
|
||||
@@ -44,9 +44,12 @@ services:
|
||||
MF_TWINS_DB_PORT: ${MF_TWINS_DB_PORT}
|
||||
MF_TWINS_CHANNEL_ID: ${MF_TWINS_CHANNEL_ID}
|
||||
MF_NATS_URL: ${MF_NATS_URL}
|
||||
MF_TWINS_MQTT_URL: ${MF_TWINS_MQTT_URL}
|
||||
MF_AUTHN_GRPC_URL: ${MF_AUTHN_GRPC_URL}
|
||||
MF_AUTHN_GRPC_TIMEOUT: ${MF_AUTHN_GRPC_TIMEOUT}
|
||||
MF_TWINS_CACHE_URL: ${MF_TWINS_CACHE_URL}
|
||||
MF_TWINS_CACHE_PASS: ${MF_TWINS_CACHE_PASS}
|
||||
MF_TWINS_CACHE_DB: ${MF_TWINS_CACHE_DB}
|
||||
|
||||
ports:
|
||||
- ${MF_TWINS_HTTP_PORT}:${MF_TWINS_HTTP_PORT}
|
||||
networks:
|
||||
|
||||
@@ -11,7 +11,7 @@ volumes:
|
||||
mainflux-authn-db-volume:
|
||||
mainflux-users-db-volume:
|
||||
mainflux-things-db-volume:
|
||||
mainflux-things-redis-volume:
|
||||
mainflux-auth-redis-volume:
|
||||
mainflux-es-redis-volume:
|
||||
mainflux-mqtt-broker-volume:
|
||||
|
||||
@@ -135,6 +135,8 @@ services:
|
||||
MF_TOKEN_RESET_ENDPOINT: ${MF_TOKEN_RESET_ENDPOINT}
|
||||
MF_AUTHN_GRPC_URL: ${MF_AUTHN_GRPC_URL}
|
||||
MF_AUTHN_GRPC_TIMEOUT: ${MF_AUTHN_GRPC_TIMEOUT}
|
||||
MF_USERS_ADMIN_EMAIL: ${MF_USERS_ADMIN_EMAIL}
|
||||
MF_USERS_ADMIN_PASSWORD: ${MF_USERS_ADMIN_PASSWORD}
|
||||
ports:
|
||||
- ${MF_USERS_HTTP_PORT}:${MF_USERS_HTTP_PORT}
|
||||
networks:
|
||||
@@ -153,14 +155,14 @@ services:
|
||||
volumes:
|
||||
- mainflux-things-db-volume:/var/lib/postgresql/data
|
||||
|
||||
things-redis:
|
||||
auth-redis:
|
||||
image: redis:5.0-alpine
|
||||
container_name: mainflux-things-redis
|
||||
container_name: mainflux-auth-redis
|
||||
restart: on-failure
|
||||
networks:
|
||||
- mainflux-base-net
|
||||
volumes:
|
||||
- mainflux-things-redis-volume:/data
|
||||
- mainflux-auth-redis-volume:/data
|
||||
|
||||
things:
|
||||
image: mainflux/things:latest
|
||||
@@ -176,12 +178,11 @@ services:
|
||||
MF_THINGS_DB_USER: ${MF_THINGS_DB_USER}
|
||||
MF_THINGS_DB_PASS: ${MF_THINGS_DB_PASS}
|
||||
MF_THINGS_DB: ${MF_THINGS_DB}
|
||||
MF_THINGS_CACHE_URL: things-redis:${MF_REDIS_TCP_PORT}
|
||||
MF_THINGS_CACHE_URL: auth-redis:${MF_REDIS_TCP_PORT}
|
||||
MF_THINGS_ES_URL: es-redis:${MF_REDIS_TCP_PORT}
|
||||
MF_THINGS_HTTP_PORT: ${MF_THINGS_HTTP_PORT}
|
||||
MF_THINGS_AUTH_HTTP_PORT: ${MF_THINGS_AUTH_HTTP_PORT}
|
||||
MF_THINGS_AUTH_GRPC_PORT: ${MF_THINGS_AUTH_GRPC_PORT}
|
||||
MF_THINGS_SECRET: ${MF_THINGS_SECRET}
|
||||
MF_JAEGER_URL: ${MF_JAEGER_URL}
|
||||
MF_AUTHN_GRPC_URL: ${MF_AUTHN_GRPC_URL}
|
||||
MF_AUTHN_GRPC_TIMEOUT: ${MF_AUTHN_GRPC_TIMEOUT}
|
||||
@@ -236,6 +237,7 @@ services:
|
||||
MF_JAEGER_URL: ${MF_JAEGER_URL}
|
||||
MF_THINGS_AUTH_GRPC_URL: ${MF_THINGS_AUTH_GRPC_URL}
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT: ${MF_THINGS_AUTH_GRPC_TIMEOUT}
|
||||
MF_AUTH_CACHE_URL: auth-redis:${MF_REDIS_TCP_PORT}
|
||||
networks:
|
||||
- mainflux-base-net
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ envsubst '
|
||||
${MF_USERS_HTTP_PORT}
|
||||
${MF_THINGS_HTTP_PORT}
|
||||
${MF_HTTP_ADAPTER_PORT}
|
||||
${MF_NGINX_MQTT_PORT}
|
||||
${MF_NGINX_MQTTS_PORT}
|
||||
${MF_WS_ADAPTER_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
exec nginx -g "daemon off;"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user