mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:50:18 +00:00
MF-1346 - Create Groups API - add grouping of entities (#1334)
* remove owner id Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add users endpoint for retrieving users from group Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups from things and users Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move groups into auth Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * separate endpoints for users and things Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix problems with retrieving members Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add groups test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups from users Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups from things Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename constant Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add new errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove unnecessary constants Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix validation Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * create groups db mock Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * adding tests Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * revert changes to docker related files Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups endpoints from users openapi Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups endpoints from users openapi Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move constant from postgres to groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move constant from postgres to groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move constant from postgres to groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove testing group Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * renam typ to groupType Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add error for max level Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove print Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups.Member interface Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix query building and add test cases Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * uncomment tests Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move groups package Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove group type, add bulk assign and unassign Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * update openapi, remove parentID from create request, reorder endpoints Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * update openapi Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * update openapi for users and things Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix groups test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix linter errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * resolve comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename assignReq structure Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * refactor mocks, response, remove type from endpoint Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * some refactor, renaming, errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * simplify check Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove package alias Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix naming and comment Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * additional comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add members grpc endpoint test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix retrieving members for different types Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix retrieving members for different types Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove unecessary structure Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix api grpc Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename const Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * refactore retrieve parents and children with common function Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * small changes for errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix compile error Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix sorting in mock Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove regexp for groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * revert as change is made by mistake Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * revert as change is made by mistake Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * refactor groups and keys package Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix naming Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix naming Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix test for timestamp compare Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix error handling Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove errors not being used Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * var renaming Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * resolve comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * minor changes Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add endpoints for groups into nginx Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * reorganize endpoints, remove some errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * reorganize endpoints, remove some errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * small fix Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix linter errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * minor changes Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * resolve comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix group save path problem Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * description constant Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename variables Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix validation Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * get back return Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix compile Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
This commit is contained in:
+18
-1
@@ -1,6 +1,9 @@
|
||||
# Auth - Authentication and Authorization service
|
||||
|
||||
Auth service provides authentication features as an API for managing authentication keys. User service is using Auth service gRPC API to obtain login token or password reset token. Authentication key consists of the following fields:
|
||||
Auth service provides authentication features as an API for managing authentication keys as well as administering groups of entities - `things` and `users`.
|
||||
|
||||
# Authentication
|
||||
User service is using Auth service gRPC API to obtain login token or password reset token. Authentication key consists of the following fields:
|
||||
- ID - key ID
|
||||
- Type - one of the three types described below
|
||||
- IssuerID - an ID of the Mainflux User who issued the key
|
||||
@@ -32,6 +35,20 @@ The following actions are supported:
|
||||
- obtain (API keys only)
|
||||
- revoke (API keys only)
|
||||
|
||||
# Groups
|
||||
User and Things service are using Auth gRPC API to get the list of ids that are part of a group. Groups can be organized as tree structure.
|
||||
Group consists of the following fields:
|
||||
|
||||
- ID - ULID id uniquely representing group
|
||||
- Name - name of the group, name of the group is unique at the same level of tree hierarchy for a given tree.
|
||||
- ParentID - id of the parent group
|
||||
- OwnerID - id of the user that created a group
|
||||
- Description - free form text, up to 1024 characters
|
||||
- Metadata - Arbitrary, object-encoded group's data
|
||||
- Path - tree path consisting of group ids
|
||||
- CreatedAt - timestamp at which the group is created
|
||||
- UpdatedAt - timestamp at which the group is updated
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
|
||||
+25
-13
@@ -66,13 +66,13 @@ func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Du
|
||||
decodeAssignResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
).Endpoint()),
|
||||
members: kitot.TraceClient(tracer, "member")(kitgrpc.NewClient(
|
||||
members: kitot.TraceClient(tracer, "members")(kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"Members",
|
||||
encodeMembersRequest,
|
||||
decodeMembersResponse,
|
||||
mainflux.AuthorizeRes{},
|
||||
mainflux.MembersRes{},
|
||||
).Endpoint()),
|
||||
|
||||
timeout: timeout,
|
||||
@@ -156,7 +156,13 @@ func (client grpcClient) Members(ctx context.Context, req *mainflux.MembersReq,
|
||||
ctx, close := context.WithTimeout(ctx, client.timeout)
|
||||
defer close()
|
||||
|
||||
res, err := client.members(ctx, membersReq{token: req.GetToken(), groupID: req.GetGroupID()})
|
||||
res, err := client.members(ctx, membersReq{
|
||||
token: req.GetToken(),
|
||||
groupID: req.GetGroupID(),
|
||||
memberType: req.GetType(),
|
||||
offset: req.GetOffset(),
|
||||
limit: req.GetLimit(),
|
||||
})
|
||||
if err != nil {
|
||||
return &mainflux.MembersRes{}, err
|
||||
}
|
||||
@@ -170,20 +176,26 @@ func (client grpcClient) Members(ctx context.Context, req *mainflux.MembersReq,
|
||||
Type: mr.groupType,
|
||||
Members: mr.members,
|
||||
}, err
|
||||
|
||||
}
|
||||
|
||||
func encodeMembersRequest(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.AuthorizeRes)
|
||||
return authorizeRes{authorized: res.Authorized}, nil
|
||||
func encodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(membersReq)
|
||||
return &mainflux.MembersReq{
|
||||
Token: req.token,
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
GroupID: req.groupID,
|
||||
Type: req.memberType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeMembersResponse(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &mainflux.AuthorizeReq{
|
||||
Sub: req.Sub,
|
||||
Obj: req.Obj,
|
||||
Act: req.Act,
|
||||
func decodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.MembersRes)
|
||||
return membersRes{
|
||||
offset: res.Offset,
|
||||
limit: res.Limit,
|
||||
total: res.Total,
|
||||
members: res.Members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
// TODO implement authorization
|
||||
authorized, err := svc.Authorize(ctx, req.token, req.Sub, req.Obj, req.Obj)
|
||||
if err != nil {
|
||||
return authorizeRes{}, err
|
||||
@@ -90,7 +89,7 @@ func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
|
||||
err = svc.Assign(ctx, req.token, req.memberID, req.groupID)
|
||||
err = svc.Assign(ctx, req.token, req.memberID, req.groupID, req.groupType)
|
||||
if err != nil {
|
||||
return emptyRes{}, err
|
||||
}
|
||||
@@ -102,30 +101,27 @@ func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
func membersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(membersReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
|
||||
_, err := svc.Identify(ctx, req.token)
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
}
|
||||
mp, err := svc.ListMembers(ctx, req.token, req.groupID, req.memberType, pm)
|
||||
if err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
|
||||
mp, err := svc.ListMembers(ctx, req.token, req.groupID, req.offset, req.limit, nil)
|
||||
if err != nil {
|
||||
return membersRes{}, err
|
||||
}
|
||||
memberIDs := []string{}
|
||||
var members []string
|
||||
for _, m := range mp.Members {
|
||||
memberIDs = append(memberIDs, m.(string))
|
||||
members = append(members, m.ID)
|
||||
}
|
||||
return membersRes{
|
||||
offset: req.offset,
|
||||
limit: req.limit,
|
||||
total: mp.PageMetadata.Total,
|
||||
members: memberIDs,
|
||||
members: members,
|
||||
}, nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
thingsType = "things"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
|
||||
numOfThings = 5
|
||||
numOfUsers = 5
|
||||
)
|
||||
|
||||
var svc auth.Service
|
||||
@@ -178,3 +184,79 @@ func TestIdentify(t *testing.T) {
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMembers(t *testing.T) {
|
||||
_, token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Name: "Mainflux",
|
||||
Description: description,
|
||||
}
|
||||
|
||||
var things []string
|
||||
for i := 0; i < numOfThings; i++ {
|
||||
id, err := uuid.New().ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
|
||||
|
||||
things = append(things, id)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for i := 0; i < numOfUsers; i++ {
|
||||
id, err := uuid.New().ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("Generate thing id expected to succeed: %s", err))
|
||||
|
||||
users = append(users, id)
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), token, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, thingsType, things...)
|
||||
assert.Nil(t, err, fmt.Sprintf("Assign members to expected to succeed: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), token, group.ID, usersType, users...)
|
||||
assert.Nil(t, err, fmt.Sprintf("Assign members to group expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
groupID string
|
||||
groupType string
|
||||
size int
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "get all things with user token",
|
||||
groupID: group.ID,
|
||||
token: token,
|
||||
groupType: thingsType,
|
||||
size: numOfThings,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "get all users with user token",
|
||||
groupID: group.ID,
|
||||
token: token,
|
||||
groupType: usersType,
|
||||
size: numOfUsers,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
}
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
for _, tc := range cases {
|
||||
m, err := client.Members(context.Background(), &mainflux.MembersReq{Token: tc.token, GroupID: tc.groupID, Type: tc.groupType, Offset: 0, Limit: 10})
|
||||
e, ok := status.FromError(err)
|
||||
assert.Equal(t, tc.size, len(m.Members), fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.size, len(m.Members)))
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
assert.True(t, ok, "OK expected to be true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,10 @@ func (req issueReq) validate() error {
|
||||
}
|
||||
|
||||
type assignReq struct {
|
||||
token string
|
||||
groupID string
|
||||
memberID string
|
||||
token string
|
||||
groupID string
|
||||
memberID string
|
||||
groupType string
|
||||
}
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
@@ -61,11 +62,11 @@ func (req assignReq) validate() error {
|
||||
}
|
||||
|
||||
type membersReq struct {
|
||||
token string
|
||||
groupID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
typ string
|
||||
token string
|
||||
groupID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
memberType string
|
||||
}
|
||||
|
||||
func (req membersReq) validate() error {
|
||||
@@ -75,7 +76,7 @@ func (req membersReq) validate() error {
|
||||
if req.groupID == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
if req.typ == "" {
|
||||
if req.memberType == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
|
||||
+16
-10
@@ -90,8 +90,8 @@ func (s *grpcServer) Assign(ctx context.Context, token *mainflux.Assignment) (*e
|
||||
return res.(*empty.Empty), nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Members(ctx context.Context, token *mainflux.MembersReq) (*mainflux.MembersRes, error) {
|
||||
_, res, err := s.members.ServeGRPC(ctx, token)
|
||||
func (s *grpcServer) Members(ctx context.Context, req *mainflux.MembersReq) (*mainflux.MembersRes, error) {
|
||||
_, res, err := s.members.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
@@ -135,17 +135,23 @@ func decodeAssignRequest(_ context.Context, grpcReq interface{}) (interface{}, e
|
||||
|
||||
func decodeMembersRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.MembersReq)
|
||||
return membersReq{token: req.GetToken(), groupID: req.GetGroupID()}, nil
|
||||
return membersReq{
|
||||
token: req.GetToken(),
|
||||
groupID: req.GetGroupID(),
|
||||
memberType: req.GetType(),
|
||||
offset: req.Offset,
|
||||
limit: req.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeMembersResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.MembersRes)
|
||||
return membersRes{
|
||||
total: res.GetTotal(),
|
||||
offset: res.GetOffset(),
|
||||
limit: res.GetLimit(),
|
||||
groupType: res.GetType(),
|
||||
members: res.GetMembers(),
|
||||
res := grpcRes.(membersRes)
|
||||
return &mainflux.MembersRes{
|
||||
Total: res.total,
|
||||
Offset: res.offset,
|
||||
Limit: res.limit,
|
||||
Type: res.groupType,
|
||||
Members: res.members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package http
|
||||
@@ -4,44 +4,42 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
func CreateGroupEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func createGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
group := groups.Group{
|
||||
group := auth.Group{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
ParentID: req.ParentID,
|
||||
Type: req.Type,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
id, err := svc.CreateGroup(ctx, req.token, group)
|
||||
group, err := svc.CreateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return groupRes{}, errors.Wrap(groups.ErrCreateGroup, err)
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
return groupRes{created: true, id: id}, nil
|
||||
return groupRes{created: true, id: group.ID}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ViewGroupEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func viewGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewGroupRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
|
||||
group, err := svc.ViewGroup(ctx, req.token, req.groupID)
|
||||
group, err := svc.ViewGroup(ctx, req.token, req.id)
|
||||
if err != nil {
|
||||
return viewGroupRes{}, errors.Wrap(groups.ErrFetchGroups, err)
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
|
||||
res := viewGroupRes{
|
||||
@@ -51,30 +49,31 @@ func ViewGroupEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
Metadata: group.Metadata,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateGroupEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func updateGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
group := groups.Group{
|
||||
group := auth.Group{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
ParentID: req.ParentID,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
_, err := svc.UpdateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return groupRes{}, errors.Wrap(groups.ErrUpdateGroup, err)
|
||||
return groupRes{}, err
|
||||
}
|
||||
|
||||
res := groupRes{created: false}
|
||||
@@ -82,31 +81,34 @@ func UpdateGroupEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteGroupEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func deleteGroupEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.RemoveGroup(ctx, req.token, req.groupID); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrDeleteGroup, err)
|
||||
if err := svc.RemoveGroup(ctx, req.token, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groupDeleteRes{}, nil
|
||||
return deleteRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ListGroupsEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func listGroupsEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
page, err := svc.ListGroups(ctx, req.token, req.level, req.metadata)
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListGroups(ctx, req.token, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, errors.Wrap(groups.ErrFetchGroups, err)
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
@@ -117,14 +119,20 @@ func ListGroupsEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func ListMembership(svc groups.Service) endpoint.Endpoint {
|
||||
func listMemberships(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMemberGroupReq)
|
||||
req := request.(listMembershipsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
page, err := svc.ListMemberships(ctx, req.token, req.memberID, req.offset, req.limit, req.metadata)
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
|
||||
page, err := svc.ListMemberships(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
@@ -137,16 +145,20 @@ func ListMembership(svc groups.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func ListGroupChildrenEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func listChildrenEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
page, err := svc.ListChildren(ctx, req.token, req.groupID, req.level, req.metadata)
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListChildren(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, errors.Wrap(groups.ErrFetchGroups, err)
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
@@ -157,16 +169,20 @@ func ListGroupChildrenEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func ListGroupParentsEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func listParentsEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listGroupsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
pm := auth.PageMetadata{
|
||||
Level: req.level,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
|
||||
page, err := svc.ListParents(ctx, req.token, req.groupID, req.level, req.metadata)
|
||||
page, err := svc.ListParents(ctx, req.token, req.id, pm)
|
||||
if err != nil {
|
||||
return groupPageRes{}, errors.Wrap(groups.ErrFetchGroups, err)
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
|
||||
if req.tree {
|
||||
@@ -177,44 +193,49 @@ func ListGroupParentsEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func AssignEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func assignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(memberGroupReq)
|
||||
req := request.(assignReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Assign(ctx, req.token, req.memberID, req.groupID); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrAssignToGroup, err)
|
||||
if err := svc.Assign(ctx, req.token, req.groupID, req.Type, req.Members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assignMemberToGroupRes{}, nil
|
||||
return assignRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func UnassignEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func unassignEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(memberGroupReq)
|
||||
req := request.(assignReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Unassign(ctx, req.token, req.memberID, req.groupID); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrUnassignFromGroup, err)
|
||||
if err := svc.Unassign(ctx, req.token, req.groupID, req.Members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeMemberFromGroupRes{}, nil
|
||||
return unassignRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ListMembersEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
func listMembersEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMemberGroupReq)
|
||||
req := request.(listMembersReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return memberPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
|
||||
page, err := svc.ListMembers(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
|
||||
pm := auth.PageMetadata{
|
||||
Offset: req.offset,
|
||||
Limit: req.limit,
|
||||
Metadata: req.metadata,
|
||||
}
|
||||
page, err := svc.ListMembers(ctx, req.token, req.id, req.groupType, pm)
|
||||
if err != nil {
|
||||
return memberPageRes{}, err
|
||||
}
|
||||
@@ -223,29 +244,30 @@ func ListMembersEndpoint(svc groups.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func buildGroupsResponseTree(page groups.GroupPage) groupPageRes {
|
||||
groupsMap := map[string]*groups.Group{}
|
||||
// Parents map keeps its array of children.
|
||||
parentsMap := map[string][]*groups.Group{}
|
||||
func buildGroupsResponseTree(page auth.GroupPage) groupPageRes {
|
||||
groupsMap := map[string]*auth.Group{}
|
||||
// Parents' map keeps its array of children.
|
||||
parentsMap := map[string][]*auth.Group{}
|
||||
for i := range page.Groups {
|
||||
if _, ok := groupsMap[page.Groups[i].ID]; !ok {
|
||||
groupsMap[page.Groups[i].ID] = &page.Groups[i]
|
||||
parentsMap[page.Groups[i].ID] = make([]*groups.Group, 0)
|
||||
parentsMap[page.Groups[i].ID] = make([]*auth.Group, 0)
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range groupsMap {
|
||||
if ch, ok := parentsMap[group.ParentID]; ok {
|
||||
ch = append(ch, group)
|
||||
parentsMap[group.ParentID] = ch
|
||||
if children, ok := parentsMap[group.ParentID]; ok {
|
||||
children = append(children, group)
|
||||
parentsMap[group.ParentID] = children
|
||||
}
|
||||
}
|
||||
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Offset: page.Offset,
|
||||
Total: page.Total,
|
||||
Level: page.Level,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
@@ -267,22 +289,22 @@ func buildGroupsResponseTree(page groups.GroupPage) groupPageRes {
|
||||
return res
|
||||
}
|
||||
|
||||
func toViewGroupRes(g groups.Group) viewGroupRes {
|
||||
func toViewGroupRes(group auth.Group) viewGroupRes {
|
||||
view := viewGroupRes{
|
||||
ID: g.ID,
|
||||
ParentID: g.ParentID,
|
||||
OwnerID: g.OwnerID,
|
||||
Name: g.Name,
|
||||
Description: g.Description,
|
||||
Metadata: g.Metadata,
|
||||
Level: g.Level,
|
||||
Path: g.Path,
|
||||
ID: group.ID,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
Level: group.Level,
|
||||
Path: group.Path,
|
||||
Children: make([]*viewGroupRes, 0),
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
CreatedAt: group.CreatedAt,
|
||||
UpdatedAt: group.UpdatedAt,
|
||||
}
|
||||
|
||||
for _, ch := range g.Children {
|
||||
for _, ch := range group.Children {
|
||||
child := toViewGroupRes(*ch)
|
||||
view.Children = append(view.Children, &child)
|
||||
}
|
||||
@@ -290,12 +312,11 @@ func toViewGroupRes(g groups.Group) viewGroupRes {
|
||||
return view
|
||||
}
|
||||
|
||||
func buildGroupsResponse(gp groups.GroupPage) groupPageRes {
|
||||
func buildGroupsResponse(gp auth.GroupPage) groupPageRes {
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: gp.Total,
|
||||
Offset: gp.Offset,
|
||||
Limit: gp.Limit,
|
||||
Total: gp.Total,
|
||||
Level: gp.Level,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
@@ -319,7 +340,7 @@ func buildGroupsResponse(gp groups.GroupPage) groupPageRes {
|
||||
return res
|
||||
}
|
||||
|
||||
func buildUsersResponse(mp groups.MemberPage) memberPageRes {
|
||||
func buildUsersResponse(mp auth.MemberPage) memberPageRes {
|
||||
res := memberPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: mp.Total,
|
||||
@@ -0,0 +1,146 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
type createGroupReq struct {
|
||||
token string
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req createGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
if len(req.Name) > maxNameSize || req.Name == "" {
|
||||
return errors.Wrap(auth.ErrMalformedEntity, auth.ErrBadGroupName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateGroupReq struct {
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listGroupsReq struct {
|
||||
token string
|
||||
id string
|
||||
level uint64
|
||||
// - `true` - result is JSON tree representing groups hierarchy,
|
||||
// - `false` - result is JSON array of groups.
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listGroupsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.level > auth.MaxLevel || req.level < auth.MinLevel {
|
||||
return auth.ErrMaxLevelExceeded
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembersReq struct {
|
||||
token string
|
||||
id string
|
||||
groupType string
|
||||
offset uint64
|
||||
limit uint64
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembersReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembershipsReq struct {
|
||||
token string
|
||||
id string
|
||||
offset uint64
|
||||
limit uint64
|
||||
tree bool
|
||||
metadata auth.GroupMetadata
|
||||
}
|
||||
|
||||
func (req listMembershipsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type assignReq struct {
|
||||
token string
|
||||
groupID string
|
||||
Type string `json:"type,omitempty"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (req assignReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.Type == "" || req.groupID == "" || len(req.Members) == 0 {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type groupReq struct {
|
||||
token string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req groupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -11,18 +11,11 @@ import (
|
||||
var (
|
||||
_ mainflux.Response = (*memberPageRes)(nil)
|
||||
_ mainflux.Response = (*groupRes)(nil)
|
||||
_ mainflux.Response = (*groupDeleteRes)(nil)
|
||||
_ mainflux.Response = (*assignMemberToGroupRes)(nil)
|
||||
_ mainflux.Response = (*removeMemberFromGroupRes)(nil)
|
||||
_ mainflux.Response = (*deleteRes)(nil)
|
||||
_ mainflux.Response = (*assignRes)(nil)
|
||||
_ mainflux.Response = (*unassignRes)(nil)
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type memberPageRes struct {
|
||||
pageRes
|
||||
Members []interface{}
|
||||
@@ -41,18 +34,19 @@ func (res memberPageRes) Empty() bool {
|
||||
}
|
||||
|
||||
type viewGroupRes struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
// Indicates a level in tree hierarchy from first group node.
|
||||
Level int `json:"level,omitempty"`
|
||||
// Path is a path in a tree, consisted of group names
|
||||
// parentName.childrenName1.childrenName2 .
|
||||
// Indicates a level in tree hierarchy from first group node - root.
|
||||
Level int `json:"level"`
|
||||
// Path in a tree consisting of group ids
|
||||
// parentID1.parentID2.childID1
|
||||
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
|
||||
Path string `json:"path"`
|
||||
Children []*viewGroupRes `json:"children"`
|
||||
Children []*viewGroupRes `json:"children,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -101,6 +95,14 @@ type groupPageRes struct {
|
||||
Groups []viewGroupRes `json:"groups"`
|
||||
}
|
||||
|
||||
type pageRes struct {
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
Level uint64 `json:"level"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (res groupPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
@@ -113,44 +115,48 @@ func (res groupPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type groupDeleteRes struct{}
|
||||
type deleteRes struct{}
|
||||
|
||||
func (res groupDeleteRes) Code() int {
|
||||
func (res deleteRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res groupDeleteRes) Headers() map[string]string {
|
||||
func (res deleteRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupDeleteRes) Empty() bool {
|
||||
func (res deleteRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type assignMemberToGroupRes struct{}
|
||||
type assignRes struct{}
|
||||
|
||||
func (res assignMemberToGroupRes) Code() int {
|
||||
func (res assignRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res assignRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res assignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type unassignRes struct{}
|
||||
|
||||
func (res unassignRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res assignMemberToGroupRes) Headers() map[string]string {
|
||||
func (res unassignRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res assignMemberToGroupRes) Empty() bool {
|
||||
func (res unassignRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type removeMemberFromGroupRes struct{}
|
||||
|
||||
func (res removeMemberFromGroupRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeMemberFromGroupRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeMemberFromGroupRes) Empty() bool {
|
||||
return true
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
)
|
||||
|
||||
const (
|
||||
maxNameSize = 254
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
levelKey = "level"
|
||||
metadataKey = "metadata"
|
||||
treeKey = "tree"
|
||||
groupType = "type"
|
||||
contentType = "application/json"
|
||||
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
defLevel = 1
|
||||
)
|
||||
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
mux.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_group")(createGroupEndpoint(svc)),
|
||||
decodeGroupCreate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "view_group")(viewGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "update_group")(updateGroupEndpoint(svc)),
|
||||
decodeGroupUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_group")(deleteGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(listGroupsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/children", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_children")(listChildrenEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/parents", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_parents_groups")(listParentsEndpoint(svc)),
|
||||
decodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(assignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "unassign")(unassignEndpoint(svc)),
|
||||
decodeAssignRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_members")(listMembersEndpoint(svc)),
|
||||
decodeListMembersRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/members/:memberID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_memberships")(listMemberships(svc)),
|
||||
decodeListMembershipsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
|
||||
}
|
||||
|
||||
func decodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
l, err := readUintQuery(r, levelKey, defLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := readBoolQuery(r, treeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listGroupsReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
level: l,
|
||||
metadata: m,
|
||||
tree: t,
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
o, err := readUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := readUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := readBoolQuery(r, treeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := readStringQuery(r, groupType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembersReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
groupType: t,
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
tree: tree,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListMembershipsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
o, err := readUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := readUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := readBoolQuery(r, treeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMembershipsReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "memberID"),
|
||||
offset: o,
|
||||
limit: l,
|
||||
metadata: m,
|
||||
tree: tree,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req createGroupReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrFailedDecode, err)
|
||||
}
|
||||
|
||||
req.token = r.Header.Get("Authorization")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, auth.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req updateGroupReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrFailedDecode, err)
|
||||
}
|
||||
|
||||
req.id = bone.GetValue(r, "groupID")
|
||||
req.token = r.Header.Get("Authorization")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := groupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := assignReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func 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 readMetadataQuery(r *http.Request, key string) (map[string]interface{}, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(vals[0]), &m)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errInvalidQueryParams, err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func readBoolQuery(r *http.Request, key string) (bool, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return true, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(vals[0])
|
||||
if err != nil {
|
||||
return false, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func readStringQuery(r *http.Request, key string) (string, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return "", errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return vals[0], nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, auth.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, auth.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, auth.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, auth.ErrMemberAlreadyAssigned):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http_test
|
||||
package keys_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -24,12 +24,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
)
|
||||
|
||||
type issueRequest struct {
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
package keys
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
package keys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
var errUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bone.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
mux.Post("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssue,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/keys/:id", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "retrieve")(retrieveEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/keys/:id", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "revoke")(revokeEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
req := issueKeyReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, auth.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, auth.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, auth.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-186
@@ -1,206 +1,24 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
groupsAPI "github.com/mainflux/mainflux/internal/groups/api"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/auth/api/http/groups"
|
||||
"github.com/mainflux/mainflux/auth/api/http/keys"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
var errUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, tracer opentracing.Tracer) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
mux := bone.New()
|
||||
|
||||
mux.Post("/keys", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
|
||||
decodeIssue,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/keys/:id", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "retrieve")(retrieveEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/keys/:id", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "revoke")(revokeEndpoint(svc)),
|
||||
decodeKeyReq,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "add_group")(groupsAPI.CreateGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupCreate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "view_group")(groupsAPI.ViewGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(groupsAPI.ListGroupsEndpoint(svc)),
|
||||
groupsAPI.DecodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/children", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_children_groups")(groupsAPI.ListGroupChildrenEndpoint(svc)),
|
||||
groupsAPI.DecodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/parents", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_parent_groups")(groupsAPI.ListGroupParentsEndpoint(svc)),
|
||||
groupsAPI.DecodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "update_group")(groupsAPI.UpdateGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_group")(groupsAPI.DeleteGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/groups/:groupID/members/:memberID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(groupsAPI.AssignEndpoint(svc)),
|
||||
groupsAPI.DecodeMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID/members/:memberID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "unassign")(groupsAPI.UnassignEndpoint(svc)),
|
||||
groupsAPI.DecodeMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/members", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_members")(groupsAPI.ListMembersEndpoint(svc)),
|
||||
groupsAPI.DecodeListMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/members/:memberID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_memberships")(groupsAPI.ListMembership(svc)),
|
||||
groupsAPI.DecodeListMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux = keys.MakeHandler(svc, mux, tracer)
|
||||
mux = groups.MakeHandler(svc, mux, tracer)
|
||||
mux.GetFunc("/version", mainflux.Version("auth"))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
req := issueKeyReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeKeyReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := keyReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, auth.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, auth.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(err, auth.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(err, auth.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-24
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
@@ -96,9 +95,9 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, token, sub, obj, act
|
||||
return lm.svc.Authorize(ctx, token, sub, obj, act)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, g groups.Group) (id string, err error) {
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (g auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, g.Name, time.Since(begin))
|
||||
message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, group.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -106,12 +105,12 @@ func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, g gr
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateGroup(ctx, token, g)
|
||||
return lm.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, g groups.Group) (gr groups.Group, err error) {
|
||||
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, g.Name, time.Since(begin))
|
||||
message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, group.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -119,7 +118,7 @@ func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, g gr
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateGroup(ctx, token, g)
|
||||
return lm.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
@@ -135,7 +134,7 @@ func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id s
|
||||
return lm.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (g groups.Group, err error) {
|
||||
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -148,7 +147,7 @@ func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (g
|
||||
return lm.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_groups for token %s took %s to complete", token, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -158,10 +157,10 @@ func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, level
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListGroups(ctx, token, level, gm)
|
||||
return lm.svc.ListGroups(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_children for token %s and parent %s took %s to complete", token, parentID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -171,10 +170,10 @@ func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID s
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChildren(ctx, token, parentID, level, gm)
|
||||
return lm.svc.ListChildren(ctx, token, parentID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_parents for token %s and child %s took for child %s to complete", token, childID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -184,10 +183,10 @@ func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID str
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListParents(ctx, token, childID, level, gm)
|
||||
return lm.svc.ListParents(ctx, token, childID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.MemberPage, err error) {
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_members for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -197,10 +196,10 @@ func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID str
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
|
||||
return lm.svc.ListMembers(ctx, token, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, groupID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_memberships for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -210,12 +209,12 @@ func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, groupID
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMemberships(ctx, token, groupID, offset, limit, gm)
|
||||
return lm.svc.ListMemberships(ctx, token, groupID, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Assign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
func (lm *loggingMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberID, groupID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -223,12 +222,12 @@ func (lm *loggingMiddleware) Assign(ctx context.Context, token, memberID, groupI
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Assign(ctx, token, memberID, groupID)
|
||||
return lm.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unassign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
func (lm *loggingMiddleware) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberID, groupID, time.Since(begin))
|
||||
message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberIDs, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -236,5 +235,5 @@ func (lm *loggingMiddleware) Unassign(ctx context.Context, token, memberID, grou
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Unassign(ctx, token, memberID, groupID)
|
||||
return lm.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
+19
-20
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
)
|
||||
|
||||
var _ auth.Service = (*metricsMiddleware)(nil)
|
||||
@@ -74,20 +73,20 @@ func (ms *metricsMiddleware) Authorize(ctx context.Context, token, sub, obj, act
|
||||
return ms.svc.Authorize(ctx, token, sub, obj, act)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, g groups.Group) (id string, err error) {
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_group").Add(1)
|
||||
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.CreateGroup(ctx, token, g)
|
||||
return ms.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, g groups.Group) (gr groups.Group, err error) {
|
||||
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, group auth.Group) (gr auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_group").Add(1)
|
||||
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.UpdateGroup(ctx, token, g)
|
||||
return ms.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
@@ -98,7 +97,7 @@ func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id s
|
||||
return ms.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (g groups.Group, err error) {
|
||||
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (group auth.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_group").Add(1)
|
||||
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
|
||||
@@ -107,65 +106,65 @@ func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (g
|
||||
return ms.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_groups").Add(1)
|
||||
ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListGroups(ctx, token, level, gm)
|
||||
return ms.svc.ListGroups(ctx, token, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "parents").Add(1)
|
||||
ms.latency.With("method", "parents").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListParents(ctx, token, childID, level, gm)
|
||||
return ms.svc.ListParents(ctx, token, childID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_children").Add(1)
|
||||
ms.latency.With("method", "list_children").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChildren(ctx, token, parentID, level, gm)
|
||||
return ms.svc.ListChildren(ctx, token, parentID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.MemberPage, err error) {
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID, groupType string, pm auth.PageMetadata) (gp auth.MemberPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_members").Add(1)
|
||||
ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
|
||||
return ms.svc.ListMembers(ctx, token, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, groupID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_memberships").Add(1)
|
||||
ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMemberships(ctx, token, groupID, offset, limit, gm)
|
||||
return ms.svc.ListMemberships(ctx, token, groupID, pm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Assign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
func (ms *metricsMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "assign").Add(1)
|
||||
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Assign(ctx, token, memberID, groupID)
|
||||
return ms.svc.Assign(ctx, token, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID string, memberIDs ...string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "unassign").Add(1)
|
||||
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Unassign(ctx, token, memberID, groupID)
|
||||
return ms.svc.Unassign(ctx, token, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MaxLevel = uint64(5)
|
||||
const MinLevel = uint64(1)
|
||||
|
||||
var (
|
||||
// ErrMaxLevelExceeded malformed entity.
|
||||
ErrMaxLevelExceeded = errors.New("level must be less than or equal 5")
|
||||
|
||||
// ErrBadGroupName malformed entity.
|
||||
ErrBadGroupName = errors.New("incorrect group name")
|
||||
|
||||
// ErrGroupConflict group conflict.
|
||||
ErrGroupConflict = errors.New("group already exists")
|
||||
|
||||
// ErrCreateGroup indicates failure to create group.
|
||||
ErrCreateGroup = errors.New("failed to create group")
|
||||
|
||||
// ErrFetchGroups indicates failure to fetch groups.
|
||||
ErrFetchGroups = errors.New("failed to fetch groups")
|
||||
|
||||
// ErrUpdateGroup indicates failure to update group.
|
||||
ErrUpdateGroup = errors.New("failed to update group")
|
||||
|
||||
// ErrDeleteGroup indicates failure to delete group.
|
||||
ErrDeleteGroup = errors.New("failed to delete group")
|
||||
|
||||
// ErrGroupNotFound indicates failure to find group.
|
||||
ErrGroupNotFound = errors.New("failed to find group")
|
||||
|
||||
// ErrAssignToGroup indicates failure to assign member to a group.
|
||||
ErrAssignToGroup = errors.New("failed to assign member to a group")
|
||||
|
||||
// ErrUnassignFromGroup indicates failure to unassign member from a group.
|
||||
ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
|
||||
|
||||
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
|
||||
ErrUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// ErrFailedDecode indicates failed to decode request body
|
||||
ErrFailedDecode = errors.New("failed to decode request body")
|
||||
|
||||
// ErrMissingParent indicates that parent can't be found
|
||||
ErrMissingParent = errors.New("failed to retrieve parent")
|
||||
|
||||
// ErrGroupNotEmpty indicates group is not empty, can't be deleted.
|
||||
ErrGroupNotEmpty = errors.New("group is not empty")
|
||||
|
||||
// ErrMemberAlreadyAssigned indicates that members is already assigned.
|
||||
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
|
||||
|
||||
// ErrSelectEntity indicates error while reading entity from database
|
||||
ErrSelectEntity = errors.New("select entity from db error")
|
||||
)
|
||||
|
||||
type GroupMetadata map[string]interface{}
|
||||
|
||||
type Member struct {
|
||||
ID string
|
||||
Type string
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
ID string
|
||||
OwnerID string
|
||||
ParentID string
|
||||
Name string
|
||||
Description string
|
||||
Metadata GroupMetadata
|
||||
// Indicates a level in tree hierarchy.
|
||||
// Root node is level 1.
|
||||
Level int
|
||||
// Path in a tree consisting of group ids
|
||||
// parentID1.parentID2.childID1
|
||||
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
|
||||
Path string
|
||||
Children []*Group
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Size uint64
|
||||
Level uint64
|
||||
Name string
|
||||
Type string
|
||||
Metadata GroupMetadata
|
||||
}
|
||||
|
||||
type GroupPage struct {
|
||||
PageMetadata
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
type MemberPage struct {
|
||||
PageMetadata
|
||||
Members []Member
|
||||
}
|
||||
|
||||
type GroupService interface {
|
||||
// CreateGroup creates new group.
|
||||
CreateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// UpdateGroup updates the group identified by the provided ID.
|
||||
UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// ViewGroup retrieves data about the group identified by ID.
|
||||
ViewGroup(ctx context.Context, token, id string) (Group, error)
|
||||
|
||||
// ListGroups retrieves groups.
|
||||
ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListChildren retrieves groups that are children to group identified by parentID
|
||||
ListChildren(ctx context.Context, token, parentID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListParents retrieves groups that are parent to group identified by childID.
|
||||
ListParents(ctx context.Context, token, childID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// ListMembers retrieves everything that is assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID, groupType string, pm PageMetadata) (MemberPage, error)
|
||||
|
||||
// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
|
||||
ListMemberships(ctx context.Context, token, memberID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RemoveGroup removes the group identified with the provided ID.
|
||||
RemoveGroup(ctx context.Context, token, id string) error
|
||||
|
||||
// Assign adds a member with memberID into the group identified by groupID.
|
||||
Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) error
|
||||
|
||||
// Unassign removes member with memberID from group identified by groupID.
|
||||
Unassign(ctx context.Context, token, groupID string, memberIDs ...string) error
|
||||
}
|
||||
|
||||
type GroupRepository interface {
|
||||
// Save group
|
||||
Save(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Update a group
|
||||
Update(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Delete a group
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// RetrieveByID retrieves group by its id
|
||||
RetrieveByID(ctx context.Context, id string) (Group, error)
|
||||
|
||||
// RetrieveAll retrieves all groups.
|
||||
RetrieveAll(ctx context.Context, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
|
||||
RetrieveAllParents(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
|
||||
RetrieveAllChildren(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// Retrieves list of groups that member belongs to
|
||||
Memberships(ctx context.Context, memberID string, pm PageMetadata) (GroupPage, error)
|
||||
|
||||
// Members retrieves everything that is assigned to a group identified by groupID.
|
||||
Members(ctx context.Context, groupID, groupType string, pm PageMetadata) (MemberPage, error)
|
||||
|
||||
// Assign adds a member to group.
|
||||
Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error
|
||||
|
||||
// Unassign removes a member from a group
|
||||
Unassign(ctx context.Context, groupID string, memberIDs ...string) error
|
||||
}
|
||||
+199
-89
@@ -5,204 +5,314 @@ package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
var _ groups.Repository = (*groupRepositoryMock)(nil)
|
||||
var _ auth.GroupRepository = (*groupRepositoryMock)(nil)
|
||||
|
||||
type groupRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
groups map[string]groups.Group
|
||||
// Map of "Maps of users assigned to a group" where group is a key
|
||||
childrenByGroups map[string]map[string]groups.Group
|
||||
groupsByMember map[string]map[string]groups.Group
|
||||
members map[string]map[string]interface{}
|
||||
mu sync.Mutex
|
||||
// Map of groups, group id as a key.
|
||||
// groups map[GroupID]auth.Group
|
||||
groups map[string]auth.Group
|
||||
// Map of groups with group id as key that are
|
||||
// children (i.e. has same parent id) is element
|
||||
// in children's map where parent id is key.
|
||||
// children map[ParentID]map[GroupID]auth.Group
|
||||
children map[string]map[string]auth.Group
|
||||
// Map of parents' id with child group id as key.
|
||||
// Each child has one parent.
|
||||
// parents map[ChildID]ParentID
|
||||
parents map[string]string
|
||||
// Map of groups (with group id as key) which
|
||||
// represent memberships is element in
|
||||
// memberships' map where member id is a key.
|
||||
// memberships map[MemberID]map[GroupID]auth.Group
|
||||
memberships map[string]map[string]auth.Group
|
||||
// Map of group members where member id is a key
|
||||
// is an element in the map members where group id is a key.
|
||||
// members map[type][GroupID]map[MemberID]MemberID
|
||||
members map[string]map[string]map[string]string
|
||||
}
|
||||
|
||||
// NewGroupRepository creates in-memory user repository
|
||||
func NewGroupRepository() groups.Repository {
|
||||
func NewGroupRepository() auth.GroupRepository {
|
||||
return &groupRepositoryMock{
|
||||
groups: make(map[string]groups.Group),
|
||||
childrenByGroups: make(map[string]map[string]groups.Group),
|
||||
groups: make(map[string]auth.Group),
|
||||
children: make(map[string]map[string]auth.Group),
|
||||
parents: make(map[string]string),
|
||||
memberships: make(map[string]map[string]auth.Group),
|
||||
members: make(map[string]map[string]map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
func (grm *groupRepositoryMock) Save(ctx context.Context, group auth.Group) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[g.ID]; ok {
|
||||
return groups.Group{}, groups.ErrGroupConflict
|
||||
if _, ok := grm.groups[group.ID]; ok {
|
||||
return auth.Group{}, auth.ErrGroupConflict
|
||||
}
|
||||
path := group.ID
|
||||
|
||||
if group.ParentID != "" {
|
||||
parent, ok := grm.groups[group.ParentID]
|
||||
if !ok {
|
||||
return auth.Group{}, auth.ErrCreateGroup
|
||||
}
|
||||
if _, ok := grm.children[group.ParentID]; !ok {
|
||||
grm.children[group.ParentID] = make(map[string]auth.Group)
|
||||
}
|
||||
grm.children[group.ParentID][group.ID] = group
|
||||
grm.parents[group.ID] = group.ParentID
|
||||
path = fmt.Sprintf("%s.%s", parent.Path, path)
|
||||
}
|
||||
|
||||
if g.ParentID != "" {
|
||||
if _, ok := grm.groups[g.ParentID]; !ok {
|
||||
return groups.Group{}, groups.ErrCreateGroup
|
||||
}
|
||||
if _, ok := grm.childrenByGroups[g.ParentID]; !ok {
|
||||
grm.childrenByGroups[g.ParentID] = make(map[string]groups.Group)
|
||||
}
|
||||
grm.childrenByGroups[g.ParentID][g.ID] = g
|
||||
}
|
||||
grm.groups[g.ID] = g
|
||||
return g, nil
|
||||
group.Path = path
|
||||
group.Level = len(strings.Split(path, "."))
|
||||
|
||||
grm.groups[group.ID] = group
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
func (grm *groupRepositoryMock) Update(ctx context.Context, group auth.Group) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
up, ok := grm.groups[g.ID]
|
||||
up, ok := grm.groups[group.ID]
|
||||
if !ok {
|
||||
return groups.Group{}, groups.ErrNotFound
|
||||
return auth.Group{}, auth.ErrNotFound
|
||||
}
|
||||
up.Name = g.Name
|
||||
up.Description = g.Description
|
||||
up.Metadata = g.Metadata
|
||||
up.Name = group.Name
|
||||
up.Description = group.Description
|
||||
up.Metadata = group.Metadata
|
||||
up.UpdatedAt = time.Now()
|
||||
|
||||
grm.groups[g.ID] = up
|
||||
if g.ParentID != "" {
|
||||
grm.childrenByGroups[g.ParentID][g.ID] = g
|
||||
}
|
||||
return g, nil
|
||||
grm.groups[group.ID] = up
|
||||
return up, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[id]; !ok {
|
||||
return groups.ErrNotFound
|
||||
return auth.ErrGroupNotFound
|
||||
}
|
||||
|
||||
if len(grm.members[id]) > 0 {
|
||||
return auth.ErrGroupNotEmpty
|
||||
}
|
||||
|
||||
// This is not quite exact, it should go in depth
|
||||
for _, ch := range grm.children[id] {
|
||||
if len(grm.members[ch.ID]) > 0 {
|
||||
return auth.ErrGroupNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// This is not quite exact, it should go in depth
|
||||
delete(grm.groups, id)
|
||||
delete(grm.childrenByGroups, id)
|
||||
for _, ch := range grm.children[id] {
|
||||
delete(grm.members, ch.ID)
|
||||
}
|
||||
|
||||
delete(grm.children, id)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
|
||||
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
|
||||
val, ok := grm.groups[id]
|
||||
if !ok {
|
||||
return groups.Group{}, groups.ErrNotFound
|
||||
return auth.Group{}, auth.ErrGroupNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAll(ctx context.Context, level uint64, m groups.Metadata) (groups.GroupPage, error) {
|
||||
func (grm *groupRepositoryMock) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []groups.Group
|
||||
var items []auth.Group
|
||||
for _, g := range grm.groups {
|
||||
items = append(items, g)
|
||||
}
|
||||
return groups.GroupPage{
|
||||
return auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Unassign(ctx context.Context, memberID, groupID string) error {
|
||||
func (grm *groupRepositoryMock) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return groups.ErrNotFound
|
||||
return auth.ErrGroupNotFound
|
||||
}
|
||||
for _, memberID := range memberIDs {
|
||||
for typ, m := range grm.members[groupID] {
|
||||
_, ok := m[memberID]
|
||||
if !ok {
|
||||
return auth.ErrGroupNotFound
|
||||
}
|
||||
delete(grm.members[groupID][typ], memberID)
|
||||
delete(grm.memberships[memberID], groupID)
|
||||
}
|
||||
|
||||
}
|
||||
delete(grm.members[groupID], memberID)
|
||||
delete(grm.groupsByMember, memberID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Assign(ctx context.Context, memberID, groupID string) error {
|
||||
func (grm *groupRepositoryMock) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return groups.ErrNotFound
|
||||
return auth.ErrGroupNotFound
|
||||
}
|
||||
|
||||
if _, ok := grm.members[groupID]; !ok {
|
||||
grm.members[groupID] = make(map[string]interface{})
|
||||
grm.members[groupID] = make(map[string]map[string]string)
|
||||
}
|
||||
|
||||
grm.members[groupID][memberID] = memberID
|
||||
grm.groupsByMember[memberID][groupID] = grm.groups[groupID]
|
||||
for _, memberID := range memberIDs {
|
||||
if _, ok := grm.members[groupID][groupType]; !ok {
|
||||
grm.members[groupID][groupType] = make(map[string]string)
|
||||
}
|
||||
if _, ok := grm.memberships[memberID]; !ok {
|
||||
grm.memberships[memberID] = make(map[string]auth.Group)
|
||||
}
|
||||
|
||||
grm.members[groupID][groupType][memberID] = memberID
|
||||
grm.memberships[memberID][groupID] = grm.groups[groupID]
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Memberships(ctx context.Context, memberID string, offset, limit uint64, um groups.Metadata) (groups.GroupPage, error) {
|
||||
func (grm *groupRepositoryMock) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []groups.Group
|
||||
memberships, ok := grm.groupsByMember[memberID]
|
||||
if !ok {
|
||||
return groups.GroupPage{}, groups.ErrNotFound
|
||||
var items []auth.Group
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
i := uint64(0)
|
||||
for _, g := range grm.memberships[memberID] {
|
||||
if i >= first && i < last {
|
||||
items = append(items, g)
|
||||
}
|
||||
i++
|
||||
}
|
||||
for _, g := range memberships {
|
||||
items = append(items, g)
|
||||
}
|
||||
return groups.GroupPage{
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Limit: pm.Limit,
|
||||
Offset: pm.Offset,
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Members(ctx context.Context, groupID string, offset, limit uint64, m groups.Metadata) (groups.MemberPage, error) {
|
||||
func (grm *groupRepositoryMock) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []groups.Member
|
||||
members, ok := grm.members[groupID]
|
||||
var items []auth.Member
|
||||
members, ok := grm.members[groupID][groupType]
|
||||
if !ok {
|
||||
return groups.MemberPage{}, groups.ErrNotFound
|
||||
return auth.MemberPage{}, auth.ErrGroupNotFound
|
||||
}
|
||||
|
||||
first := uint64(pm.Offset)
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
i := uint64(0)
|
||||
for _, g := range members {
|
||||
items = append(items, g)
|
||||
if i >= first && i < last {
|
||||
items = append(items, auth.Member{ID: g, Type: groupType})
|
||||
}
|
||||
i++
|
||||
}
|
||||
return groups.MemberPage{
|
||||
return auth.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID string, level uint64, m groups.Metadata) (groups.GroupPage, error) {
|
||||
func (grm *groupRepositoryMock) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if groupID == "" {
|
||||
return groups.GroupPage{}, nil
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
|
||||
var items []groups.Group
|
||||
parent, ok := grm.groups[groupID]
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return groups.GroupPage{}, nil
|
||||
return auth.GroupPage{}, auth.ErrGroupNotFound
|
||||
}
|
||||
|
||||
for {
|
||||
items = append(items, parent)
|
||||
parent, ok = grm.groups[parent.ParentID]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
groups := make([]auth.Group, 0)
|
||||
groups, err := grm.getParents(groups, group)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
return groups.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: uint64(len(items)),
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: groups,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(groups)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, um groups.Metadata) (groups.GroupPage, error) {
|
||||
panic("not implemented")
|
||||
func (grm *groupRepositoryMock) getParents(groups []auth.Group, group auth.Group) ([]auth.Group, error) {
|
||||
groups = append(groups, group)
|
||||
parentID, ok := grm.parents[group.ID]
|
||||
if !ok && parentID == "" {
|
||||
return groups, nil
|
||||
}
|
||||
parent, ok := grm.groups[parentID]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("parent with id: %s not found", parentID))
|
||||
}
|
||||
return grm.getParents(groups, parent)
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
group, ok := grm.groups[groupID]
|
||||
if !ok {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
|
||||
groups := make([]auth.Group, 0)
|
||||
groups = append(groups, group)
|
||||
for ch := range grm.parents {
|
||||
g, ok := grm.groups[ch]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("child with id %s not found", ch))
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
return auth.GroupPage{
|
||||
Groups: groups,
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: uint64(len(groups)),
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
+458
-14
@@ -3,7 +3,6 @@ info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
@@ -34,9 +33,8 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
security:
|
||||
- Authorization: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
@@ -53,9 +51,8 @@ paths:
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ApiKeyId"
|
||||
security:
|
||||
- Authorization: []
|
||||
responses:
|
||||
'204':
|
||||
description: Key revoked.
|
||||
@@ -63,13 +60,214 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/groups:
|
||||
post:
|
||||
summary: Creates new group
|
||||
description: |
|
||||
Creates new group that can be used for grouping entities - things, users.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/requestBodies/GroupCreateReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets all groups.
|
||||
description: |
|
||||
Gets all groups up to a max level of hierarchy that can be fetched in one
|
||||
request ( max level = 5). Result can be filtered by metadata. Groups will
|
||||
be returned as JSON array or JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}:
|
||||
get:
|
||||
summary: Gets group info.
|
||||
description: |
|
||||
Gets info on a group specified by id.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates group data.
|
||||
description: |
|
||||
Updates Name, Description or Metadata of a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/GroupUpdateReq"
|
||||
responses:
|
||||
'200':
|
||||
description: Group updated.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Deletes group.
|
||||
description: |
|
||||
Deletes group. If group is parent and descendant groups do not have any members
|
||||
child groups will be deleted. Group cannot be deleted if has members or if
|
||||
any descendant group has members.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'204':
|
||||
description: Group removed.
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}/children:
|
||||
get:
|
||||
summary: Gets group children.
|
||||
description: |
|
||||
Gets the whole tree of descendants of group for given id including itself.
|
||||
For performance reason request is limited up to a given level of hierarchy
|
||||
(max. 5).
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}/parents:
|
||||
get:
|
||||
summary: Gets group info.
|
||||
description: |
|
||||
Gets a direct line of ancestors for a group specified by id.
|
||||
Result is up to a specified hierarchy level or up to a root group.
|
||||
Result can be a JSON array or a JSON tree.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/GroupsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: Group does not exist.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/groups/{groupId}/members:
|
||||
post:
|
||||
summary: Assigns members to a group.
|
||||
description: |
|
||||
Assigns thing or user id to a group.
|
||||
tags:
|
||||
- auth
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/MembersReq"
|
||||
responses:
|
||||
'201':
|
||||
$ref: "#/components/responses/GroupCreateRes"
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'409':
|
||||
description: Failed due to using an existing email address.
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets members of a group.
|
||||
description: |
|
||||
Array of member ids that are in the group specified with groupID.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/MemberType"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/MembersRes"
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
components:
|
||||
securitySchemes:
|
||||
Authorization:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: jwt
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
@@ -104,8 +302,141 @@ components:
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires. If this field is missing,
|
||||
that means that Key is valid indefinitely.
|
||||
|
||||
GroupReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
parent_id:
|
||||
type: string
|
||||
format: ulid
|
||||
description: Id of parent group, it must be existing group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
GroupUpdateSchema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Free-form group name. Group name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
description: Group description, free form text.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
GroupResSchema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
description: Unique group identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form group name.
|
||||
parent_id:
|
||||
type: string
|
||||
description: Group ID of parent group.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of user that created the group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
level:
|
||||
type: integer
|
||||
description: Level in hierarchy, distance from the root group.
|
||||
path:
|
||||
type: string
|
||||
description: Hierarchy path, concatenated ids of group ancestors.
|
||||
children:
|
||||
type: object
|
||||
# schema: GroupResSchema
|
||||
created_at:
|
||||
type: string
|
||||
description: Datetime of group creation.
|
||||
updated_at:
|
||||
type: string
|
||||
description: Datetime of last group updated.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- owner_id
|
||||
- description
|
||||
- level
|
||||
- path
|
||||
- created_at
|
||||
- updated_at
|
||||
MembersReqSchema:
|
||||
type: object
|
||||
properties:
|
||||
members:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
format: uuid | ulid
|
||||
type:
|
||||
type: string
|
||||
description: Type of entity
|
||||
GroupsPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
level:
|
||||
type: integer
|
||||
description: Level of hierarchy up to which groups are fetched.
|
||||
required:
|
||||
- groups
|
||||
- total
|
||||
- level
|
||||
MembershipPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
required:
|
||||
- groups
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
ApiKeyId:
|
||||
name: id
|
||||
description: API Key ID.
|
||||
@@ -114,7 +445,66 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Group ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
MemberType:
|
||||
name: type
|
||||
description: Member type association.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
enum: [users, things]
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Level:
|
||||
name: level
|
||||
description: Level of hierarchy up to which to retrieve groups from given group id.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 5
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
Tree:
|
||||
name: tree
|
||||
description: Specify type of response, JSON array or tree.
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
@@ -138,7 +528,27 @@ components:
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
GroupCreateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupReqSchema"
|
||||
GroupUpdateReq:
|
||||
description: JSON-formatted document describing group create request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupUpdateSchema"
|
||||
MembersReq:
|
||||
description: JSON array of member IDs.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembersReqSchema"
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
@@ -148,3 +558,37 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
GroupCreateRes:
|
||||
description: Group created.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created group's relative URL.
|
||||
example: /groups/{groupId}
|
||||
GroupRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupResSchema"
|
||||
GroupsPageRes:
|
||||
description: Group data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupsPage"
|
||||
MembersRes:
|
||||
description: Groups data retrieved. Groups assigned to a member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
MembershipPageRes:
|
||||
description: Groups data retrieved. Groups assigned to a member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembershipPage"
|
||||
|
||||
+370
-326
@@ -13,75 +13,88 @@ import (
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
const maxLevel = 5
|
||||
|
||||
var (
|
||||
errDeleteGroupDB = errors.New("delete group failed")
|
||||
errSelectDb = errors.New("select group from db error")
|
||||
errConvertingStringToUUID = errors.New("error converting string")
|
||||
errInvalidGroupType = errors.New("invalid group type")
|
||||
errUpdateDB = errors.New("failed to update db")
|
||||
errRetrieveDB = errors.New("failed retrieving from db")
|
||||
errStringToUUID = errors.New("error converting string")
|
||||
errGetTotal = errors.New("failed to get total number of groups")
|
||||
errCreateMetadataQuery = errors.New("failed to create query for metadata")
|
||||
|
||||
errTruncation = "string_data_right_truncation"
|
||||
errFK = "foreign_key_violation"
|
||||
groupIDFkeyy = "group_relations_group_id_fkey"
|
||||
)
|
||||
|
||||
var _ groups.Repository = (*groupRepository)(nil)
|
||||
var _ auth.GroupRepository = (*groupRepository)(nil)
|
||||
|
||||
type groupRepository struct {
|
||||
db Database
|
||||
types map[string]dbGroupType
|
||||
db Database
|
||||
}
|
||||
|
||||
// NewGroupRepo instantiates a PostgreSQL implementation of group
|
||||
// repository.
|
||||
func NewGroupRepo(db Database) groups.Repository {
|
||||
q := `SELECT * FROM group_type`
|
||||
rows, err := db.QueryxContext(context.Background(), q)
|
||||
if err != nil {
|
||||
pqErr, _ := err.(*pq.Error)
|
||||
// If there is a problem with group type setup exit.
|
||||
panic(pqErr)
|
||||
}
|
||||
|
||||
types := map[string]dbGroupType{}
|
||||
for rows.Next() {
|
||||
dbgrt := dbGroupType{}
|
||||
if err := rows.StructScan(&dbgrt); err != nil {
|
||||
panic(errors.Wrap(errSelectDb, err))
|
||||
}
|
||||
if _, ok := types[dbgrt.Name]; ok {
|
||||
panic(fmt.Sprintf("duplicated group type: %s", dbgrt.Name))
|
||||
}
|
||||
types[dbgrt.Name] = dbgrt
|
||||
}
|
||||
|
||||
func NewGroupRepo(db Database) auth.GroupRepository {
|
||||
return &groupRepository{
|
||||
db: db,
|
||||
types: types,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (gr groupRepository) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
var id string
|
||||
q := `INSERT INTO groups (name, description, id, owner_id, metadata, path, type, created_at, updated_at)
|
||||
VALUES (:name, :description, :id, :owner_id, :metadata, :name, :type, now(), now()) RETURNING id`
|
||||
func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
// For root group path is initialized with id
|
||||
q := `INSERT INTO groups (name, description, id, path, owner_id, metadata, created_at, updated_at)
|
||||
VALUES (:name, :description, :id, :id, :owner_id, :metadata, :created_at, :updated_at)
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
if g.ParentID != "" {
|
||||
// For children groups type is inherited from the parent, this is done in trigger inherit_type_tr - init.go
|
||||
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, path)
|
||||
SELECT :name, :description, :id, :owner_id, :parent_id, :metadata, text2ltree(ltree2text(tg.path) || '.' || :name) FROM groups tg WHERE id = :parent_id RETURNING id`
|
||||
// Path is constructed in insert_group_tr - init.go
|
||||
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at)
|
||||
VALUES ( :name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at)
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
}
|
||||
|
||||
dbg, err := gr.toDBGroup(g)
|
||||
if err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbg)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, err)
|
||||
case errDuplicate:
|
||||
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return auth.Group{}, errors.Wrap(auth.ErrCreateGroup, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
dbg = dbGroup{}
|
||||
if err := row.StructScan(&dbg); err != nil {
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
return toGroup(dbg)
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
|
||||
|
||||
dbu, err := gr.toDBGroup(g)
|
||||
if err != nil {
|
||||
return groups.Group{}, err
|
||||
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, err)
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
|
||||
@@ -90,294 +103,268 @@ func (gr groupRepository) Save(ctx context.Context, g groups.Group) (groups.Grou
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return groups.Group{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
return auth.Group{}, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return groups.Group{}, errors.Wrap(groups.ErrGroupConflict, err)
|
||||
return auth.Group{}, errors.Wrap(auth.ErrGroupConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return groups.Group{}, errors.Wrap(groups.ErrCreateGroup, err)
|
||||
return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
if err := row.Scan(&id); err != nil {
|
||||
return groups.Group{}, err
|
||||
}
|
||||
g.ID = id
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = now() WHERE id = :id`
|
||||
|
||||
dbu, err := gr.toDBGroup(g)
|
||||
if err != nil {
|
||||
return groups.Group{}, errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
|
||||
return groups.Group{}, errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
qd := `DELETE FROM groups WHERE id = :id`
|
||||
group := groups.Group{
|
||||
ID: groupID,
|
||||
}
|
||||
dbg, err := gr.toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDeleteGroupDB, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errDeleteGroupDB, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(groups.ErrDeleteGroup, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
|
||||
dbu := dbGroup{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level FROM groups WHERE id = $1`
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return groups.Group{}, errors.Wrap(groups.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return groups.Group{}, errors.Wrap(errRetrieveDB, err)
|
||||
dbu = dbGroup{}
|
||||
if err := row.StructScan(&dbu); err != nil {
|
||||
return g, errors.Wrap(auth.ErrUpdateGroup, err)
|
||||
}
|
||||
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
qd := `DELETE FROM groups WHERE id = :id`
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
dbg, err := gr.toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrUpdateGroup, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
switch pqErr.Constraint {
|
||||
case groupIDFkeyy:
|
||||
return errors.Wrap(auth.ErrGroupNotEmpty, err)
|
||||
}
|
||||
return errors.Wrap(auth.ErrGroupConflict, err)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(auth.ErrUpdateGroup, errors.New(pqErr.Message))
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrDeleteGroup, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(auth.ErrDeleteGroup, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
dbu := dbGroup{
|
||||
ID: id,
|
||||
}
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups WHERE id = $1`
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return auth.Group{}, errors.Wrap(auth.ErrGroupNotFound, err)
|
||||
|
||||
}
|
||||
return auth.Group{}, errors.Wrap(auth.ErrSelectEntity, err)
|
||||
}
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
_, metaQuery, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
var mq string
|
||||
if metaQuery != "" {
|
||||
mq = fmt.Sprintf(" AND %s", metaQuery)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
|
||||
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
|
||||
cq := fmt.Sprintf("SELECT COUNT(*) FROM groups WHERE nlevel(path) <= :level %s", mq)
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", "", "", level, gm)
|
||||
dbPage, err := toDBGroupPage("", "", pm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := processRows(rows)
|
||||
items, err := gr.processRows(rows)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
cq := "SELECT COUNT(*) FROM groups"
|
||||
if metaQuery != "" {
|
||||
cq = fmt.Sprintf(" %s WHERE %s", cq, metaQuery)
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return groups.GroupPage{}, nil
|
||||
}
|
||||
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level`
|
||||
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path @> parent.path`
|
||||
|
||||
_, mq, err := getGroupsMetadataQuery("groups", gm)
|
||||
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveParents, err)
|
||||
}
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) < :level`
|
||||
|
||||
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path <@ parent.path `
|
||||
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveChildren, err)
|
||||
}
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) retrieve(ctx context.Context, groupID, retQuery, cntQuery string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return auth.GroupPage{}, nil
|
||||
}
|
||||
_, mq, err := getGroupsMetadataQuery("g", pm.Metadata)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :parent_id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level %s`, mq)
|
||||
retQuery = fmt.Sprintf(`%s %s`, retQuery, mq)
|
||||
cntQuery = fmt.Sprintf(`%s %s`, cntQuery, mq)
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :parent_id AND g.path @> parent.path %s`, mq)
|
||||
|
||||
if level > maxLevel {
|
||||
level = maxLevel
|
||||
dbPage, err := toDBGroupPage(groupID, "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", groupID, "", level, gm)
|
||||
rows, err := gr.db.NamedQueryContext(ctx, retQuery, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := processRows(rows)
|
||||
items, err := gr.processRows(rows)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
total, err := total(ctx, gr.db, cntQuery, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Level: pm.Level,
|
||||
Total: total,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return groups.GroupPage{}, nil
|
||||
}
|
||||
_, mq, err := getGroupsMetadataQuery("groups", gm)
|
||||
func (gr groupRepository) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM groups parent, groups g
|
||||
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) <= :level %s`, mq)
|
||||
q := fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
|
||||
WHERE gr.group_id = :group_id AND gr.type = :type %s`, mq)
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path <@ parent.path %s`, mq)
|
||||
|
||||
if level > maxLevel {
|
||||
level = maxLevel
|
||||
if groupType == "" {
|
||||
q = fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
|
||||
WHERE gr.group_id = :group_id %s`, mq)
|
||||
}
|
||||
|
||||
dbPage, err := toDBGroupPage("", groupID, "", "", level, gm)
|
||||
params, err := gr.toDBMemberPage("", groupID, groupType, pm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := processRows(rows)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: total,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Members(ctx context.Context, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
m, mq, err := getGroupsMetadataQuery("groups", gm)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT gr.member_id FROM groups, group_relations gr
|
||||
WHERE gr.group_id = :group AND gr.group_id = g.id
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"group": groupID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"metadata": m,
|
||||
return auth.MemberPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []groups.Member
|
||||
var items []auth.Member
|
||||
for rows.Next() {
|
||||
member := dbMember{}
|
||||
if err := rows.StructScan(&member); err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, err
|
||||
return auth.MemberPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, groups.Member(member.ID))
|
||||
items = append(items, auth.Member{ID: member.MemberID, Type: member.Type})
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups, group_relations g
|
||||
WHERE g.group_id = groups.id AND g.group_id = :group %s;`, mq)
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups g, group_relations gr
|
||||
WHERE gr.group_id = :group_id AND gr.group_id = g.id AND gr.type = :type %s;`, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
|
||||
page := groups.MemberPage{
|
||||
page := auth.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Memberships(ctx context.Context, userID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
m, mq, err := getGroupsMetadataQuery("groups", gm)
|
||||
func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
|
||||
if mq != "" {
|
||||
@@ -385,99 +372,140 @@ func (gr groupRepository) Memberships(ctx context.Context, userID string, offset
|
||||
}
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :userID
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"userID": userID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"metadata": m,
|
||||
params, err := gr.toDBMemberPage("", "", "", pm)
|
||||
if err != nil {
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []groups.Group
|
||||
var items []auth.Group
|
||||
for rows.Next() {
|
||||
dbgr := dbGroup{}
|
||||
if err := rows.StructScan(&dbgr); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
dbg := dbGroup{}
|
||||
if err := rows.StructScan(&dbg); err != nil {
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
gr, err := toGroup(dbgr)
|
||||
gr, err := toGroup(dbg)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
return auth.GroupPage{}, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :userID %s;`, mq)
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.member_id = :member_id %s `, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
page := auth.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
PageMetadata: auth.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Size: uint64(len(items)),
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Assign(ctx context.Context, memberID, groupID string) error {
|
||||
dbr, err := toDBGroupRelation(memberID, groupID)
|
||||
func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string, ids ...string) error {
|
||||
tx, err := gr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(groups.ErrAssignToGroup, err)
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
qIns := `INSERT INTO group_relations (group_id, member_id) VALUES (:group_id, :member_id)`
|
||||
_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return errors.Wrap(groups.ErrGroupConflict, err)
|
||||
case errFK:
|
||||
return errors.Wrap(groups.ErrNotFound, err)
|
||||
}
|
||||
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
|
||||
VALUES(:group_id, :member_id, :type, :created_at, :updated_at)`
|
||||
|
||||
for _, id := range ids {
|
||||
dbg, err := toDBGroupRelation(id, groupID, groupType)
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
return errors.Wrap(groups.ErrAssignToGroup, err)
|
||||
created := time.Now()
|
||||
dbg.CreatedAt = created
|
||||
dbg.UpdatedAt = created
|
||||
|
||||
if _, err := tx.NamedExecContext(ctx, qIns, dbg); err != nil {
|
||||
tx.Rollback()
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
case errFK:
|
||||
return errors.Wrap(auth.ErrConflict, errors.New(pqErr.Detail))
|
||||
case errDuplicate:
|
||||
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pqErr.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
|
||||
q := `DELETE FROM group_relations WHERE member_id = :member_id AND group_id = :group_id`
|
||||
dbr, err := toDBGroupRelation(userID, groupID)
|
||||
func (gr groupRepository) Unassign(ctx context.Context, groupID string, ids ...string) error {
|
||||
tx, err := gr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(groups.ErrNotFound, err)
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
|
||||
return errors.Wrap(groups.ErrGroupConflict, err)
|
||||
|
||||
qDel := `DELETE from group_relations WHERE group_id = :group_id AND member_id = :member_id`
|
||||
|
||||
for _, id := range ids {
|
||||
dbg, err := toDBGroupRelation(id, groupID, "")
|
||||
if err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExecContext(ctx, qDel, dbg); err != nil {
|
||||
tx.Rollback()
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return errors.Wrap(auth.ErrConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return errors.Wrap(auth.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbMember struct {
|
||||
ID string `db:"member_id"`
|
||||
}
|
||||
type dbGroupType struct {
|
||||
ID int `db:"id"`
|
||||
Name string `db:"name"`
|
||||
MemberID string `db:"member_id"`
|
||||
GroupID string `db:"group_id"`
|
||||
Type string `db:"type"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type dbGroup struct {
|
||||
@@ -487,7 +515,6 @@ type dbGroup struct {
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Type int `db:"type"`
|
||||
Level int `db:"level"`
|
||||
Path string `db:"path"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
@@ -501,7 +528,19 @@ type dbGroupPage struct {
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Path string `db:"path"`
|
||||
Level uint64 `db:"level"`
|
||||
Size uint64 `db:"size"`
|
||||
Total uint64 `db:"total"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
}
|
||||
|
||||
type dbMemberPage struct {
|
||||
GroupID string `db:"group_id"`
|
||||
MemberID string `db:"member_id"`
|
||||
Type string `db:"type"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func toUUID(id string) (uuid.NullUUID, error) {
|
||||
@@ -520,11 +559,10 @@ func toString(id uuid.NullUUID) (string, error) {
|
||||
if id.UUID == uuid.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return "", errConvertingStringToUUID
|
||||
return "", errStringToUUID
|
||||
}
|
||||
|
||||
func (gr groupRepository) toDBGroup(g groups.Group) (dbGroup, error) {
|
||||
|
||||
func (gr groupRepository) toDBGroup(g auth.Group) (dbGroup, error) {
|
||||
ownerID, err := toUUID(g.OwnerID)
|
||||
if err != nil {
|
||||
return dbGroup{}, err
|
||||
@@ -536,10 +574,6 @@ func (gr groupRepository) toDBGroup(g groups.Group) (dbGroup, error) {
|
||||
}
|
||||
|
||||
meta := dbMetadata(g.Metadata)
|
||||
gType, ok := gr.types[g.Type]
|
||||
if !ok {
|
||||
return dbGroup{}, errInvalidGroupType
|
||||
}
|
||||
|
||||
return dbGroup{
|
||||
ID: g.ID,
|
||||
@@ -548,46 +582,52 @@ func (gr groupRepository) toDBGroup(g groups.Group) (dbGroup, error) {
|
||||
OwnerID: ownerID,
|
||||
Description: g.Description,
|
||||
Metadata: meta,
|
||||
Type: gType.ID,
|
||||
Path: g.Path,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBGroupPage(ownerID, id, parentID, path string, level uint64, metadata groups.Metadata) (dbGroupPage, error) {
|
||||
owner, err := toUUID(ownerID)
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
func toDBGroupPage(id, path string, pm auth.PageMetadata) (dbGroupPage, error) {
|
||||
level := auth.MaxLevel
|
||||
if pm.Level < auth.MaxLevel {
|
||||
level = pm.Level
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
}
|
||||
|
||||
return dbGroupPage{
|
||||
Metadata: dbMetadata(metadata),
|
||||
Metadata: dbMetadata(pm.Metadata),
|
||||
ID: id,
|
||||
OwnerID: owner,
|
||||
Level: level,
|
||||
Path: path,
|
||||
ParentID: parentID,
|
||||
Level: level,
|
||||
Total: pm.Total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGroup(dbu dbGroup) (groups.Group, error) {
|
||||
func (gr groupRepository) toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) {
|
||||
return dbMemberPage{
|
||||
GroupID: groupID,
|
||||
MemberID: memberID,
|
||||
Type: groupType,
|
||||
Metadata: dbMetadata(pm.Metadata),
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGroup(dbu dbGroup) (auth.Group, error) {
|
||||
ownerID, err := toString(dbu.OwnerID)
|
||||
if err != nil {
|
||||
return groups.Group{}, err
|
||||
return auth.Group{}, err
|
||||
}
|
||||
|
||||
return groups.Group{
|
||||
return auth.Group{
|
||||
ID: dbu.ID,
|
||||
Name: dbu.Name,
|
||||
ParentID: dbu.ParentID.String,
|
||||
OwnerID: ownerID,
|
||||
Description: dbu.Description,
|
||||
Metadata: groups.Metadata(dbu.Metadata),
|
||||
Metadata: auth.GroupMetadata(dbu.Metadata),
|
||||
Level: dbu.Level,
|
||||
Path: dbu.Path,
|
||||
UpdatedAt: dbu.UpdatedAt,
|
||||
@@ -596,55 +636,59 @@ func toGroup(dbu dbGroup) (groups.Group, error) {
|
||||
}
|
||||
|
||||
type dbGroupRelation struct {
|
||||
GroupID uuid.UUID `db:"group_id"`
|
||||
MemberID uuid.UUID `db:"member_id"`
|
||||
GroupID sql.NullString `db:"group_id"`
|
||||
MemberID sql.NullString `db:"member_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Type string `db:"type"`
|
||||
}
|
||||
|
||||
func toDBGroupRelation(memberID, groupID string) (dbGroupRelation, error) {
|
||||
grID, err := uuid.FromString(groupID)
|
||||
if err != nil {
|
||||
return dbGroupRelation{}, err
|
||||
func toDBGroupRelation(memberID, groupID, groupType string) (dbGroupRelation, error) {
|
||||
var grID sql.NullString
|
||||
if groupID != "" {
|
||||
grID = sql.NullString{String: groupID, Valid: true}
|
||||
}
|
||||
memID, err := uuid.FromString(memberID)
|
||||
if err != nil {
|
||||
return dbGroupRelation{}, err
|
||||
|
||||
var mID sql.NullString
|
||||
if memberID != "" {
|
||||
mID = sql.NullString{String: memberID, Valid: true}
|
||||
}
|
||||
|
||||
return dbGroupRelation{
|
||||
GroupID: grID,
|
||||
MemberID: memID,
|
||||
MemberID: mID,
|
||||
Type: groupType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getGroupsMetadataQuery(db string, m groups.Metadata) ([]byte, string, error) {
|
||||
mq := ""
|
||||
mb := []byte("{}")
|
||||
func getGroupsMetadataQuery(db string, m auth.GroupMetadata) (mb []byte, mq string, err error) {
|
||||
if len(m) > 0 {
|
||||
mq = db + `.metadata @> :metadata`
|
||||
if db == "" {
|
||||
mq = `metadata @> :metadata`
|
||||
mq = `metadata @> :metadata`
|
||||
if db != "" {
|
||||
mq = db + "." + mq
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", errors.Wrap(err, errCreateMetadataQuery)
|
||||
}
|
||||
mb = b
|
||||
}
|
||||
return mb, mq, nil
|
||||
}
|
||||
|
||||
func processRows(rows *sqlx.Rows) ([]groups.Group, error) {
|
||||
var items []groups.Group
|
||||
func (gr groupRepository) processRows(rows *sqlx.Rows) ([]auth.Group, error) {
|
||||
var items []auth.Group
|
||||
for rows.Next() {
|
||||
dbgr := dbGroup{}
|
||||
if err := rows.StructScan(&dbgr); err != nil {
|
||||
return items, errors.Wrap(errSelectDb, err)
|
||||
dbg := dbGroup{}
|
||||
if err := rows.StructScan(&dbg); err != nil {
|
||||
return items, err
|
||||
}
|
||||
gr, err := toGroup(dbgr)
|
||||
group, err := toGroup(dbg)
|
||||
if err != nil {
|
||||
continue
|
||||
return items, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
items = append(items, group)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -652,13 +696,13 @@ func processRows(rows *sqlx.Rows) ([]groups.Group, error) {
|
||||
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
|
||||
rows, err := db.NamedQueryContext(ctx, query, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, errors.Wrap(errGetTotal, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
total := uint64(0)
|
||||
if rows.Next() {
|
||||
if err := rows.Scan(&total); err != nil {
|
||||
return 0, err
|
||||
return 0, errors.Wrap(errGetTotal, err)
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
|
||||
@@ -0,0 +1,777 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNameSize = 254
|
||||
maxDescSize = 1024
|
||||
groupName = "Mainflux"
|
||||
description = "description"
|
||||
)
|
||||
|
||||
var (
|
||||
invalidName = strings.Repeat("m", maxNameSize+1)
|
||||
invalidDesc = strings.Repeat("m", maxDescSize+1)
|
||||
metadata = auth.GroupMetadata{
|
||||
"admin": "true",
|
||||
}
|
||||
)
|
||||
|
||||
func generateGroupID(t *testing.T) string {
|
||||
grpID, err := ulidProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
return grpID
|
||||
}
|
||||
|
||||
func TestGroupSave(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
usrID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
wrongID, err := ulidProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
grpID := generateGroupID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "create new group",
|
||||
group: auth.Group{
|
||||
ID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create new group with existing name",
|
||||
group: auth.Group{
|
||||
ID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: auth.ErrGroupConflict,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid name",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
OwnerID: usrID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid description",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: "withParent",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent and existing name",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: grpID,
|
||||
OwnerID: usrID,
|
||||
Name: groupName,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with wrong parent",
|
||||
group: auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: wrongID,
|
||||
OwnerID: usrID,
|
||||
Name: "wrongParent",
|
||||
},
|
||||
err: auth.ErrCreateGroup,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := groupRepo.Save(context.Background(), tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGroupRetrieveByID(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "TestGroupRetrieveByID1",
|
||||
OwnerID: uid,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
retrieved, err := groupRepo.RetrieveByID(context.Background(), group1.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.True(t, retrieved.ID == group1.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group1.ID, retrieved.ID))
|
||||
|
||||
// Round to milliseconds as otherwise saving and retriving from DB
|
||||
// adds rounding error.
|
||||
creationTime := time.Now().UTC().Round(time.Millisecond)
|
||||
group2 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "TestGroupRetrieveByID",
|
||||
OwnerID: uid,
|
||||
ParentID: group1.ID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group2)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), group2.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.True(t, retrieved.ID == group2.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group2.ID, retrieved.ID))
|
||||
assert.True(t, retrieved.CreatedAt.Equal(creationTime), fmt.Sprintf("Save group, CreatedAt: expected %s got %s\n", creationTime, retrieved.CreatedAt))
|
||||
assert.True(t, retrieved.UpdatedAt.Equal(creationTime), fmt.Sprintf("Save group, UpdatedAt: expected %s got %s\n", creationTime, retrieved.UpdatedAt))
|
||||
assert.True(t, retrieved.Level == 2, fmt.Sprintf("Save group, Level: expected %d got %d\n", retrieved.Level, 2))
|
||||
assert.True(t, retrieved.ParentID == group1.ID, fmt.Sprintf("Save group, Level: expected %s got %s\n", group1.ID, retrieved.ParentID))
|
||||
assert.True(t, retrieved.Description == description, fmt.Sprintf("Save group, Description: expected %v got %v\n", retrieved.Description, description))
|
||||
assert.True(t, retrieved.Path == fmt.Sprintf("%s.%s", group1.ID, group2.ID), fmt.Sprintf("Save group, Path: expected %s got %s\n", fmt.Sprintf("%s.%s", group1.ID, group2.ID), retrieved.Path))
|
||||
|
||||
retrieved, err = groupRepo.RetrieveByID(context.Background(), generateGroupID(t))
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", auth.ErrGroupNotFound, err))
|
||||
}
|
||||
|
||||
func TestGroupUpdate(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
updateTime := time.Now().UTC()
|
||||
groupID := generateGroupID(t)
|
||||
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
Name: groupName + "TestGroupUpdate",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
retrieved, err := groupRepo.RetrieveByID(context.Background(), group.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
groupUpdate auth.Group
|
||||
groupExpected auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update group for existing id",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Name: groupName + "Updated",
|
||||
UpdatedAt: updateTime,
|
||||
Metadata: auth.GroupMetadata{"admin": "false"},
|
||||
},
|
||||
groupExpected: auth.Group{
|
||||
Name: groupName + "Updated",
|
||||
UpdatedAt: updateTime,
|
||||
Metadata: auth.GroupMetadata{"admin": "false"},
|
||||
CreatedAt: retrieved.CreatedAt,
|
||||
Path: retrieved.Path,
|
||||
ParentID: retrieved.ParentID,
|
||||
ID: retrieved.ID,
|
||||
Level: retrieved.Level,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update group for non-existing id",
|
||||
groupUpdate: auth.Group{
|
||||
ID: "wrong",
|
||||
Name: groupName + "-2",
|
||||
},
|
||||
err: auth.ErrUpdateGroup,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid name",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid description",
|
||||
groupUpdate: auth.Group{
|
||||
ID: groupID,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: auth.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
updated, err := groupRepo.Update(context.Background(), tc.groupUpdate)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if tc.desc == "update group for existing id" {
|
||||
assert.True(t, updated.Level == tc.groupExpected.Level, fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Level, updated.Level))
|
||||
assert.True(t, updated.Name == tc.groupExpected.Name, fmt.Sprintf("%s:Name: expected %s got %s\n", tc.desc, tc.groupExpected.Name, updated.Name))
|
||||
assert.True(t, updated.Metadata["admin"] == tc.groupExpected.Metadata["admin"], fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Metadata["admin"], updated.Metadata["admin"]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupDelete(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
groupParent := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
groupParent, err = groupRepo.Save(context.Background(), groupParent)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
creationTime = time.Now().UTC()
|
||||
groupChild1 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: groupParent.ID,
|
||||
Name: groupName + "child1",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
creationTime = time.Now().UTC()
|
||||
groupChild2 := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
ParentID: groupParent.ID,
|
||||
Name: groupName + "child2",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
meta := auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
|
||||
groupChild1, err = groupRepo.Save(context.Background(), groupChild1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
groupChild2, err = groupRepo.Save(context.Background(), groupChild2)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
gp, err := groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Retrieve children for parent: expected %v got %v\n", nil, err))
|
||||
assert.True(t, gp.Total == 3, fmt.Sprintf("Number of children + parent: expected %d got %d\n", 3, gp.Total))
|
||||
|
||||
thingID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("thing id create unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), groupChild1.ID, "things", thingID)
|
||||
require.Nil(t, err, fmt.Sprintf("thing assign got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete non empty group: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupChild2.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete empty group: expected %v got %v\n", nil, err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupParent.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete parent with children with members: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
|
||||
|
||||
gp, err = groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("retrieve children after one child removed: expected %v got %v\n", nil, err))
|
||||
assert.True(t, gp.Total == 2, fmt.Sprintf("number of children + parent: expected %d got %d\n", 2, gp.Total))
|
||||
|
||||
err = groupRepo.Unassign(context.Background(), groupChild1.ID, thingID)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to remove thing from a group error: %s", err))
|
||||
|
||||
err = groupRepo.Delete(context.Background(), groupParent.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete parent with children with no members: expected %v got %v\n", nil, err))
|
||||
|
||||
_, err = groupRepo.RetrieveByID(context.Background(), groupChild1.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.PageMetadata{
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
wrongMeta := auth.PageMetadata{
|
||||
Metadata: auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
},
|
||||
Level: auth.MaxLevel,
|
||||
}
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(auth.MaxLevel)
|
||||
parentID := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if i < metaNum {
|
||||
group.Metadata = metadata.Metadata
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
Size uint64
|
||||
Metadata auth.PageMetadata
|
||||
}{
|
||||
"retrieve all groups": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: n,
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
Size: n,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: metaNum,
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: metaNum,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(0),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: wrongMeta.Metadata,
|
||||
},
|
||||
Size: uint64(0),
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(metaNum),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: uint64(metaNum),
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
Metadata: auth.PageMetadata{
|
||||
Total: uint64(metaNum),
|
||||
Limit: n,
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata.Metadata,
|
||||
},
|
||||
Size: uint64(metaNum),
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAll(context.Background(), tc.Metadata)
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
|
||||
assert.Equal(t, tc.Metadata.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Metadata.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllParents(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.GroupMetadata{
|
||||
"field": "value",
|
||||
}
|
||||
wrongMeta := auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
}
|
||||
|
||||
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.Equal(t, uint64(0), p.Total, fmt.Sprintf("expected total %d got %d\n", 0, p.Total))
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(10)
|
||||
parentID := ""
|
||||
parentMiddle := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if n-i <= metaNum {
|
||||
group.Metadata = metadata
|
||||
}
|
||||
if i == n/2 {
|
||||
parentMiddle = group.ID
|
||||
}
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
level uint64
|
||||
parentID string
|
||||
Size uint64
|
||||
Total uint64
|
||||
Metadata auth.GroupMetadata
|
||||
}{
|
||||
"retrieve all parents": {
|
||||
Total: n,
|
||||
Size: auth.MaxLevel + 1,
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
Total: metaNum,
|
||||
Size: metaNum,
|
||||
Metadata: metadata,
|
||||
parentID: parentID,
|
||||
level: auth.MaxLevel,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
Total: uint64(0),
|
||||
Metadata: wrongMeta,
|
||||
Size: uint64(0),
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
Total: n,
|
||||
Size: 2 + 1,
|
||||
level: uint64(2),
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
Total: metaNum,
|
||||
Size: metaNum,
|
||||
level: 3,
|
||||
Metadata: metadata,
|
||||
parentID: parentID,
|
||||
},
|
||||
"retrieve parent groups from children in the middle": {
|
||||
Total: n/2 + 1,
|
||||
Size: n/2 + 1,
|
||||
level: auth.MaxLevel,
|
||||
parentID: parentMiddle,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAllParents(context.Background(), tc.parentID, auth.PageMetadata{Level: tc.level, Metadata: tc.Metadata})
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
|
||||
assert.Equal(t, tc.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllChildren(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
metadata := auth.GroupMetadata{
|
||||
"field": "value",
|
||||
}
|
||||
wrongMeta := auth.GroupMetadata{
|
||||
"wrong": "wrong",
|
||||
}
|
||||
|
||||
metaNum := uint64(3)
|
||||
|
||||
n := uint64(10)
|
||||
groupID := generateGroupID(t)
|
||||
firstParentID := groupID
|
||||
parentID := ""
|
||||
parentMiddle := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: groupID,
|
||||
Name: fmt.Sprintf("%s-%d", groupName, i),
|
||||
OwnerID: uid,
|
||||
ParentID: parentID,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
// Create Groups with metadata.
|
||||
if i < metaNum {
|
||||
group.Metadata = metadata
|
||||
}
|
||||
if i == n/2 {
|
||||
parentMiddle = group.ID
|
||||
}
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = group.ID
|
||||
groupID = generateGroupID(t)
|
||||
}
|
||||
|
||||
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
assert.Equal(t, n, p.Total, fmt.Sprintf("expected total %d got %d\n", n, p.Total))
|
||||
|
||||
cases := map[string]struct {
|
||||
parentID string
|
||||
size uint64
|
||||
total uint64
|
||||
metadata auth.PageMetadata
|
||||
}{
|
||||
"retrieve all children": {
|
||||
size: auth.MaxLevel,
|
||||
total: n,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with existing metadata": {
|
||||
size: metaNum,
|
||||
total: metaNum,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: metadata,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with non-existing metadata": {
|
||||
total: 0,
|
||||
size: 0,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
Metadata: wrongMeta,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth": {
|
||||
total: n,
|
||||
size: 2,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: 2,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve groups with hierarchy level depth and existing metadata": {
|
||||
total: metaNum,
|
||||
size: metaNum,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: 3,
|
||||
Metadata: metadata,
|
||||
},
|
||||
parentID: firstParentID,
|
||||
},
|
||||
"retrieve parent groups from children in the middle": {
|
||||
total: n / 2,
|
||||
size: n / 2,
|
||||
metadata: auth.PageMetadata{
|
||||
Level: auth.MaxLevel,
|
||||
},
|
||||
parentID: parentMiddle,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := groupRepo.RetrieveAllChildren(context.Background(), tc.parentID, tc.metadata)
|
||||
size := len(page.Groups)
|
||||
assert.Equal(t, tc.size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssign(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
group, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
|
||||
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrMemberAlreadyAssigned), fmt.Sprintf("assign member again: expected %v got %v\n", auth.ErrMemberAlreadyAssigned, err))
|
||||
}
|
||||
|
||||
func TestUnassign(t *testing.T) {
|
||||
t.Cleanup(func() { cleanUp(t) })
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
ID: generateGroupID(t),
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
pm := auth.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
group, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
|
||||
|
||||
mid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
|
||||
|
||||
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 2, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 2, mp.Total))
|
||||
|
||||
err = groupRepo.Unassign(context.Background(), group.ID, mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
|
||||
|
||||
mp, err = groupRepo.Members(context.Background(), group.ID, "things", pm)
|
||||
require.Nil(t, err, fmt.Sprintf("members retrieve unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
}
|
||||
|
||||
func cleanUp(t *testing.T) {
|
||||
_, err := db.Exec("delete from group_relations")
|
||||
require.Nil(t, err, fmt.Sprintf("clean relations unexpected error: %s", err))
|
||||
_, err = db.Exec("delete from groups")
|
||||
require.Nil(t, err, fmt.Sprintf("clean groups unexpected error: %s", err))
|
||||
}
|
||||
+17
-23
@@ -54,12 +54,7 @@ func migrateDB(db *sqlx.DB) error {
|
||||
expires_at TIMESTAMP,
|
||||
PRIMARY KEY (id, issuer_id)
|
||||
)`,
|
||||
`CREATE extension LTREE`,
|
||||
`CREATE TABLE IF NOT EXISTS group_type (
|
||||
id INTEGER UNIQUE NOT NULL,
|
||||
name VARCHAR(254) UNIQUE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)`,
|
||||
`CREATE EXTENSION IF NOT EXISTS LTREE`,
|
||||
`CREATE TABLE IF NOT EXISTS groups (
|
||||
id VARCHAR(254) UNIQUE NOT NULL,
|
||||
parent_id VARCHAR(254),
|
||||
@@ -67,26 +62,23 @@ func migrateDB(db *sqlx.DB) error {
|
||||
name VARCHAR(254) NOT NULL,
|
||||
description VARCHAR(1024),
|
||||
metadata JSONB,
|
||||
path LTREE,
|
||||
type INTEGER NOT NULL,
|
||||
path LTREE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (owner_id, path),
|
||||
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (type) REFERENCES group_type (id)
|
||||
UNIQUE (owner_id, name, parent_id),
|
||||
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS group_relations (
|
||||
member_id VARCHAR(254) NOT NULL,
|
||||
group_id VARCHAR(254) NOT NULL,
|
||||
member_id VARCHAR(254) NOT NULL,
|
||||
group_id VARCHAR(254) NOT NULL,
|
||||
type VARCHAR(254),
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES groups (id),
|
||||
PRIMARY KEY (member_id, group_id)
|
||||
)`,
|
||||
`CREATE INDEX path_gist_idx ON groups USING GIST (path);`,
|
||||
`INSERT INTO group_type (id, name) VALUES (1, 'things')`,
|
||||
`INSERT INTO group_type (id, name) VALUES (2, 'users')`,
|
||||
`CREATE OR REPLACE FUNCTION inherit_type()
|
||||
`CREATE OR REPLACE FUNCTION inherit_group()
|
||||
RETURNS trigger
|
||||
LANGUAGE PLPGSQL
|
||||
AS
|
||||
@@ -95,24 +87,26 @@ func migrateDB(db *sqlx.DB) error {
|
||||
IF NEW.parent_id IS NULL OR NEW.parent_id = '' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
SELECT type INTO NEW.type FROM groups WHERE id = NEW.parent_id;
|
||||
IF NOT EXISTS (SELECT id FROM groups WHERE id = NEW.parent_id) THEN
|
||||
RAISE EXCEPTION 'wrong parent id';
|
||||
END IF;
|
||||
SELECT text2ltree(ltree2text(path) || '.' || NEW.id) INTO NEW.path FROM groups WHERE id = NEW.parent_id;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$`,
|
||||
`CREATE TRIGGER inherit_type_tr
|
||||
`CREATE TRIGGER inherit_group_tr
|
||||
BEFORE INSERT
|
||||
ON groups
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE inherit_type();`,
|
||||
EXECUTE PROCEDURE inherit_group();`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS keys`,
|
||||
`DROP EXTENSION IF EXISTS LTREE`,
|
||||
`DROP TABLE IF EXISTS groups`,
|
||||
`DROP TABLE IF EXISTS group_type`,
|
||||
`DROP TABLE IF EXISTS group_relations`,
|
||||
`DROP FUNCTION IF EXISTS inherit_type`,
|
||||
`DROP TRIGGER IF EXISTS inherit_type_tr ON groups`,
|
||||
`DROP FUNCTION IF EXISTS inherit_group`,
|
||||
`DROP TRIGGER IF EXISTS inherit_group_tr ON groups`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/auth/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/ulid"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -21,8 +22,9 @@ import (
|
||||
const email = "user-save@example.com"
|
||||
|
||||
var (
|
||||
expTime = time.Now().Add(5 * time.Minute)
|
||||
idProvider = uuid.New()
|
||||
expTime = time.Now().Add(5 * time.Minute)
|
||||
idProvider = uuid.New()
|
||||
ulidProvider = ulid.New()
|
||||
)
|
||||
|
||||
func TestKeySave(t *testing.T) {
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -23,6 +23,7 @@ type Database interface {
|
||||
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
|
||||
QueryxContext(context.Context, string, ...interface{}) (*sqlx.Rows, error)
|
||||
NamedQueryContext(context.Context, string, interface{}) (*sqlx.Rows, error)
|
||||
BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error)
|
||||
}
|
||||
|
||||
// NewDatabase creates a ThingDatabase instance
|
||||
@@ -52,6 +53,16 @@ func (d database) QueryxContext(ctx context.Context, query string, args ...inter
|
||||
return d.db.QueryxContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (d database) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*sqlx.Tx, error) {
|
||||
span := opentracing.SpanFromContext(ctx)
|
||||
if span != nil {
|
||||
span.SetTag("span.kind", "client")
|
||||
span.SetTag("peer.service", "postgres")
|
||||
span.SetTag("db.type", "sql")
|
||||
}
|
||||
return d.db.BeginTxx(ctx, opts)
|
||||
}
|
||||
|
||||
func addSpanTags(ctx context.Context, query string) {
|
||||
span := opentracing.SpanFromContext(ctx)
|
||||
if span != nil {
|
||||
|
||||
+79
-67
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/ulid"
|
||||
)
|
||||
@@ -38,6 +37,18 @@ var (
|
||||
// ErrFailedToRetrieveMembers failed to retrieve group members.
|
||||
ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members")
|
||||
|
||||
// ErrFailedToRetrieveMembership failed to retrieve memberships
|
||||
ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships")
|
||||
|
||||
// ErrFailedToRetrieveAll failed to retrieve groups.
|
||||
ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups")
|
||||
|
||||
// ErrFailedToRetrieveParents failed to retrieve groups.
|
||||
ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups")
|
||||
|
||||
// ErrFailedToRetrieveChildren failed to retrieve groups.
|
||||
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new user key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
@@ -83,21 +94,21 @@ type Service interface {
|
||||
Authz
|
||||
|
||||
// Implements groups API, creating groups, assigning members
|
||||
groups.Service
|
||||
GroupService
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
groups groups.Repository
|
||||
groups GroupRepository
|
||||
idProvider mainflux.IDProvider
|
||||
ulidProvider mainflux.IDProvider
|
||||
tokenizer Tokenizer
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, groups groups.Repository, idp mainflux.IDProvider, tokenizer Tokenizer) Service {
|
||||
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
@@ -215,65 +226,61 @@ func (svc service) login(token string) (string, string, error) {
|
||||
return key.IssuerID, key.Subject, nil
|
||||
}
|
||||
|
||||
func (svc service) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
|
||||
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
user, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
ulid, err := svc.ulidProvider.ID()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrGenerateGroupID, err)
|
||||
return Group{}, errors.Wrap(ErrGenerateGroupID, err)
|
||||
}
|
||||
|
||||
g.ID = ulid
|
||||
g.OwnerID = user.ID
|
||||
if _, err := svc.groups.Save(ctx, g); err != nil {
|
||||
return "", err
|
||||
}
|
||||
timestamp := getTimestmap()
|
||||
group.UpdatedAt = timestamp
|
||||
group.CreatedAt = timestamp
|
||||
|
||||
return g.ID, nil
|
||||
}
|
||||
group.ID = ulid
|
||||
group.OwnerID = user.ID
|
||||
|
||||
func (svc service) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAll(ctx, level, gm)
|
||||
|
||||
}
|
||||
|
||||
func (svc service) ListParents(ctx context.Context, token string, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAllParents(ctx, childID, level, gm)
|
||||
}
|
||||
|
||||
func (svc service) ListChildren(ctx context.Context, token string, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAllChildren(ctx, parentID, level, gm)
|
||||
}
|
||||
|
||||
func (svc service) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
p, err := svc.groups.Members(ctx, groupID, offset, limit, gm)
|
||||
group, err = svc.groups.Save(ctx, group)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(ErrFailedToRetrieveMembers, err)
|
||||
return Group{}, err
|
||||
}
|
||||
mp := groups.MemberPage{
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: p.Total,
|
||||
Offset: p.Offset,
|
||||
Limit: p.Limit,
|
||||
},
|
||||
Members: make([]groups.Member, 0),
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (svc service) ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAll(ctx, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListParents(ctx context.Context, token string, childID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAllParents(ctx, childID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListChildren(ctx context.Context, token string, parentID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAllChildren(ctx, parentID, pm)
|
||||
}
|
||||
|
||||
func (svc service) ListMembers(ctx context.Context, token string, groupID, groupType string, pm PageMetadata) (MemberPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return MemberPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
|
||||
if err != nil {
|
||||
return MemberPage{}, errors.Wrap(ErrFailedToRetrieveMembers, err)
|
||||
}
|
||||
mp.Members = append(mp.Members, p.Members)
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
@@ -284,38 +291,43 @@ func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
return svc.groups.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) Unassign(ctx context.Context, token, memberID, groupID string) error {
|
||||
func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.Unassign(ctx, memberID, groupID)
|
||||
}
|
||||
|
||||
func (svc service) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return svc.groups.Update(ctx, g)
|
||||
group.UpdatedAt = getTimestmap()
|
||||
return svc.groups.Update(ctx, group)
|
||||
}
|
||||
|
||||
func (svc service) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
|
||||
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (svc service) Assign(ctx context.Context, token, memberID, groupID string) error {
|
||||
func (svc service) Assign(ctx context.Context, token string, groupID, groupType string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.Assign(ctx, memberID, groupID)
|
||||
return svc.groups.Assign(ctx, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (svc service) ListMemberships(ctx context.Context, token string, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
func (svc service) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) error {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.Memberships(ctx, memberID, offset, limit, gm)
|
||||
return svc.groups.Unassign(ctx, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
func (svc service) ListMemberships(ctx context.Context, token string, memberID string, pm PageMetadata) (GroupPage, error) {
|
||||
if _, err := svc.Identify(ctx, token); err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.Memberships(ctx, memberID, pm)
|
||||
}
|
||||
|
||||
func getTimestmap() time.Time {
|
||||
return time.Now().UTC().Round(time.Millisecond)
|
||||
}
|
||||
|
||||
+695
-3
@@ -15,12 +15,17 @@ import (
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var idProvider = uuid.New()
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
groupName = "mfx"
|
||||
description = "Description"
|
||||
)
|
||||
|
||||
func newService() auth.Service {
|
||||
@@ -289,3 +294,690 @@ func TestIdentify(t *testing.T) {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.idt, idt))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Name: "Group",
|
||||
Description: description,
|
||||
}
|
||||
|
||||
parentGroup := auth.Group{
|
||||
Name: "ParentGroup",
|
||||
Description: description,
|
||||
}
|
||||
|
||||
parent, err := svc.CreateGroup(context.Background(), apiToken, parentGroup)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating parent group expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "create new group",
|
||||
group: group,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with existing name",
|
||||
group: group,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with parent",
|
||||
group: auth.Group{
|
||||
Name: groupName,
|
||||
ParentID: parent.ID,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with invalid parent",
|
||||
group: auth.Group{
|
||||
Name: groupName,
|
||||
ParentID: "xxxxxxxxxx",
|
||||
},
|
||||
err: auth.ErrCreateGroup,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.CreateGroup(context.Background(), apiToken, tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Name: "Group",
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), apiToken, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating parent group failed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group auth.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update group",
|
||||
group: auth.Group{
|
||||
ID: group.ID,
|
||||
Name: "NewName",
|
||||
Description: "NewDescription",
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value2",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
g, err := svc.UpdateGroup(context.Background(), apiToken, tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, g.ID, tc.group.ID, fmt.Sprintf("ID: expected %s got %s\n", g.ID, tc.group.ID))
|
||||
assert.Equal(t, g.Name, tc.group.Name, fmt.Sprintf("Name: expected %s got %s\n", g.Name, tc.group.Name))
|
||||
assert.Equal(t, g.Description, tc.group.Description, fmt.Sprintf("Description: expected %s got %s\n", g.Description, tc.group.Description))
|
||||
assert.Equal(t, g.Metadata["field"], g.Metadata["field"], fmt.Sprintf("Metadata: expected %s got %s\n", g.Metadata, tc.group.Metadata))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestViewGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Name: "Group",
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), apiToken, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating parent group failed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
groupID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
|
||||
desc: "view group",
|
||||
token: apiToken,
|
||||
groupID: group.ID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "view group with unauthorized token",
|
||||
token: "wrongtoken",
|
||||
groupID: group.ID,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "view group for wrong id",
|
||||
token: apiToken,
|
||||
groupID: "wrong",
|
||||
err: auth.ErrGroupNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.ViewGroup(context.Background(), tc.token, tc.groupID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListGroups(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
n := uint64(10)
|
||||
parentID := ""
|
||||
for i := uint64(0); i < n; i++ {
|
||||
group.Name = fmt.Sprintf("Group%d", i)
|
||||
group.ParentID = parentID
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = g.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
level uint64
|
||||
size uint64
|
||||
metadata auth.GroupMetadata
|
||||
err error
|
||||
}{
|
||||
"list all groups": {
|
||||
token: apiToken,
|
||||
level: 5,
|
||||
size: n,
|
||||
err: nil,
|
||||
},
|
||||
"list groups for level 1": {
|
||||
token: apiToken,
|
||||
level: 1,
|
||||
size: n,
|
||||
err: nil,
|
||||
},
|
||||
"list all groups with wrong token": {
|
||||
token: "wrongToken",
|
||||
level: 5,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := svc.ListGroups(context.Background(), tc.token, auth.PageMetadata{Level: tc.level, Metadata: tc.metadata})
|
||||
size := uint64(len(page.Groups))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestListChildren(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
n := uint64(10)
|
||||
parentID := ""
|
||||
groupIDs := make([]string, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
group.Name = fmt.Sprintf("Group%d", i)
|
||||
group.ParentID = parentID
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = g.ID
|
||||
groupIDs[i] = g.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
level uint64
|
||||
size uint64
|
||||
id string
|
||||
metadata auth.GroupMetadata
|
||||
err error
|
||||
}{
|
||||
"list all children": {
|
||||
token: apiToken,
|
||||
level: 5,
|
||||
id: groupIDs[0],
|
||||
size: n,
|
||||
err: nil,
|
||||
},
|
||||
"list all groups with wrong token": {
|
||||
token: "wrongToken",
|
||||
level: 5,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := svc.ListChildren(context.Background(), tc.token, tc.id, auth.PageMetadata{Level: tc.level, Metadata: tc.metadata})
|
||||
size := uint64(len(page.Groups))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListParents(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
n := uint64(10)
|
||||
parentID := ""
|
||||
groupIDs := make([]string, n)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
group.Name = fmt.Sprintf("Group%d", i)
|
||||
group.ParentID = parentID
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
parentID = g.ID
|
||||
groupIDs[i] = g.ID
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
level uint64
|
||||
size uint64
|
||||
id string
|
||||
metadata auth.GroupMetadata
|
||||
err error
|
||||
}{
|
||||
"list all parents": {
|
||||
token: apiToken,
|
||||
level: 5,
|
||||
id: groupIDs[n-1],
|
||||
size: n,
|
||||
err: nil,
|
||||
},
|
||||
"list all parents with wrong token": {
|
||||
token: "wrongToken",
|
||||
level: 5,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := svc.ListParents(context.Background(), tc.token, tc.id, auth.PageMetadata{Level: tc.level, Metadata: tc.metadata})
|
||||
size := uint64(len(page.Groups))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMembers(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("Creating group expected to succeed: %s", err))
|
||||
group.ID = g.ID
|
||||
|
||||
n := uint64(10)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
|
||||
err = svc.Assign(context.Background(), apiToken, group.ID, "things", uid)
|
||||
require.Nil(t, err, fmt.Sprintf("Assign member expected to succeed: %s\n", err))
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
size uint64
|
||||
offset uint64
|
||||
limit uint64
|
||||
group auth.Group
|
||||
metadata auth.GroupMetadata
|
||||
err error
|
||||
}{
|
||||
"list all members": {
|
||||
token: apiToken,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
group: group,
|
||||
size: n,
|
||||
err: nil,
|
||||
},
|
||||
"list half members": {
|
||||
token: apiToken,
|
||||
offset: n / 2,
|
||||
limit: n,
|
||||
group: group,
|
||||
size: n / 2,
|
||||
err: nil,
|
||||
},
|
||||
"list all members with wrong token": {
|
||||
token: "wrongToken",
|
||||
offset: 0,
|
||||
limit: n,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := svc.ListMembers(context.Background(), tc.token, tc.group.ID, "things", auth.PageMetadata{Offset: tc.offset, Limit: tc.limit, Metadata: tc.metadata})
|
||||
size := uint64(len(page.Members))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestListMemberships(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
group := auth.Group{
|
||||
Description: description,
|
||||
Metadata: auth.GroupMetadata{
|
||||
"field": "value",
|
||||
},
|
||||
}
|
||||
|
||||
memberID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
|
||||
n := uint64(10)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
group.Name = fmt.Sprintf("Group%d", i)
|
||||
g, err := svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
|
||||
err = svc.Assign(context.Background(), apiToken, g.ID, "things", memberID)
|
||||
require.Nil(t, err, fmt.Sprintf("Assign member expected to succeed: %s\n", err))
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
size uint64
|
||||
offset uint64
|
||||
limit uint64
|
||||
group auth.Group
|
||||
metadata auth.GroupMetadata
|
||||
err error
|
||||
}{
|
||||
"list all members": {
|
||||
token: apiToken,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
group: group,
|
||||
size: n,
|
||||
err: nil,
|
||||
},
|
||||
"list half members": {
|
||||
token: apiToken,
|
||||
offset: n / 2,
|
||||
limit: n,
|
||||
group: group,
|
||||
size: n / 2,
|
||||
err: nil,
|
||||
},
|
||||
"list all members with wrong token": {
|
||||
token: "wrongToken",
|
||||
offset: 0,
|
||||
limit: n,
|
||||
size: 0,
|
||||
err: auth.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := svc.ListMemberships(context.Background(), tc.token, memberID, auth.PageMetadata{Limit: tc.limit, Offset: tc.offset, Metadata: tc.metadata})
|
||||
size := uint64(len(page.Groups))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
Name: groupName,
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
err = svc.RemoveGroup(context.Background(), "wrongToken", group.ID)
|
||||
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
|
||||
|
||||
err = svc.RemoveGroup(context.Background(), apiToken, "wrongID")
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Remove group with wrong id: expected %v got %v", auth.ErrGroupNotFound, err))
|
||||
|
||||
gp, err := svc.ListGroups(context.Background(), apiToken, auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("list groups unexpected error: %s", err))
|
||||
assert.True(t, gp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, gp.Total))
|
||||
|
||||
err = svc.RemoveGroup(context.Background(), apiToken, group.ID)
|
||||
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
|
||||
|
||||
gp, err = svc.ListGroups(context.Background(), apiToken, auth.PageMetadata{Level: auth.MaxLevel})
|
||||
require.Nil(t, err, fmt.Sprintf("list groups save unexpected error: %s", err))
|
||||
assert.True(t, gp.Total == 0, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 0, gp.Total))
|
||||
|
||||
}
|
||||
|
||||
func TestAssign(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), apiToken, group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
|
||||
mp, err := svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Offset: 0, Limit: 10})
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
|
||||
err = svc.Assign(context.Background(), "wrongToken", group.ID, "things", mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
|
||||
|
||||
}
|
||||
|
||||
func TestUnassign(t *testing.T) {
|
||||
svc := newService()
|
||||
_, secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.UserKey, IssuedAt: time.Now(), IssuerID: id, Subject: email})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
key := auth.Key{
|
||||
ID: "id",
|
||||
Type: auth.APIKey,
|
||||
IssuerID: id,
|
||||
Subject: email,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, apiToken, err := svc.Issue(context.Background(), secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
creationTime := time.Now().UTC()
|
||||
group := auth.Group{
|
||||
Name: groupName + "Updated",
|
||||
OwnerID: uid,
|
||||
CreatedAt: creationTime,
|
||||
UpdatedAt: creationTime,
|
||||
}
|
||||
|
||||
group, err = svc.CreateGroup(context.Background(), apiToken, group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
mid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
err = svc.Assign(context.Background(), apiToken, group.ID, "things", mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
|
||||
mp, err := svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Limit: 10, Offset: 0})
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
|
||||
|
||||
err = svc.Unassign(context.Background(), apiToken, group.ID, mid)
|
||||
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
|
||||
|
||||
mp, err = svc.ListMembers(context.Background(), apiToken, group.ID, "things", auth.PageMetadata{Limit: 10, Offset: 0})
|
||||
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
|
||||
assert.True(t, mp.Total == 0, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 0, mp.Total))
|
||||
|
||||
err = svc.Unassign(context.Background(), "wrongToken", group.ID, mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrUnauthorizedAccess), fmt.Sprintf("Unauthorized access: expected %v got %v", auth.ErrUnauthorizedAccess, err))
|
||||
|
||||
err = svc.Unassign(context.Background(), apiToken, group.ID, mid)
|
||||
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Unauthorized access: expected %v got %v", nil, err))
|
||||
}
|
||||
|
||||
+39
-40
@@ -7,41 +7,40 @@ package tracing
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
assign = "assign"
|
||||
saveGroup = "save_group"
|
||||
deleteGroup = "delete_group"
|
||||
updateGroup = "update_group"
|
||||
retrieveByID = "retrieve_by_id"
|
||||
retrieveAllAncestors = "retrieve_all_ancestors"
|
||||
retrieveAllChildren = "retrieve_all_children"
|
||||
retrieveAll = "retrieve_all_groups"
|
||||
retrieveByName = "retrieve_by_name"
|
||||
memberships = "memberships"
|
||||
members = "members"
|
||||
unassign = "unassign"
|
||||
assign = "assign"
|
||||
saveGroup = "save_group"
|
||||
deleteGroup = "delete_group"
|
||||
updateGroup = "update_group"
|
||||
retrieveByID = "retrieve_by_id"
|
||||
retrieveAllParents = "retrieve_all_parents"
|
||||
retrieveAllChildren = "retrieve_all_children"
|
||||
retrieveAll = "retrieve_all_groups"
|
||||
memberships = "memberships"
|
||||
members = "members"
|
||||
unassign = "unassign"
|
||||
)
|
||||
|
||||
var _ groups.Repository = (*groupRepositoryMiddleware)(nil)
|
||||
var _ auth.GroupRepository = (*groupRepositoryMiddleware)(nil)
|
||||
|
||||
type groupRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo groups.Repository
|
||||
repo auth.GroupRepository
|
||||
}
|
||||
|
||||
// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
|
||||
func GroupRepositoryMiddleware(tracer opentracing.Tracer, gr groups.Repository) groups.Repository {
|
||||
func GroupRepositoryMiddleware(tracer opentracing.Tracer, gr auth.GroupRepository) auth.GroupRepository {
|
||||
return groupRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: gr,
|
||||
}
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
func (grm groupRepositoryMiddleware) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, saveGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
@@ -49,7 +48,7 @@ func (grm groupRepositoryMiddleware) Save(ctx context.Context, g groups.Group) (
|
||||
return grm.repo.Save(ctx, g)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
func (grm groupRepositoryMiddleware) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, updateGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
@@ -65,7 +64,7 @@ func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string)
|
||||
return grm.repo.Delete(ctx, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
|
||||
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveByID)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
@@ -73,58 +72,58 @@ func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string
|
||||
return grm.repo.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllAncestors)
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllParents)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllParents(ctx, groupID, level, gm)
|
||||
return grm.repo.RetrieveAllParents(ctx, groupID, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllChildren)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllChildren(ctx, groupID, level, gm)
|
||||
return grm.repo.RetrieveAllChildren(ctx, groupID, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAll)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAll(ctx, level, gm)
|
||||
return grm.repo.RetrieveAll(ctx, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, memberships)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Memberships(ctx, memberID, offset, limit, gm)
|
||||
return grm.repo.Memberships(ctx, memberID, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Members(ctx context.Context, memberID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
func (grm groupRepositoryMiddleware) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, members)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Members(ctx, memberID, offset, limit, gm)
|
||||
return grm.repo.Members(ctx, groupID, groupType, pm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, memberID, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, unassign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Unassign(ctx, memberID, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, memberID, groupID string) error {
|
||||
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error {
|
||||
span := createSpan(ctx, grm.tracer, assign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Assign(ctx, memberID, groupID)
|
||||
return grm.repo.Assign(ctx, groupID, groupType, memberIDs...)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, groupID string, memberIDs ...string) error {
|
||||
span := createSpan(ctx, grm.tracer, unassign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Unassign(ctx, groupID, memberIDs...)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
@@ -236,46 +235,6 @@ func findIndex(list []string, val string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListMemberships(ctx context.Context, token, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Assign(ctx context.Context, token, memberID, groupID string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Unassign(ctx context.Context, token, memberID, groupID string) error {
|
||||
func (svc *mainfluxThings) ListMembers(ctx context.Context, token, groupID string, pm things.PageMetadata) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
+1
-4
@@ -310,9 +310,6 @@ func newService(auth mainflux.AuthServiceClient, dbTracer opentracing.Tracer, ca
|
||||
channelsRepo := postgres.NewChannelRepository(database)
|
||||
channelsRepo = tracing.ChannelRepositoryMiddleware(dbTracer, channelsRepo)
|
||||
|
||||
groupsRepo := postgres.NewGroupRepo(database)
|
||||
groupsRepo = tracing.GroupRepositoryMiddleware(dbTracer, groupsRepo)
|
||||
|
||||
chanCache := rediscache.NewChannelCache(cacheClient)
|
||||
chanCache = tracing.ChannelCacheMiddleware(cacheTracer, chanCache)
|
||||
|
||||
@@ -320,7 +317,7 @@ func newService(auth mainflux.AuthServiceClient, dbTracer opentracing.Tracer, ca
|
||||
thingCache = tracing.ThingCacheMiddleware(cacheTracer, thingCache)
|
||||
idProvider := uuid.New()
|
||||
|
||||
svc := things.New(auth, thingsRepo, channelsRepo, groupsRepo, chanCache, thingCache, idProvider)
|
||||
svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
svc = rediscache.NewEventStoreMiddleware(svc, esClient)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
|
||||
+3
-4
@@ -288,7 +288,6 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthServic
|
||||
database := postgres.NewDatabase(db)
|
||||
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 {
|
||||
@@ -297,7 +296,7 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthServic
|
||||
|
||||
idProvider := uuid.New()
|
||||
|
||||
svc := users.New(userRepo, groupRepo, hasher, auth, emailer, idProvider, c.passRegex)
|
||||
svc := users.New(userRepo, hasher, auth, emailer, idProvider, c.passRegex)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -314,14 +313,14 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthServic
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
if err := createAdmin(svc, userRepo, groupRepo, c); err != nil {
|
||||
if err := createAdmin(svc, userRepo, 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 {
|
||||
func createAdmin(svc users.Service, userRepo users.UserRepository, c config) error {
|
||||
user := users.User{
|
||||
Email: c.adminEmail,
|
||||
Password: c.adminPassword,
|
||||
|
||||
@@ -49,13 +49,26 @@ http {
|
||||
add_header Access-Control-Allow-Headers '*';
|
||||
|
||||
server_name localhost;
|
||||
# Proxy pass to users service
|
||||
location /groups/users/ {
|
||||
include snippets/proxy-headers.conf;
|
||||
proxy_pass http://users:${MF_USERS_HTTP_PORT}/groups/;
|
||||
}
|
||||
|
||||
# Proxy pass to users service
|
||||
location ~ ^/(users|tokens|password|groups) {
|
||||
location ~ ^/(users|tokens|password) {
|
||||
include snippets/proxy-headers.conf;
|
||||
proxy_pass http://users:${MF_USERS_HTTP_PORT};
|
||||
}
|
||||
|
||||
|
||||
# Proxy pass to things service
|
||||
location /groups/things/ {
|
||||
include snippets/proxy-headers.conf;
|
||||
add_header Access-Control-Expose-Headers Location;
|
||||
proxy_pass http://things:${MF_THINGS_HTTP_PORT}/groups/;
|
||||
}
|
||||
|
||||
# Proxy pass to things service
|
||||
location ~ ^/(things|channels|connect) {
|
||||
include snippets/proxy-headers.conf;
|
||||
@@ -63,6 +76,13 @@ http {
|
||||
proxy_pass http://things:${MF_THINGS_HTTP_PORT};
|
||||
}
|
||||
|
||||
location ~ ^/(groups|members|keys) {
|
||||
include snippets/proxy-headers.conf;
|
||||
add_header Access-Control-Expose-Headers Location;
|
||||
proxy_pass http://auth:${MF_AUTH_HTTP_PORT};
|
||||
}
|
||||
|
||||
|
||||
location /version {
|
||||
include snippets/proxy-headers.conf;
|
||||
proxy_pass http://things:${MF_THINGS_HTTP_PORT};
|
||||
|
||||
@@ -56,12 +56,26 @@ http {
|
||||
|
||||
server_name localhost;
|
||||
|
||||
# Proxy pass to users service
|
||||
location /groups/users/ {
|
||||
include snippets/proxy-headers.conf;
|
||||
proxy_pass http://users:${MF_USERS_HTTP_PORT}/groups/;
|
||||
}
|
||||
|
||||
# Proxy pass to users service
|
||||
location ~ ^/(users|tokens|password|groups) {
|
||||
location ~ ^/(users|tokens|password) {
|
||||
include snippets/proxy-headers.conf;
|
||||
proxy_pass http://users:${MF_USERS_HTTP_PORT};
|
||||
}
|
||||
|
||||
|
||||
# Proxy pass to things service
|
||||
location /groups/things/ {
|
||||
include snippets/proxy-headers.conf;
|
||||
add_header Access-Control-Expose-Headers Location;
|
||||
proxy_pass http://things:${MF_THINGS_HTTP_PORT}/groups/;
|
||||
}
|
||||
|
||||
# Proxy pass to things service
|
||||
location ~ ^/(things|channels|connect) {
|
||||
include snippets/proxy-headers.conf;
|
||||
@@ -69,6 +83,13 @@ http {
|
||||
proxy_pass http://things:${MF_THINGS_HTTP_PORT};
|
||||
}
|
||||
|
||||
location ~ ^/(groups|members|keys) {
|
||||
include snippets/proxy-headers.conf;
|
||||
add_header Access-Control-Expose-Headers Location;
|
||||
proxy_pass http://auth:${MF_AUTH_HTTP_PORT};
|
||||
}
|
||||
|
||||
|
||||
location /version {
|
||||
include snippets/proxy-headers.conf;
|
||||
proxy_pass http://things:${MF_THINGS_HTTP_PORT};
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var groupRegexp = regexp.MustCompile("^[A-Za-z0-9]+[A-Za-z0-9_-]*$")
|
||||
|
||||
type createGroupReq struct {
|
||||
token string
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req createGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
if len(req.Name) > maxNameSize || req.Name == "" || !groupRegexp.MatchString(req.Name) {
|
||||
return errors.Wrap(groups.ErrMalformedEntity, groups.ErrBadGroupName)
|
||||
}
|
||||
if req.Type == "" {
|
||||
return errors.Wrap(groups.ErrMalformedEntity, groups.ErrMissingGroupType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateGroupReq struct {
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return groups.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.ParentID != "" {
|
||||
return groups.ErrParentInvariant
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listGroupsReq struct {
|
||||
token string
|
||||
level uint64
|
||||
metadata groups.Metadata
|
||||
name string
|
||||
groupID string
|
||||
tree bool
|
||||
}
|
||||
|
||||
func (req listGroupsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.level < 0 || req.level > 5 {
|
||||
return groups.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMemberGroupReq struct {
|
||||
token string
|
||||
offset uint64
|
||||
limit uint64
|
||||
metadata groups.Metadata
|
||||
name string
|
||||
groupID string
|
||||
memberID string
|
||||
tree bool
|
||||
}
|
||||
|
||||
func (req listMemberGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.groupID == "" && req.memberID == "" {
|
||||
return groups.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type memberGroupReq struct {
|
||||
token string
|
||||
groupID string
|
||||
memberID string
|
||||
}
|
||||
|
||||
func (req memberGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.groupID == "" && req.memberID == "" {
|
||||
return groups.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type groupReq struct {
|
||||
token string
|
||||
groupID string
|
||||
name string
|
||||
}
|
||||
|
||||
func (req groupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.groupID == "" && req.name == "" {
|
||||
return groups.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var errInvalidQueryParams = errors.New("invalid query params")
|
||||
|
||||
const (
|
||||
maxNameSize = 254
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
nameKey = "name"
|
||||
levelKey = "level"
|
||||
metadataKey = "metadata"
|
||||
treeKey = "tree"
|
||||
contentType = "application/json"
|
||||
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
defLevel = 1
|
||||
)
|
||||
|
||||
func DecodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, groups.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
l, err := readUintQuery(r, levelKey, defLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := readStringQuery(r, nameKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := readBoolQuery(r, treeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listGroupsReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
level: l,
|
||||
name: n,
|
||||
metadata: m,
|
||||
tree: t,
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeListMemberGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, groups.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
o, err := readUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := readUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := readStringQuery(r, nameKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := readBoolQuery(r, treeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listMemberGroupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
memberID: bone.GetValue(r, "memberID"),
|
||||
offset: o,
|
||||
limit: l,
|
||||
name: n,
|
||||
metadata: m,
|
||||
tree: t,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, groups.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req createGroupReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrFailedDecode, err)
|
||||
}
|
||||
|
||||
req.token = r.Header.Get("Authorization")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, groups.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req updateGroupReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(groups.ErrFailedDecode, err)
|
||||
}
|
||||
|
||||
req.id = bone.GetValue(r, "groupID")
|
||||
req.token = r.Header.Get("Authorization")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := groupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
name: bone.GetValue(r, "name"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeMemberGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := memberGroupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
memberID: bone.GetValue(r, "memberID"),
|
||||
}
|
||||
|
||||
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 readStringQuery(r *http.Request, key string) (string, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return "", errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return vals[0], nil
|
||||
}
|
||||
|
||||
func readMetadataQuery(r *http.Request, key string) (map[string]interface{}, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(vals[0]), &m)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errInvalidQueryParams, err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func readBoolQuery(r *http.Request, key string) (bool, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return true, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(vals[0])
|
||||
if err != nil {
|
||||
return false, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package groups
|
||||
|
||||
import "github.com/mainflux/mainflux/pkg/errors"
|
||||
|
||||
var (
|
||||
// ErrUnauthorizedAccess unauthorized access.
|
||||
ErrUnauthorizedAccess = errors.New("unauthorized access")
|
||||
|
||||
// ErrMalformedEntity malformed entity.
|
||||
ErrMalformedEntity = errors.New("malformed entity")
|
||||
|
||||
// ErrBadGroupName malformed entity.
|
||||
ErrBadGroupName = errors.New("incorrect group name")
|
||||
|
||||
// ErrGroupConflict group conflict.
|
||||
ErrGroupConflict = errors.New("group already exists")
|
||||
|
||||
// ErrCreateGroup indicates failure to create group.
|
||||
ErrCreateGroup = errors.New("failed to create group")
|
||||
|
||||
// ErrFetchGroups indicates failure to fetch groups.
|
||||
ErrFetchGroups = errors.New("failed to fetch groups")
|
||||
|
||||
// ErrUpdateGroup indicates failure to update group.
|
||||
ErrUpdateGroup = errors.New("failed to update group")
|
||||
|
||||
// ErrDeleteGroup indicates failure to delete group.
|
||||
ErrDeleteGroup = errors.New("failed to delete group")
|
||||
|
||||
// ErrNotFound indicates failure to find group.
|
||||
ErrNotFound = errors.New("failed to find group")
|
||||
|
||||
// ErrAssignToGroup indicates failure to assign member to a group.
|
||||
ErrAssignToGroup = errors.New("failed to assign member to a group")
|
||||
|
||||
// ErrUnassignFromGroup indicates failure to unassign member from a group.
|
||||
ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
|
||||
|
||||
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
|
||||
ErrUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// ErrFailedDecode indicates failed to decode request body
|
||||
ErrFailedDecode = errors.New("failed to decode request body")
|
||||
|
||||
// ErrMissingParent indicates that parent can't be found
|
||||
ErrMissingParent = errors.New("failed to retrieve parent")
|
||||
|
||||
// ErrParentInvariant indicates that parent can't be changed
|
||||
ErrParentInvariant = errors.New("parent can't be changed")
|
||||
|
||||
// ErrMissingGroupType indicates missing group type
|
||||
ErrMissingGroupType = errors.New("specifying group type is mandatory")
|
||||
)
|
||||
@@ -1,116 +0,0 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Member interface{}
|
||||
|
||||
type Metadata map[string]interface{}
|
||||
|
||||
type Group struct {
|
||||
ID string
|
||||
OwnerID string
|
||||
ParentID string
|
||||
Name string
|
||||
Description string
|
||||
Metadata Metadata
|
||||
// Indicates a level in hierarchy from first group node.
|
||||
// For a root node level is 1.
|
||||
Level int
|
||||
// Path is a path in a tree, consisted of group names
|
||||
// parentName.childrenName1.childrenName2 .
|
||||
Path string
|
||||
Type string
|
||||
Children []*Group
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Name string
|
||||
}
|
||||
|
||||
type GroupPage struct {
|
||||
PageMetadata
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
type MemberPage struct {
|
||||
PageMetadata
|
||||
Members []Member
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
// CreateGroup creates new group.
|
||||
CreateGroup(ctx context.Context, token string, g Group) (string, error)
|
||||
|
||||
// UpdateGroup updates the group identified by the provided ID.
|
||||
UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
|
||||
|
||||
// ViewGroup retrieves data about the group identified by ID.
|
||||
ViewGroup(ctx context.Context, token, id string) (Group, error)
|
||||
|
||||
// ListGroups retrieves groups.
|
||||
ListGroups(ctx context.Context, token string, level uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// ListChildren retrieves groups that are children to group identified by parentID
|
||||
ListChildren(ctx context.Context, token, parentID string, level uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// ListParents retrieves groups that are parent to group identified by childID.
|
||||
ListParents(ctx context.Context, token, childID string, level uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// ListMembers retrieves everything that is assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, m Metadata) (MemberPage, error)
|
||||
|
||||
// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
|
||||
ListMemberships(ctx context.Context, token, memberID string, offset, limit uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// RemoveGroup removes the group identified with the provided ID.
|
||||
RemoveGroup(ctx context.Context, token, id string) error
|
||||
|
||||
// Assign adds member with memberID into the group identified by groupID.
|
||||
Assign(ctx context.Context, token, memberID, groupID string) error
|
||||
|
||||
// Unassign removes member with memberID from group identified by groupID.
|
||||
Unassign(ctx context.Context, token, memberID, groupID string) error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
// Save group
|
||||
Save(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Update a group
|
||||
Update(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Delete a group
|
||||
Delete(ctx context.Context, groupID string) error
|
||||
|
||||
// RetrieveByID retrieves group by its id
|
||||
RetrieveByID(ctx context.Context, id string) (Group, error)
|
||||
|
||||
// RetrieveAll retrieves all groups.
|
||||
RetrieveAll(ctx context.Context, level uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
|
||||
RetrieveAllParents(ctx context.Context, groupID string, level uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
|
||||
RetrieveAllChildren(ctx context.Context, groupID string, level uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// Retrieves list of groups that member belongs to
|
||||
Memberships(ctx context.Context, memberID string, offset, limit uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// Members retrieves everything that is assigned to a group identified by groupID.
|
||||
Members(ctx context.Context, groupID string, offset, limit uint64, m Metadata) (MemberPage, error)
|
||||
|
||||
// Assign adds member to group.
|
||||
Assign(ctx context.Context, memberID, groupID string) error
|
||||
|
||||
// Unassign removes a member from a group
|
||||
Unassign(ctx context.Context, memberID, groupID string) error
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func newThingsService(tokens map[string]string) things.Service {
|
||||
thingCache := mocks.NewThingCache()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, idProvider)
|
||||
return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
|
||||
@@ -31,13 +31,12 @@ var (
|
||||
|
||||
func newUserService() users.Service {
|
||||
usersRepo := mocks.NewUserRepository()
|
||||
groupsRepo := mocks.NewGroupRepository()
|
||||
hasher := mocks.NewHasher()
|
||||
auth := mocks.NewAuthService(map[string]string{"user@example.com": "user@example.com"})
|
||||
emailer := mocks.NewEmailer()
|
||||
idProvider := uuid.New()
|
||||
|
||||
return users.New(usersRepo, groupsRepo, hasher, auth, emailer, idProvider, passRegex)
|
||||
return users.New(usersRepo, hasher, auth, emailer, idProvider, passRegex)
|
||||
}
|
||||
|
||||
func newUserServer(svc users.Service) *httptest.Server {
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ done
|
||||
###
|
||||
# Users
|
||||
###
|
||||
MF_USERS_LOG_LEVEL=info MF_EMAIL_TEMPLATE=../docker/templates/users.tmpl $BUILD_DIR/mainflux-users &
|
||||
MF_USERS_LOG_LEVEL=info MF_USERS_ADMIN_EMAIL=admin@mainflux.com MF_USERS_ADMIN_PASSWORD=12345678 MF_EMAIL_TEMPLATE=../docker/templates/users.tmpl $BUILD_DIR/mainflux-users &
|
||||
|
||||
###
|
||||
# Things
|
||||
|
||||
@@ -53,6 +53,9 @@ func identifyEndpoint(svc things.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identifyReq)
|
||||
id, err := svc.Identify(ctx, req.key)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
|
||||
@@ -50,5 +50,5 @@ func newService(tokens map[string]string) things.Service {
|
||||
thingCache := mocks.NewThingCache()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, idProvider)
|
||||
return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ const (
|
||||
email = "user@example.com"
|
||||
token = "token"
|
||||
wrong = "wrong_value"
|
||||
wrongID = "0"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -75,7 +74,7 @@ func newService(tokens map[string]string) things.Service {
|
||||
thingCache := mocks.NewThingCache()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, idProvider)
|
||||
return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
}
|
||||
|
||||
func newServer(svc things.Service) *httptest.Server {
|
||||
|
||||
+2
-133
@@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
@@ -280,98 +279,7 @@ func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id strin
|
||||
return lm.svc.Identify(ctx, key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, g groups.Group) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, g.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateGroup(ctx, token, g)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, g groups.Group) (gr groups.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, g.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateGroup(ctx, token, g)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (g groups.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_groups for token %s took %s to complete", token, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListGroups(ctx, token, level, gm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_children for token %s and parent %s took %s to complete", token, parentID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChildren(ctx, token, parentID, level, gm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_parents for token %s and child %s took for child %s to complete", token, childID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListParents(ctx, token, childID, level, gm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.MemberPage, err error) {
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID string, pm things.PageMetadata) (tp things.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_members for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -381,44 +289,5 @@ func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID str
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_memberships for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMemberships(ctx, token, groupID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Assign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberID, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Assign(ctx, token, memberID, groupID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unassign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberID, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Unassign(ctx, token, memberID, groupID)
|
||||
return lm.svc.ListMembers(ctx, token, groupID, pm)
|
||||
}
|
||||
|
||||
+2
-90
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
@@ -202,98 +201,11 @@ func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string,
|
||||
return ms.svc.Identify(ctx, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, g groups.Group) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_group").Add(1)
|
||||
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.CreateGroup(ctx, token, g)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, g groups.Group) (gr groups.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_group").Add(1)
|
||||
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.UpdateGroup(ctx, token, g)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_group").Add(1)
|
||||
ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (g groups.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_group").Add(1)
|
||||
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_groups").Add(1)
|
||||
ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListGroups(ctx, token, level, gm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "parents").Add(1)
|
||||
ms.latency.With("method", "parents").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListParents(ctx, token, childID, level, gm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_children").Add(1)
|
||||
ms.latency.With("method", "list_children").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChildren(ctx, token, parentID, level, gm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.MemberPage, err error) {
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID string, pm things.PageMetadata) (tp things.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_members").Add(1)
|
||||
ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_memberships").Add(1)
|
||||
ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMemberships(ctx, token, groupID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Assign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "assign").Add(1)
|
||||
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Assign(ctx, token, memberID, groupID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, memberID, groupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "unassign").Add(1)
|
||||
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Unassign(ctx, token, memberID, groupID)
|
||||
return ms.svc.ListMembers(ctx, token, groupID, pm)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
@@ -489,3 +491,40 @@ func disconnectEndpoint(svc things.Service) endpoint.Endpoint {
|
||||
return disconnectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMembersEndpoint(svc things.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listThingsGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return thingsPageRes{}, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
page, err := svc.ListMembers(ctx, req.token, req.groupID, req.pageMetadata)
|
||||
if err != nil {
|
||||
return thingsPageRes{}, err
|
||||
}
|
||||
|
||||
return buildThingsResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildThingsResponse(up things.Page) thingsPageRes {
|
||||
res := thingsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: up.Total,
|
||||
Offset: up.Offset,
|
||||
Limit: up.Limit,
|
||||
},
|
||||
Things: []viewThingRes{},
|
||||
}
|
||||
for _, th := range up.Things {
|
||||
view := viewThingRes{
|
||||
ID: th.ID,
|
||||
Key: th.Key,
|
||||
Owner: th.Owner,
|
||||
Metadata: th.Metadata,
|
||||
}
|
||||
res.Things = append(res.Things, view)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func newService(tokens map[string]string) things.Service {
|
||||
thingCache := mocks.NewThingCache()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, idProvider)
|
||||
return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
}
|
||||
|
||||
func newServer(svc things.Service) *httptest.Server {
|
||||
@@ -2313,11 +2313,6 @@ type thingRes struct {
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type thingsRes struct {
|
||||
Things []things.Thing
|
||||
created bool
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
const maxNameSize = 1024
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type createThingReq struct {
|
||||
token string
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -287,3 +284,40 @@ func (req createConnectionsReq) validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listThingsGroupReq struct {
|
||||
token string
|
||||
groupID string
|
||||
pageMetadata things.PageMetadata
|
||||
}
|
||||
|
||||
func (req listThingsGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return auth.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.groupID == "" {
|
||||
return auth.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.pageMetadata.Limit == 0 || req.pageMetadata.Limit > maxLimitSize {
|
||||
return things.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if len(req.pageMetadata.Name) > maxNameSize {
|
||||
return things.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.pageMetadata.Order != "" &&
|
||||
req.pageMetadata.Order != "name" && req.pageMetadata.Order != "id" {
|
||||
return things.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.pageMetadata.Dir != "" &&
|
||||
req.pageMetadata.Dir != "asc" && req.pageMetadata.Dir != "desc" {
|
||||
return things.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import (
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
groupsAPI "github.com/mainflux/mainflux/internal/groups/api"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
@@ -176,79 +175,9 @@ func MakeHandler(tracer opentracing.Tracer, svc things.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/things/:memberID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_memberships")(groupsAPI.ListMembership(svc)),
|
||||
groupsAPI.DecodeListMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "add_group")(groupsAPI.CreateGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupCreate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(groupsAPI.ListGroupsEndpoint(svc)),
|
||||
groupsAPI.DecodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_group")(groupsAPI.DeleteGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Put("/groups/:groupID/things/:memberID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "assign")(groupsAPI.AssignEndpoint(svc)),
|
||||
groupsAPI.DecodeMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/groups/:groupID/things/:memberID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "unassign")(groupsAPI.UnassignEndpoint(svc)),
|
||||
groupsAPI.DecodeMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/groups/:groupID/things", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_things")(groupsAPI.ListMembersEndpoint(svc)),
|
||||
groupsAPI.DecodeListMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Put("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "update_group")(groupsAPI.UpdateGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/groups/:groupID/children", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_children_groups")(groupsAPI.ListGroupChildrenEndpoint(svc)),
|
||||
groupsAPI.DecodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/groups/:groupID/parents", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_parent_groups")(groupsAPI.ListGroupParentsEndpoint(svc)),
|
||||
groupsAPI.DecodeListGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "view_group")(groupsAPI.ViewGroupEndpoint(svc)),
|
||||
groupsAPI.DecodeGroupRequest,
|
||||
r.Get("/groups/:groupId", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_things")(listMembersEndpoint(svc)),
|
||||
decodeListThingsGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
@@ -479,6 +408,34 @@ func decodeCreateConnections(_ context.Context, r *http.Request) (interface{}, e
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListThingsGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := readUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := readUintQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listThingsGroupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupId"),
|
||||
pageMetadata: things.PageMetadata{
|
||||
Offset: o,
|
||||
Limit: l,
|
||||
Metadata: m,
|
||||
},
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
@@ -528,7 +485,7 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
errors.Contains(errorVal, things.ErrRemoveEntity),
|
||||
errors.Contains(errorVal, things.ErrConnect),
|
||||
errors.Contains(errorVal, things.ErrDisconnect),
|
||||
errors.Contains(errorVal, groups.ErrCreateGroup):
|
||||
errors.Contains(errorVal, auth.ErrCreateGroup):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
case errors.Contains(errorVal, io.ErrUnexpectedEOF),
|
||||
|
||||
@@ -149,6 +149,45 @@ func (trm *thingRepositoryMock) RetrieveAll(_ context.Context, owner string, pm
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (trm *thingRepositoryMock) RetrieveByIDs(_ context.Context, thingIDs []string, pm things.PageMetadata) (things.Page, error) {
|
||||
trm.mu.Lock()
|
||||
defer trm.mu.Unlock()
|
||||
|
||||
items := make([]things.Thing, 0)
|
||||
|
||||
if pm.Limit == 0 {
|
||||
return things.Page{}, nil
|
||||
}
|
||||
|
||||
first := uint64(pm.Offset) + 1
|
||||
last := first + uint64(pm.Limit)
|
||||
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
for _, id := range thingIDs {
|
||||
suffix := fmt.Sprintf("-%s", id)
|
||||
for k, v := range trm.things {
|
||||
id, _ := strconv.ParseUint(v.ID, 10, 64)
|
||||
if strings.HasSuffix(k, suffix) && id >= first && id < last {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items = sortThings(pm, items)
|
||||
|
||||
page := things.Page{
|
||||
Things: items,
|
||||
PageMetadata: things.PageMetadata{
|
||||
Total: trm.counter,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (trm *thingRepositoryMock) RetrieveByChannel(_ context.Context, owner, chID string, pm things.PageMetadata) (things.Page, error) {
|
||||
trm.mu.Lock()
|
||||
defer trm.mu.Unlock()
|
||||
|
||||
+40
-3
@@ -511,8 +511,37 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/groups/{groupId}:
|
||||
get:
|
||||
summary: Retrieves things
|
||||
description: |
|
||||
Retrieves a list of things that belong to a group. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- things
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Order"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/ThingsPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
@@ -645,7 +674,7 @@ components:
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
format: jwt
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
@@ -663,6 +692,14 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Unique group identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
|
||||
@@ -377,6 +377,8 @@ func (cr channelRepository) hasThing(ctx context.Context, chanID, thingID string
|
||||
type dbMetadata map[string]interface{}
|
||||
|
||||
// Scan implements the database/sql scanner interface.
|
||||
// When interface is nil `m` is set to nil.
|
||||
// If error occurs on casting data then m points to empty metadata.
|
||||
func (m *dbMetadata) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
m = nil
|
||||
|
||||
@@ -166,7 +166,8 @@ func TestSingleChannelRetrieval(t *testing.T) {
|
||||
}
|
||||
chs, _ := chanRepo.Save(context.Background(), ch)
|
||||
ch.ID = chs[0].ID
|
||||
chanRepo.Connect(context.Background(), email, []string{ch.ID}, []string{th.ID})
|
||||
err = chanRepo.Connect(context.Background(), email, []string{ch.ID}, []string{th.ID})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
nonexistentChanID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
@@ -677,7 +678,8 @@ func TestDisconnect(t *testing.T) {
|
||||
})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
chID = chs[0].ID
|
||||
chanRepo.Connect(context.Background(), email, []string{chID}, []string{thID})
|
||||
err = chanRepo.Connect(context.Background(), email, []string{chID}, []string{thID})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
nonexistentThingID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
@@ -763,7 +765,8 @@ func TestHasThing(t *testing.T) {
|
||||
})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
chID = chs[0].ID
|
||||
chanRepo.Connect(context.Background(), email, []string{chID}, []string{thID})
|
||||
err = chanRepo.Connect(context.Background(), email, []string{chID}, []string{thID})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
nonexistentChanID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
@@ -838,7 +841,8 @@ func TestHasThingByID(t *testing.T) {
|
||||
})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
|
||||
chID = chs[0].ID
|
||||
chanRepo.Connect(context.Background(), email, []string{chID}, []string{thID})
|
||||
err = chanRepo.Connect(context.Background(), email, []string{chID}, []string{thID})
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
nonexistentChanID, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
@@ -1,601 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const maxLevel = 5
|
||||
|
||||
var (
|
||||
errDeleteGroupDB = errors.New("delete group failed")
|
||||
errSelectDb = errors.New("select group from db error")
|
||||
errConvertingStringToUUID = errors.New("error converting string")
|
||||
)
|
||||
|
||||
var _ groups.Repository = (*groupRepository)(nil)
|
||||
|
||||
type groupRepository struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// NewGroupRepo instantiates a PostgreSQL implementation of group
|
||||
// repository.
|
||||
func NewGroupRepo(db Database) groups.Repository {
|
||||
return &groupRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (gr groupRepository) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
var id string
|
||||
q := `INSERT INTO thing_groups (name, description, id, owner_id, metadata, path, created_at, updated_at)
|
||||
VALUES (:name, :description, :id, :owner_id, :metadata, CAST(:id AS ltree), now(), now()) RETURNING id`
|
||||
if g.ParentID != "" {
|
||||
q = `INSERT INTO thing_groups (name, description, id, owner_id, parent_id, metadata, path, created_at, updated_at)
|
||||
SELECT :name, :description, :id, :owner_id, :parent_id, :metadata, text2ltree(ltree2text(tg.path) || '.' || CAST(:id AS TEXT)), now(), now() FROM thing_groups tg WHERE id = :parent_id RETURNING id`
|
||||
}
|
||||
|
||||
dbu, err := toDBGroup(g)
|
||||
if err != nil {
|
||||
return groups.Group{}, err
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return groups.Group{}, errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return groups.Group{}, errors.Wrap(groups.ErrGroupConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return groups.Group{}, errors.Wrap(groups.ErrCreateGroup, err)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
if err := row.Scan(&id); err != nil {
|
||||
return groups.Group{}, err
|
||||
}
|
||||
g.ID = id
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
q := `UPDATE thing_groups SET description = :description, name = :name, metadata = :metadata, updated_at = now() WHERE id = :id`
|
||||
|
||||
dbu, err := toDBGroup(g)
|
||||
if err != nil {
|
||||
return groups.Group{}, errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
|
||||
return groups.Group{}, errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
qd := `DELETE FROM thing_groups WHERE id = :id`
|
||||
group := groups.Group{
|
||||
ID: groupID,
|
||||
}
|
||||
dbg, err := toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDeleteGroupDB, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errDeleteGroupDB, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(groups.ErrDeleteGroup, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
|
||||
dbu := dbGroup{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level FROM thing_groups WHERE id = $1`
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return groups.Group{}, errors.Wrap(groups.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return groups.Group{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
return toGroup(dbu)
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM thing_groups
|
||||
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
|
||||
cq := fmt.Sprintf("SELECT COUNT(*) FROM thing_groups WHERE nlevel(path) <= :level %s", mq)
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", "", "", level, gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := processRows(rows)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: total,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return groups.GroupPage{}, nil
|
||||
}
|
||||
|
||||
_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM thing_groups parent, thing_groups g
|
||||
WHERE parent.id = :parent_id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level %s`, mq)
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_groups parent, thing_groups g WHERE parent.id = :parent_id AND g.path @> parent.path %s`, mq)
|
||||
|
||||
if level > maxLevel {
|
||||
level = maxLevel
|
||||
}
|
||||
|
||||
dbPage, err := toDBGroupPage("", "", groupID, "", level, gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := processRows(rows)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: total,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if groupID == "" {
|
||||
return groups.GroupPage{}, nil
|
||||
}
|
||||
_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
|
||||
FROM thing_groups parent, thing_groups g
|
||||
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) <= :level %s`, mq)
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_groups parent, thing_groups g WHERE parent.id = :id AND g.path <@ parent.path %s`, mq)
|
||||
|
||||
if level > maxLevel {
|
||||
level = maxLevel
|
||||
}
|
||||
|
||||
dbPage, err := toDBGroupPage("", groupID, "", "", level, gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, err := processRows(rows)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: total,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Members(ctx context.Context, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
m, mq, err := getGroupsMetadataQuery("things_group", gm)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT th.id, th.name, th.key, th.metadata FROM things th, thing_group_relations g
|
||||
WHERE th.id = g.thing_id AND g.group_id = :group
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"group": groupID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"metadata": m,
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []groups.Member
|
||||
for rows.Next() {
|
||||
dbTh := dbThing{}
|
||||
if err := rows.StructScan(&dbTh); err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
thing, err := toThing(dbTh)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, thing)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM things th, thing_group_relations g
|
||||
WHERE th.id = g.thing_id AND g.group_id = :group %s;`, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := groups.MemberPage{
|
||||
Members: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Memberships(ctx context.Context, userID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
m, mq, err := getGroupsMetadataQuery("thing_groups", gm)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
FROM thing_group_relations gr, thing_groups g
|
||||
WHERE gr.group_id = g.id and gr.thing_id = :userID
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"userID": userID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"metadata": m,
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []groups.Group
|
||||
for rows.Next() {
|
||||
dbgr := dbGroup{}
|
||||
if err := rows.StructScan(&dbgr); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
gr, err := toGroup(dbgr)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_group_relations gr, thing_groups g
|
||||
WHERE gr.group_id = g.id and gr.thing_id = :userID %s;`, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := groups.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Assign(ctx context.Context, thingID, groupID string) error {
|
||||
dbr, err := toDBGroupRelation(thingID, groupID)
|
||||
if err != nil {
|
||||
return errors.Wrap(groups.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
qIns := `INSERT INTO thing_group_relations (group_id, thing_id) VALUES (:group_id, :thing_id)`
|
||||
_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(groups.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return errors.Wrap(groups.ErrGroupConflict, err)
|
||||
case errFK:
|
||||
return errors.Wrap(groups.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(groups.ErrAssignToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
|
||||
q := `DELETE FROM thing_group_relations WHERE thing_id = :thing_id AND group_id = :group_id`
|
||||
dbr, err := toDBGroupRelation(userID, groupID)
|
||||
if err != nil {
|
||||
return errors.Wrap(groups.ErrNotFound, err)
|
||||
}
|
||||
if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
|
||||
return errors.Wrap(groups.ErrGroupConflict, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbGroup struct {
|
||||
ID string `db:"id"`
|
||||
ParentID sql.NullString `db:"parent_id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Level int `db:"level"`
|
||||
Path string `db:"path"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type dbGroupPage struct {
|
||||
ID string `db:"id"`
|
||||
ParentID string `db:"parent_id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Path string `db:"path"`
|
||||
Level uint64 `db:"level"`
|
||||
Size uint64 `db:"size"`
|
||||
}
|
||||
|
||||
func toUUID(id string) (uuid.NullUUID, error) {
|
||||
var uid uuid.NullUUID
|
||||
if id == "" {
|
||||
return uuid.NullUUID{UUID: uuid.Nil, Valid: false}, nil
|
||||
}
|
||||
err := uid.Scan(id)
|
||||
return uid, err
|
||||
}
|
||||
|
||||
func toString(id uuid.NullUUID) (string, error) {
|
||||
if id.Valid {
|
||||
return id.UUID.String(), nil
|
||||
}
|
||||
if id.UUID == uuid.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return "", errConvertingStringToUUID
|
||||
}
|
||||
|
||||
func toDBGroup(g groups.Group) (dbGroup, error) {
|
||||
ownerID, err := toUUID(g.OwnerID)
|
||||
if err != nil {
|
||||
return dbGroup{}, err
|
||||
}
|
||||
|
||||
var parentID sql.NullString
|
||||
if g.ParentID != "" {
|
||||
parentID = sql.NullString{String: g.ParentID, Valid: true}
|
||||
}
|
||||
|
||||
meta := dbMetadata(g.Metadata)
|
||||
|
||||
return dbGroup{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
ParentID: parentID,
|
||||
OwnerID: ownerID,
|
||||
Description: g.Description,
|
||||
Metadata: meta,
|
||||
Path: g.Path,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBGroupPage(ownerID, id, parentID, path string, level uint64, metadata groups.Metadata) (dbGroupPage, error) {
|
||||
owner, err := toUUID(ownerID)
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
}
|
||||
|
||||
return dbGroupPage{
|
||||
Metadata: dbMetadata(metadata),
|
||||
ID: id,
|
||||
OwnerID: owner,
|
||||
Level: level,
|
||||
Path: path,
|
||||
ParentID: parentID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGroup(dbu dbGroup) (groups.Group, error) {
|
||||
ownerID, err := toString(dbu.OwnerID)
|
||||
if err != nil {
|
||||
return groups.Group{}, err
|
||||
}
|
||||
|
||||
return groups.Group{
|
||||
ID: dbu.ID,
|
||||
Name: dbu.Name,
|
||||
ParentID: dbu.ParentID.String,
|
||||
OwnerID: ownerID,
|
||||
Description: dbu.Description,
|
||||
Metadata: groups.Metadata(dbu.Metadata),
|
||||
Level: dbu.Level,
|
||||
Path: dbu.Path,
|
||||
UpdatedAt: dbu.UpdatedAt,
|
||||
CreatedAt: dbu.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dbGroupRelation struct {
|
||||
GroupID string `db:"group_id"`
|
||||
ThingID uuid.UUID `db:"thing_id"`
|
||||
}
|
||||
|
||||
func toDBGroupRelation(thingID, groupID string) (dbGroupRelation, error) {
|
||||
thID, err := uuid.FromString(thingID)
|
||||
if err != nil {
|
||||
return dbGroupRelation{}, err
|
||||
}
|
||||
return dbGroupRelation{
|
||||
GroupID: groupID,
|
||||
ThingID: thID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getGroupsMetadataQuery(db string, m groups.Metadata) ([]byte, string, error) {
|
||||
mq := ""
|
||||
mb := []byte("{}")
|
||||
if len(m) > 0 {
|
||||
mq = db + `.metadata @> :metadata`
|
||||
if db == "" {
|
||||
mq = `metadata @> :metadata`
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
mb = b
|
||||
}
|
||||
return mb, mq, nil
|
||||
}
|
||||
|
||||
func processRows(rows *sqlx.Rows) ([]groups.Group, error) {
|
||||
var items []groups.Group
|
||||
for rows.Next() {
|
||||
dbgr := dbGroup{}
|
||||
if err := rows.StructScan(&dbgr); err != nil {
|
||||
return items, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
gr, err := toGroup(dbgr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -97,28 +97,6 @@ func migrateDB(db *sqlx.DB) error {
|
||||
Id: "things_4",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS things ADD CONSTRAINT things_id_key UNIQUE (id)`,
|
||||
`CREATE extension LTREE`,
|
||||
`CREATE TABLE IF NOT EXISTS thing_groups (
|
||||
id VARCHAR(254) UNIQUE NOT NULL,
|
||||
parent_id VARCHAR(254),
|
||||
owner_id UUID,
|
||||
name VARCHAR(254) NOT NULL,
|
||||
description VARCHAR(1024),
|
||||
metadata JSONB,
|
||||
path LTREE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (owner_id, path),
|
||||
FOREIGN KEY (parent_id) REFERENCES thing_groups (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS thing_group_relations (
|
||||
thing_id UUID NOT NULL,
|
||||
group_id VARCHAR(254) NOT NULL,
|
||||
FOREIGN KEY (thing_id) REFERENCES things (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES thing_groups (id),
|
||||
PRIMARY KEY (thing_id, group_id)
|
||||
)`,
|
||||
`CREATE INDEX path_gist_idx ON thing_groups USING GIST (path);`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,19 +12,16 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/things/postgres"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
wrongID = "0"
|
||||
wrongValue = "wrong-value"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = logger.New(os.Stdout, logger.Info.String())
|
||||
db *sqlx.DB
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/lib/pq" // required for DB access
|
||||
@@ -22,11 +23,6 @@ const (
|
||||
errTruncation = "string_data_right_truncation"
|
||||
)
|
||||
|
||||
var (
|
||||
errUpdateDB = errors.New("failed to update db")
|
||||
errRetrieveDB = errors.New("failed retrieving from db")
|
||||
)
|
||||
|
||||
var _ things.ThingRepository = (*thingRepository)(nil)
|
||||
|
||||
type thingRepository struct {
|
||||
@@ -181,6 +177,72 @@ func (tr thingRepository) RetrieveByKey(ctx context.Context, key string) (string
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (tr thingRepository) RetrieveByIDs(ctx context.Context, thingIDs []string, pm things.PageMetadata) (things.Page, error) {
|
||||
if len(thingIDs) == 0 {
|
||||
return things.Page{}, nil
|
||||
}
|
||||
|
||||
nq, name := getNameQuery(pm.Name)
|
||||
oq := getOrderQuery(pm.Order)
|
||||
dq := getDirQuery(pm.Dir)
|
||||
idq := fmt.Sprintf("WHERE id IN ('%s') ", strings.Join(thingIDs, "','"))
|
||||
|
||||
m, mq, err := getMetadataQuery(pm.Metadata)
|
||||
if err != nil {
|
||||
return things.Page{}, errors.Wrap(things.ErrSelectEntity, err)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, owner, name, key, metadata FROM things
|
||||
%s%s%s ORDER BY %s %s LIMIT :limit OFFSET :offset;`, idq, mq, nq, oq, dq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"limit": pm.Limit,
|
||||
"offset": pm.Offset,
|
||||
"name": name,
|
||||
"metadata": m,
|
||||
}
|
||||
|
||||
rows, err := tr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return things.Page{}, errors.Wrap(things.ErrSelectEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []things.Thing
|
||||
for rows.Next() {
|
||||
dbth := dbThing{}
|
||||
if err := rows.StructScan(&dbth); err != nil {
|
||||
return things.Page{}, errors.Wrap(things.ErrSelectEntity, err)
|
||||
}
|
||||
|
||||
th, err := toThing(dbth)
|
||||
if err != nil {
|
||||
return things.Page{}, errors.Wrap(things.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
items = append(items, th)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM things %s%s%s;`, idq, mq, nq)
|
||||
|
||||
total, err := total(ctx, tr.db, cq, params)
|
||||
if err != nil {
|
||||
return things.Page{}, errors.Wrap(things.ErrSelectEntity, err)
|
||||
}
|
||||
|
||||
page := things.Page{
|
||||
Things: items,
|
||||
PageMetadata: things.PageMetadata{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Order: pm.Order,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (tr thingRepository) RetrieveAll(ctx context.Context, owner string, pm things.PageMetadata) (things.Page, error) {
|
||||
nq, name := getNameQuery(pm.Name)
|
||||
oq := getOrderQuery(pm.Order)
|
||||
|
||||
@@ -5,6 +5,7 @@ package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -377,11 +378,17 @@ func TestMultiThingRetrieval(t *testing.T) {
|
||||
|
||||
email := "thing-multi-retrieval@example.com"
|
||||
name := "thing_name"
|
||||
metadata := things.Metadata{
|
||||
"field": "value",
|
||||
}
|
||||
metaStr := `{"field1":"value1","field2":{"subfield11":"value2","subfield12":{"subfield121":"value3","subfield122":"value4"}}}`
|
||||
subMetaStr := `{"field2":{"subfield12":{"subfield121":"value3"}}}`
|
||||
|
||||
metadata := things.Metadata{}
|
||||
json.Unmarshal([]byte(metaStr), &metadata)
|
||||
|
||||
subMeta := things.Metadata{}
|
||||
json.Unmarshal([]byte(subMetaStr), &subMeta)
|
||||
|
||||
wrongMeta := things.Metadata{
|
||||
"wrong": "wrong",
|
||||
"field": "value1",
|
||||
}
|
||||
|
||||
offset := uint64(1)
|
||||
@@ -481,6 +488,16 @@ func TestMultiThingRetrieval(t *testing.T) {
|
||||
},
|
||||
size: metaNum + nameMetaNum,
|
||||
},
|
||||
"retrieve things with partial metadata": {
|
||||
owner: email,
|
||||
pageMetadata: things.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: n,
|
||||
Total: metaNum + nameMetaNum,
|
||||
Metadata: subMeta,
|
||||
},
|
||||
size: metaNum + nameMetaNum,
|
||||
},
|
||||
"retrieve things with non-existing metadata": {
|
||||
owner: email,
|
||||
pageMetadata: things.PageMetadata{
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
wrongID = 0
|
||||
wrongValue = "wrong-value"
|
||||
)
|
||||
|
||||
|
||||
+2
-43
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
@@ -245,46 +244,6 @@ func (es eventStore) Identify(ctx context.Context, key string) (string, error) {
|
||||
return es.svc.Identify(ctx, key)
|
||||
}
|
||||
|
||||
func (es eventStore) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
|
||||
return es.svc.CreateGroup(ctx, token, g)
|
||||
}
|
||||
|
||||
func (es eventStore) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
|
||||
return es.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
return es.svc.ListGroups(ctx, token, level, gm)
|
||||
}
|
||||
|
||||
func (es eventStore) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
return es.svc.ListParents(ctx, token, childID, level, gm)
|
||||
}
|
||||
|
||||
func (es eventStore) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
return es.svc.ListChildren(ctx, token, parentID, level, gm)
|
||||
}
|
||||
|
||||
func (es eventStore) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
return es.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (es eventStore) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
return es.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) Unassign(ctx context.Context, token, memberID, groupID string) error {
|
||||
return es.svc.Unassign(ctx, token, memberID, groupID)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
|
||||
return es.svc.UpdateGroup(ctx, token, g)
|
||||
}
|
||||
|
||||
func (es eventStore) Assign(ctx context.Context, token, memberID, groupID string) error {
|
||||
return es.svc.Assign(ctx, token, memberID, groupID)
|
||||
}
|
||||
|
||||
func (es eventStore) ListMemberships(ctx context.Context, token, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
return es.svc.ListMemberships(ctx, token, memberID, offset, limit, gm)
|
||||
func (es eventStore) ListMembers(ctx context.Context, token, groupID string, pm things.PageMetadata) (things.Page, error) {
|
||||
return es.svc.ListMembers(ctx, token, groupID, pm)
|
||||
}
|
||||
|
||||
@@ -47,11 +47,11 @@ func newService(tokens map[string]string) things.Service {
|
||||
thingCache := mocks.NewThingCache()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, idProvider)
|
||||
return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
}
|
||||
|
||||
func TestCreateThings(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
svc = redis.NewEventStoreMiddleware(svc, redisClient)
|
||||
@@ -111,7 +111,7 @@ func TestCreateThings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateThing(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -169,7 +169,7 @@ func TestUpdateThing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestViewThing(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -185,7 +185,7 @@ func TestViewThing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListThings(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -200,7 +200,7 @@ func TestListThings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListThingsByChannel(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -221,7 +221,7 @@ func TestListThingsByChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveThing(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -280,7 +280,7 @@ func TestRemoveThing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateChannels(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
svc = redis.NewEventStoreMiddleware(svc, redisClient)
|
||||
@@ -337,7 +337,7 @@ func TestCreateChannels(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create channel without sending event.
|
||||
@@ -405,7 +405,7 @@ func TestUpdateChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create channel without sending event.
|
||||
@@ -421,7 +421,7 @@ func TestViewChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -436,7 +436,7 @@ func TestListChannels(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListChannelsByThing(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing without sending event.
|
||||
@@ -457,7 +457,7 @@ func TestListChannelsByThing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create channel without sending event.
|
||||
@@ -516,7 +516,7 @@ func TestRemoveChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConnectEvent(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing and channel that will be connected.
|
||||
@@ -582,7 +582,7 @@ func TestConnectEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDisconnectEvent(t *testing.T) {
|
||||
redisClient.FlushAll().Err()
|
||||
_ = redisClient.FlushAll().Err()
|
||||
|
||||
svc := newService(map[string]string{token: email})
|
||||
// Create thing and channel that will be connected.
|
||||
|
||||
+26
-112
@@ -6,15 +6,12 @@ package things
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/ulid"
|
||||
)
|
||||
|
||||
const things = "things"
|
||||
|
||||
var (
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
@@ -41,12 +38,6 @@ var (
|
||||
// ErrDisconnect indicates error in removing connection
|
||||
ErrDisconnect = errors.New("remove connection failed")
|
||||
|
||||
// ErrCreateGroup indicates error in creating group.
|
||||
ErrCreateGroup = errors.New("failed to create group")
|
||||
|
||||
// ErrGenerateGroupID indicates error in creating group.
|
||||
ErrGenerateGroupID = errors.New("failed to generate group id")
|
||||
|
||||
// ErrFailedToRetrieveThings failed to retrieve things.
|
||||
ErrFailedToRetrieveThings = errors.New("failed to retrieve group members")
|
||||
)
|
||||
@@ -128,7 +119,8 @@ type Service interface {
|
||||
// Identify returns thing ID for given thing key.
|
||||
Identify(ctx context.Context, key string) (string, error)
|
||||
|
||||
groups.Service
|
||||
// ListMembers retrieves everything that is assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID string, pm PageMetadata) (Page, error)
|
||||
}
|
||||
|
||||
// PageMetadata contains page metadata that helps navigation.
|
||||
@@ -140,7 +132,7 @@ type PageMetadata struct {
|
||||
Order string
|
||||
Dir string
|
||||
Metadata map[string]interface{}
|
||||
Connected bool // Used for connected or diconnected lists
|
||||
Connected bool // Used for connected or disconnected lists
|
||||
}
|
||||
|
||||
var _ Service = (*thingsService)(nil)
|
||||
@@ -149,7 +141,6 @@ type thingsService struct {
|
||||
auth mainflux.AuthServiceClient
|
||||
things ThingRepository
|
||||
channels ChannelRepository
|
||||
groups groups.Repository
|
||||
channelCache ChannelCache
|
||||
thingCache ThingCache
|
||||
idProvider mainflux.IDProvider
|
||||
@@ -157,11 +148,10 @@ type thingsService struct {
|
||||
}
|
||||
|
||||
// New instantiates the things service implementation.
|
||||
func New(auth mainflux.AuthServiceClient, things ThingRepository, channels ChannelRepository, groups groups.Repository, ccache ChannelCache, tcache ThingCache, idp mainflux.IDProvider) Service {
|
||||
func New(auth mainflux.AuthServiceClient, things ThingRepository, channels ChannelRepository, ccache ChannelCache, tcache ThingCache, idp mainflux.IDProvider) Service {
|
||||
return &thingsService{
|
||||
auth: auth,
|
||||
things: things,
|
||||
groups: groups,
|
||||
channels: channels,
|
||||
channelCache: ccache,
|
||||
thingCache: tcache,
|
||||
@@ -417,107 +407,31 @@ func (ts *thingsService) hasThing(ctx context.Context, chanID, thingKey string)
|
||||
return thingID, nil
|
||||
}
|
||||
|
||||
func (ts *thingsService) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
|
||||
user, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
func (ts *thingsService) ListMembers(ctx context.Context, token, groupID string, pm PageMetadata) (Page, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
res, err := ts.members(ctx, token, groupID, "things", pm.Offset, pm.Limit)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
return Page{}, nil
|
||||
}
|
||||
|
||||
ulid, err := ts.ulidProvider.ID()
|
||||
return ts.things.RetrieveByIDs(ctx, res, pm)
|
||||
}
|
||||
|
||||
func (ts *thingsService) members(ctx context.Context, token, groupID, groupType string, limit, offset uint64) ([]string, error) {
|
||||
req := mainflux.MembersReq{
|
||||
Token: token,
|
||||
GroupID: groupID,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Type: groupType,
|
||||
}
|
||||
|
||||
res, err := ts.auth.Members(ctx, &req)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrGenerateGroupID, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
g.ID = ulid
|
||||
g.OwnerID = user.GetId()
|
||||
if _, err := ts.groups.Save(ctx, g); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return g.ID, nil
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.RetrieveAll(ctx, level, gm)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListParents(ctx context.Context, token string, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.RetrieveAllParents(ctx, childID, level, gm)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListChildren(ctx context.Context, token string, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.RetrieveAllChildren(ctx, parentID, level, gm)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
p, err := ts.groups.Members(ctx, groupID, offset, limit, gm)
|
||||
if err != nil {
|
||||
return groups.MemberPage{}, errors.Wrap(ErrFailedToRetrieveThings, err)
|
||||
}
|
||||
mp := groups.MemberPage{
|
||||
PageMetadata: groups.PageMetadata{
|
||||
Total: p.Total,
|
||||
Offset: p.Offset,
|
||||
Limit: p.Limit,
|
||||
Name: things,
|
||||
},
|
||||
Members: make([]groups.Member, 0),
|
||||
}
|
||||
mp.Members = append(mp.Members, p.Members)
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
func (ts *thingsService) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (ts *thingsService) Unassign(ctx context.Context, token, memberID, groupID string) error {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.Unassign(ctx, memberID, groupID)
|
||||
}
|
||||
|
||||
func (ts *thingsService) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return ts.groups.Update(ctx, g)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (ts *thingsService) Assign(ctx context.Context, token, memberID, groupID string) error {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.Assign(ctx, memberID, groupID)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListMemberships(ctx context.Context, token string, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
|
||||
return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return ts.groups.Memberships(ctx, memberID, offset, limit, gm)
|
||||
return res.Members, nil
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func newService(tokens map[string]string) things.Service {
|
||||
thingCache := mocks.NewThingCache()
|
||||
idProvider := uuid.NewMock()
|
||||
|
||||
return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, idProvider)
|
||||
return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider)
|
||||
}
|
||||
|
||||
func TestCreateThings(t *testing.T) {
|
||||
|
||||
+4
-1
@@ -73,9 +73,12 @@ type ThingRepository interface {
|
||||
// RetrieveByKey returns thing ID for given thing key.
|
||||
RetrieveByKey(ctx context.Context, key string) (string, error)
|
||||
|
||||
// RetrieveAll retrieves the subset of things owned by the specified user.
|
||||
// RetrieveAll retrieves the subset of things owned by the specified user
|
||||
RetrieveAll(ctx context.Context, owner string, pm PageMetadata) (Page, error)
|
||||
|
||||
// RetrieveByIDs retrieves the subset of things specified by given thing ids.
|
||||
RetrieveByIDs(ctx context.Context, thingIDs []string, pm PageMetadata) (Page, error)
|
||||
|
||||
// RetrieveByChannel retrieves the subset of things owned by the specified
|
||||
// user and connected or not connected to specified channel.
|
||||
RetrieveByChannel(ctx context.Context, owner, chID string, pm PageMetadata) (Page, error)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
saveChannelOp = "save_channel"
|
||||
saveChannelsOp = "save_channels"
|
||||
updateChannelOp = "update_channel"
|
||||
retrieveChannelByIDOp = "retrieve_channel_by_id"
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing contains middlewares that will add spans to existing traces.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/internal/groups"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
assign = "assign"
|
||||
saveGroup = "save_group"
|
||||
deleteGroup = "delete_group"
|
||||
updateGroup = "update_group"
|
||||
retrieveByID = "retrieve_by_id"
|
||||
retrieveAllAncestors = "retrieve_all_ancestors"
|
||||
retrieveAllChildren = "retrieve_all_children"
|
||||
retrieveAll = "retrieve_all_groups"
|
||||
retrieveByName = "retrieve_by_name"
|
||||
memberships = "memberships"
|
||||
members = "members"
|
||||
unassign = "unassign"
|
||||
)
|
||||
|
||||
var _ groups.Repository = (*groupRepositoryMiddleware)(nil)
|
||||
|
||||
type groupRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo groups.Repository
|
||||
}
|
||||
|
||||
// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
|
||||
func GroupRepositoryMiddleware(tracer opentracing.Tracer, gr groups.Repository) groups.Repository {
|
||||
return groupRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: gr,
|
||||
}
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, saveGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Save(ctx, g)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, updateGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Update(ctx, g)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, deleteGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Delete(ctx, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveByID)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllAncestors)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllParents(ctx, groupID, level, gm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAllChildren)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllChildren(ctx, groupID, level, gm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAll)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAll(ctx, level, gm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, memberships)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Memberships(ctx, memberID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Members(ctx context.Context, memberID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, members)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Members(ctx, memberID, offset, limit, gm)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, memberID, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, unassign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Unassign(ctx, memberID, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, memberID, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, assign)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Assign(ctx, memberID, groupID)
|
||||
}
|
||||
@@ -90,6 +90,14 @@ func (trm thingRepositoryMiddleware) RetrieveAll(ctx context.Context, owner stri
|
||||
return trm.repo.RetrieveAll(ctx, owner, pm)
|
||||
}
|
||||
|
||||
func (trm thingRepositoryMiddleware) RetrieveByIDs(ctx context.Context, thingIDs []string, pm things.PageMetadata) (things.Page, error) {
|
||||
span := createSpan(ctx, trm.tracer, retrieveAllThingsOp)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return trm.repo.RetrieveByIDs(ctx, thingIDs, pm)
|
||||
}
|
||||
|
||||
func (trm thingRepositoryMiddleware) RetrieveByChannel(ctx context.Context, owner, chID string, pm things.PageMetadata) (things.Page, error) {
|
||||
span := createSpan(ctx, trm.tracer, retrieveThingsByChannelOp)
|
||||
defer span.Finish()
|
||||
|
||||
+9
-167
@@ -7,6 +7,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
@@ -172,182 +174,22 @@ func loginEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func createGroupEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
group := users.Group{
|
||||
Name: req.Name,
|
||||
ParentID: req.ParentID,
|
||||
Description: req.Description,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
saved, err := svc.CreateGroup(ctx, req.token, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := createGroupRes{
|
||||
ID: saved.ID,
|
||||
Name: saved.Name,
|
||||
Description: saved.Description,
|
||||
Metadata: saved.Metadata,
|
||||
ParentID: saved.ParentID,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func assignUserToGroup(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(userGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.Assign(ctx, req.token, req.userID, req.groupID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return assignUserToGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeUserFromGroup(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(userGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.Unassign(ctx, req.token, req.userID, req.groupID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return removeUserFromGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMembersEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listUserGroupReq)
|
||||
req := request.(listMemberGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return users.UserPage{}, err
|
||||
return userPageRes{}, errors.Wrap(auth.ErrMalformedEntity, err)
|
||||
}
|
||||
up, err := svc.ListMembers(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
|
||||
|
||||
page, err := svc.ListMembers(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
|
||||
if err != nil {
|
||||
return users.UserPage{}, err
|
||||
return userPageRes{}, err
|
||||
}
|
||||
return buildUsersResponse(up), nil
|
||||
|
||||
return buildUsersResponse(page), nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMembershipsEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listUserGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return users.UserPage{}, err
|
||||
}
|
||||
gp, err := svc.ListMemberships(ctx, req.token, req.userID, req.offset, req.limit, req.metadata)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
return buildGroupsResponse(gp), nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateGroupEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return updateGroupRes{}, err
|
||||
}
|
||||
|
||||
group := users.Group{
|
||||
ID: req.id,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
if err := svc.UpdateGroup(ctx, req.token, group); err != nil {
|
||||
return updateGroupRes{}, err
|
||||
}
|
||||
|
||||
return updateGroupRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewGroupEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
group, err := svc.ViewGroup(ctx, req.token, req.groupID)
|
||||
if err != nil {
|
||||
return viewGroupRes{}, err
|
||||
}
|
||||
res := viewGroupRes{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
ParentID: group.ParentID,
|
||||
OwnerID: group.OwnerID,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listGroupsEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listUserGroupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
gp, err := svc.ListGroups(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
|
||||
if err != nil {
|
||||
return groupPageRes{}, err
|
||||
}
|
||||
return buildGroupsResponse(gp), nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGroupEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(groupReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.RemoveGroup(ctx, req.token, req.groupID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groupDeleteRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildGroupsResponse(gp users.GroupPage) groupPageRes {
|
||||
res := groupPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: gp.Total,
|
||||
Offset: gp.Offset,
|
||||
Limit: gp.Limit,
|
||||
},
|
||||
Groups: []viewGroupRes{},
|
||||
}
|
||||
for _, group := range gp.Groups {
|
||||
view := viewGroupRes{
|
||||
ID: group.ID,
|
||||
ParentID: group.ParentID,
|
||||
Name: group.Name,
|
||||
Description: group.Description,
|
||||
Metadata: group.Metadata,
|
||||
}
|
||||
res.Groups = append(res.Groups, view)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func buildUsersResponse(up users.UserPage) userPageRes {
|
||||
res := userPageRes{
|
||||
pageRes: pageRes{
|
||||
|
||||
@@ -42,7 +42,6 @@ var (
|
||||
weakPassword = toJSON(errorRes{users.ErrPasswordFormat.Error()})
|
||||
unsupportedRes = toJSON(errorRes{api.ErrUnsupportedContentType.Error()})
|
||||
failDecodeRes = toJSON(errorRes{api.ErrFailedDecode.Error()})
|
||||
groupExists = toJSON(errorRes{users.ErrGroupConflict.Error()})
|
||||
passRegex = regexp.MustCompile("^.{8,}$")
|
||||
)
|
||||
|
||||
@@ -73,13 +72,12 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
|
||||
func newService() users.Service {
|
||||
usersRepo := mocks.NewUserRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
hasher := bcrypt.New()
|
||||
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
|
||||
email := mocks.NewEmailer()
|
||||
idProvider := uuid.New()
|
||||
|
||||
return users.New(usersRepo, groupRepo, hasher, auth, email, idProvider, passRegex)
|
||||
return users.New(usersRepo, hasher, auth, email, idProvider, passRegex)
|
||||
}
|
||||
|
||||
func newServer(svc users.Service) *httptest.Server {
|
||||
@@ -454,71 +452,6 @@ func TestPasswordChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupCreate(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
|
||||
|
||||
_, err := svc.Register(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
tkn, _ := auth.Issue(context.Background(), &mainflux.IssueReq{Id: user.ID, Email: user.Email, Type: 0})
|
||||
token := tkn.GetValue()
|
||||
|
||||
expectedSuccess := ""
|
||||
|
||||
groupData := struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}{}
|
||||
|
||||
groupData.Token = token
|
||||
groupData.Name = "Mainflux"
|
||||
createValidTokenRequest := toJSON(groupData)
|
||||
|
||||
groupData.Token = "invalid"
|
||||
createInvalidTokenRequest := toJSON(groupData)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
status int
|
||||
res string
|
||||
tok string
|
||||
}{
|
||||
{"group create with valid token", createValidTokenRequest, contentType, http.StatusCreated, expectedSuccess, token},
|
||||
{"group create with existing name", createValidTokenRequest, contentType, http.StatusConflict, groupExists, token},
|
||||
{"group create with invalid token", createInvalidTokenRequest, contentType, http.StatusForbidden, unauthRes, ""},
|
||||
{"group create with empty JSON request", "{}", contentType, http.StatusBadRequest, malformedRes, token},
|
||||
{"group create empty request", "", contentType, http.StatusBadRequest, malformedRes, token},
|
||||
{"group create missing content type", createValidTokenRequest, "", http.StatusUnsupportedMediaType, unsupportedRes, token},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/groups", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.req),
|
||||
token: tc.tok,
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
token := strings.Trim(string(body), "\n")
|
||||
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, token, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, token))
|
||||
}
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+3
-107
@@ -155,9 +155,9 @@ func (lm *loggingMiddleware) SendPasswordReset(ctx context.Context, host, email,
|
||||
return lm.svc.SendPasswordReset(ctx, host, email, token)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group users.Group) (u users.Group, err error) {
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, m users.Metadata) (mp users.UserPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_group with name %s took %s to complete", group.Name, time.Since(begin))
|
||||
message := fmt.Sprintf("Method list_members for group %s took %s to complete", groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -165,109 +165,5 @@ func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, grou
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListGroups(ctx context.Context, token, id string, offset, limit uint64, um users.Metadata) (e users.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_groups for parent %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListGroups(ctx, token, id, offset, limit, um)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, id string, offset, limit uint64, um users.Metadata) (e users.UserPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_members for parent %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMembers(ctx, token, id, offset, limit, um)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_group with id %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, group users.Group) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_group %s took %s to complete", group.Name, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (u users.Group, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_group with id %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Assign(ctx context.Context, token, userID, groupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method assign user %s, group %s took %s to complete", userID, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Assign(ctx, token, userID, groupID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unassign(ctx context.Context, token, userID, groupID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unassign for user %s, group %s took %s to complete", userID, groupID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Unassign(ctx, token, userID, groupID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, id string, offset, limit uint64, um users.Metadata) (e users.GroupPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_memberships for user %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListMemberships(ctx, token, id, offset, limit, um)
|
||||
return lm.svc.ListMembers(ctx, token, groupID, offset, limit, m)
|
||||
}
|
||||
|
||||
+2
-75
@@ -118,84 +118,11 @@ func (ms *metricsMiddleware) SendPasswordReset(ctx context.Context, host, email,
|
||||
return ms.svc.SendPasswordReset(ctx, host, email, token)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group users.Group) (users.Group, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_group").Add(1)
|
||||
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CreateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListGroups(ctx context.Context, token, id string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_groups").Add(1)
|
||||
ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListGroups(ctx, token, id, offset, limit, um)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, id string, offset, limit uint64, um users.Metadata) (users.UserPage, error) {
|
||||
func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm users.Metadata) (users.UserPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_members").Add(1)
|
||||
ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMembers(ctx, token, id, offset, limit, um)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_group").Add(1)
|
||||
ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveGroup(ctx, token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, group users.Group) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_group").Add(1)
|
||||
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateGroup(ctx, token, group)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, name string) (users.Group, error) {
|
||||
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_group").Add(1)
|
||||
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewGroup(ctx, token, name)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Assign(ctx context.Context, token, userID, groupID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "assign").Add(1)
|
||||
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Assign(ctx, token, userID, groupID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, userID, groupID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "unassign").Add(1)
|
||||
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Unassign(ctx, token, userID, groupID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, id string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_memberships").Add(1)
|
||||
ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListMemberships(ctx, token, id, offset, limit, um)
|
||||
return ms.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
|
||||
}
|
||||
|
||||
+6
-82
@@ -4,13 +4,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
groups "github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNameSize = 1024
|
||||
)
|
||||
|
||||
type userReq struct {
|
||||
user users.User
|
||||
}
|
||||
@@ -105,95 +102,22 @@ func (req passwChangeReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type createGroupReq struct {
|
||||
token string
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req createGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return users.ErrUnauthorizedAccess
|
||||
}
|
||||
if len(req.Name) > maxNameSize || req.Name == "" {
|
||||
return users.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateGroupReq struct {
|
||||
token string
|
||||
id string
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (req updateGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return users.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.id == "" {
|
||||
return users.ErrMalformedEntity
|
||||
}
|
||||
if req.Name == "" || len(req.Name) > maxNameSize {
|
||||
return users.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listUserGroupReq struct {
|
||||
type listMemberGroupReq struct {
|
||||
token string
|
||||
offset uint64
|
||||
limit uint64
|
||||
metadata users.Metadata
|
||||
name string
|
||||
groupID string
|
||||
userID string
|
||||
}
|
||||
|
||||
func (req listUserGroupReq) validate() error {
|
||||
func (req listMemberGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return users.ErrUnauthorizedAccess
|
||||
return groups.ErrUnauthorizedAccess
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type userGroupReq struct {
|
||||
token string
|
||||
groupID string
|
||||
userID string
|
||||
}
|
||||
|
||||
func (req userGroupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return users.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.groupID == "" {
|
||||
return users.ErrMalformedEntity
|
||||
}
|
||||
if req.userID == "" {
|
||||
return users.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type groupReq struct {
|
||||
token string
|
||||
groupID string
|
||||
name string
|
||||
}
|
||||
|
||||
func (req groupReq) validate() error {
|
||||
if req.token == "" {
|
||||
return users.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.groupID == "" && req.name == "" {
|
||||
return users.ErrMalformedEntity
|
||||
return groups.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+7
-24
@@ -8,7 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -19,7 +19,7 @@ var (
|
||||
_ mainflux.Response = (*viewGroupRes)(nil)
|
||||
_ mainflux.Response = (*createGroupRes)(nil)
|
||||
_ mainflux.Response = (*createUserRes)(nil)
|
||||
_ mainflux.Response = (*groupDeleteRes)(nil)
|
||||
_ mainflux.Response = (*deleteRes)(nil)
|
||||
_ mainflux.Response = (*assignUserToGroupRes)(nil)
|
||||
_ mainflux.Response = (*removeUserFromGroupRes)(nil)
|
||||
)
|
||||
@@ -93,7 +93,7 @@ func (res updateUserRes) Empty() bool {
|
||||
type viewUserRes struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Groups []users.Group `json:"groups"`
|
||||
Groups []auth.Group `json:"groups"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
@@ -211,34 +211,17 @@ func (res passwChangeRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type groupPageRes struct {
|
||||
pageRes
|
||||
Groups []viewGroupRes
|
||||
}
|
||||
type deleteRes struct{}
|
||||
|
||||
func (res groupPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res groupPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type groupDeleteRes struct{}
|
||||
|
||||
func (res groupDeleteRes) Code() int {
|
||||
func (res deleteRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res groupDeleteRes) Headers() map[string]string {
|
||||
func (res deleteRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res groupDeleteRes) Empty() bool {
|
||||
func (res deleteRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
+5
-130
@@ -27,7 +27,6 @@ const (
|
||||
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
nameKey = "name"
|
||||
emailKey = "email"
|
||||
metadataKey = "metadata"
|
||||
|
||||
@@ -88,13 +87,6 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/users/:userID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_memberships")(listMembershipsEndpoint(svc)),
|
||||
decodeListUserGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/password/reset-request", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "res-req")(passwordResetRequestEndpoint(svc)),
|
||||
decodePasswordResetRequest,
|
||||
@@ -116,65 +108,9 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Post("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "add_group")(createGroupEndpoint(svc)),
|
||||
decodeGroupCreate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_groups")(listGroupsEndpoint(svc)),
|
||||
decodeListUserGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "delete_group")(deleteGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Put("/groups/:groupID/users/:userID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "assign_user_to_group")(assignUserToGroup(svc)),
|
||||
decodeUserGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Delete("/groups/:groupID/users/:userID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "remove_user_from_group")(removeUserFromGroup(svc)),
|
||||
decodeUserGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/users", kithttp.NewServer(
|
||||
mux.Get("/groups/:groupId", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_members")(listMembersEndpoint(svc)),
|
||||
decodeListUserGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Patch("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "update_group")(updateGroupEndpoint(svc)),
|
||||
decodeGroupUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID/groups", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "list_children_groups")(listGroupsEndpoint(svc)),
|
||||
decodeListUserGroupsRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
mux.Get("/groups/:groupID", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "group")(viewGroupEndpoint(svc)),
|
||||
decodeGroupRequest,
|
||||
decodeListMemberGroupRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
@@ -304,49 +240,7 @@ func decodePasswordChange(_ context.Context, r *http.Request) (interface{}, erro
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Group related methods
|
||||
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
var req createGroupReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(users.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
req.token = r.Header.Get("Authorization")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateGroupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "groupID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(users.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := groupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
name: bone.GetValue(r, "name"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListUserGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeListMemberGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := readUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -357,40 +251,21 @@ func decodeListUserGroupsRequest(_ context.Context, r *http.Request) (interface{
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := readStringQuery(r, nameKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := readMetadataQuery(r, metadataKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupID := bone.GetValue(r, "groupID")
|
||||
userID := bone.GetValue(r, "userID")
|
||||
|
||||
req := listUserGroupReq{
|
||||
req := listMemberGroupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: groupID,
|
||||
userID: userID,
|
||||
groupID: bone.GetValue(r, "groupId"),
|
||||
offset: o,
|
||||
limit: l,
|
||||
name: n,
|
||||
metadata: m,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUserGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := userGroupReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
groupID: bone.GetValue(r, "groupID"),
|
||||
userID: bone.GetValue(r, "userID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Group of users
|
||||
type Group struct {
|
||||
ID string
|
||||
Name string
|
||||
OwnerID string
|
||||
ParentID string
|
||||
Description string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// GroupRepository specifies an group persistence API.
|
||||
type GroupRepository interface {
|
||||
// Save persists the group.
|
||||
Save(ctx context.Context, g Group) (Group, error)
|
||||
|
||||
// Update updates the group data.
|
||||
Update(ctx context.Context, g Group) error
|
||||
|
||||
// Delete deletes group for given id.
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// RetrieveByID retrieves group by its unique identifier.
|
||||
RetrieveByID(ctx context.Context, id string) (Group, error)
|
||||
|
||||
// RetrieveByName retrieves group by name
|
||||
RetrieveByName(ctx context.Context, name string) (Group, error)
|
||||
|
||||
// RetrieveAllWithAncestors retrieves all groups if groupID == "", if groupID is specified returns children groups
|
||||
RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// RetrieveMemberships retrieves all groups that user belongs to
|
||||
RetrieveMemberships(ctx context.Context, userID string, offset, limit uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// Assign adds user to group.
|
||||
Assign(ctx context.Context, userID, groupID string) error
|
||||
|
||||
// Unassign removes user from group
|
||||
Unassign(ctx context.Context, userID, groupID string) error
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
var _ users.GroupRepository = (*groupRepositoryMock)(nil)
|
||||
|
||||
type groupRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
groups map[string]users.Group
|
||||
// Map of "Maps of users assigned to a group" where group is a key
|
||||
users map[string]map[string]users.User
|
||||
groupsByUser map[string]map[string]users.Group
|
||||
groupsByName map[string]users.Group
|
||||
childrenByGroups map[string]map[string]users.Group
|
||||
}
|
||||
|
||||
// NewGroupRepository creates in-memory user repository
|
||||
func NewGroupRepository() users.GroupRepository {
|
||||
return &groupRepositoryMock{
|
||||
groups: make(map[string]users.Group),
|
||||
groupsByName: make(map[string]users.Group),
|
||||
users: make(map[string]map[string]users.User),
|
||||
groupsByUser: make(map[string]map[string]users.Group),
|
||||
childrenByGroups: make(map[string]map[string]users.Group),
|
||||
}
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Save(ctx context.Context, g users.Group) (users.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[g.ID]; ok {
|
||||
return users.Group{}, users.ErrGroupConflict
|
||||
}
|
||||
if _, ok := grm.groupsByName[g.Name]; ok {
|
||||
return users.Group{}, users.ErrGroupConflict
|
||||
}
|
||||
if g.ParentID != "" {
|
||||
if _, ok := grm.groups[g.ParentID]; !ok {
|
||||
return users.Group{}, users.ErrCreateGroup
|
||||
}
|
||||
if _, ok := grm.childrenByGroups[g.ParentID]; !ok {
|
||||
grm.childrenByGroups[g.ParentID] = make(map[string]users.Group)
|
||||
}
|
||||
grm.childrenByGroups[g.ParentID][g.ID] = g
|
||||
}
|
||||
grm.groups[g.ID] = g
|
||||
grm.groupsByName[g.Name] = g
|
||||
|
||||
if _, ok := grm.groupsByUser[g.OwnerID]; !ok {
|
||||
grm.groupsByUser[g.OwnerID] = make(map[string]users.Group)
|
||||
}
|
||||
grm.groupsByUser[g.OwnerID][g.ID] = g
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[id]; !ok {
|
||||
return users.ErrNotFound
|
||||
}
|
||||
delete(grm.groups, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Unassign(ctx context.Context, userID, groupID string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return users.ErrNotFound
|
||||
}
|
||||
delete(grm.users[groupID], userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Update(ctx context.Context, g users.Group) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var group users.Group
|
||||
group, ok := grm.groups[g.ID]
|
||||
if !ok {
|
||||
return users.ErrNotFound
|
||||
}
|
||||
|
||||
group.Description = g.Description
|
||||
group.Metadata = g.Metadata
|
||||
group.ParentID = g.ParentID
|
||||
group.Name = g.Name
|
||||
group.OwnerID = g.OwnerID
|
||||
grm.groups[g.ID] = group
|
||||
grm.groupsByName[g.ID] = group
|
||||
grm.groupsByUser[g.OwnerID][g.ID] = group
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Remove(ctx context.Context, g users.Group) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[g.ID]; !ok {
|
||||
return users.ErrDeleteGroupMissing
|
||||
}
|
||||
|
||||
if _, ok := grm.groups[g.ID]; !ok {
|
||||
return users.ErrDeleteGroupMissing
|
||||
}
|
||||
|
||||
delete(grm.users, g.ID)
|
||||
delete(grm.groups, g.ID)
|
||||
delete(grm.childrenByGroups, g.ID)
|
||||
delete(grm.groupsByName, g.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
|
||||
val, ok := grm.groups[id]
|
||||
if !ok {
|
||||
return users.Group{}, users.ErrNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var val users.Group
|
||||
err := users.ErrNotFound
|
||||
|
||||
for _, g := range grm.groups {
|
||||
if g.Name == name {
|
||||
val = g
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) Assign(ctx context.Context, userID, groupID string) error {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
if _, ok := grm.groups[groupID]; !ok {
|
||||
return users.ErrNotFound
|
||||
}
|
||||
if _, ok := grm.users[groupID]; !ok {
|
||||
grm.users[groupID] = make(map[string]users.User)
|
||||
}
|
||||
if _, ok := grm.groupsByUser[userID]; !ok {
|
||||
grm.groupsByUser[userID] = make(map[string]users.Group)
|
||||
}
|
||||
|
||||
grm.users[groupID][userID] = users.User{ID: userID}
|
||||
grm.groupsByUser[userID][groupID] = users.Group{ID: groupID}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveMemberships(ctx context.Context, userID string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []users.Group
|
||||
groups, ok := grm.groupsByUser[userID]
|
||||
if !ok {
|
||||
return users.GroupPage{}, users.ErrNotFound
|
||||
}
|
||||
for _, g := range groups {
|
||||
items = append(items, g)
|
||||
}
|
||||
return users.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: users.PageMetadata{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grm *groupRepositoryMock) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
grm.mu.Lock()
|
||||
defer grm.mu.Unlock()
|
||||
var items []users.Group
|
||||
for _, g := range grm.groups {
|
||||
items = append(items, g)
|
||||
}
|
||||
return users.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: users.PageMetadata{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: uint64(len(items)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+1
-13
@@ -89,7 +89,7 @@ func (urm *userRepositoryMock) RetrieveByID(ctx context.Context, id string) (use
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (urm *userRepositoryMock) RetrieveAll(ctx context.Context, offset, limit uint64, email string, um users.Metadata) (users.UserPage, error) {
|
||||
func (urm *userRepositoryMock) RetrieveAll(ctx context.Context, offset, limit uint64, ids []string, email string, um users.Metadata) (users.UserPage, error) {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
@@ -110,18 +110,6 @@ func (urm *userRepositoryMock) RetrieveAll(ctx context.Context, offset, limit ui
|
||||
return up, nil
|
||||
}
|
||||
|
||||
func (urm *userRepositoryMock) RetrieveMembers(ctx context.Context, groupID string, offset, limit uint64, um users.Metadata) (users.UserPage, error) {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
_, ok := urm.usersByGroupID[groupID]
|
||||
if !ok {
|
||||
return users.UserPage{}, users.ErrNotFound
|
||||
}
|
||||
|
||||
return users.UserPage{}, nil
|
||||
}
|
||||
|
||||
func (urm *userRepositoryMock) UpdatePassword(_ context.Context, token, password string) error {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
+99
-138
@@ -25,8 +25,58 @@ paths:
|
||||
'415':
|
||||
description: Missing or invalid content type.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves users
|
||||
description: |
|
||||
Retrieves a list of users. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/UsersPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates info on currently logged in user.
|
||||
description: |
|
||||
Updates info on currently logged in user. Info is updated using
|
||||
authorization token and the new received info.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UserUpdateReq"
|
||||
responses:
|
||||
'200':
|
||||
description: User updated.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'404':
|
||||
description: Failed due to non existing user.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/users/profile:
|
||||
get:
|
||||
summary: Gets info on currently logged in user.
|
||||
description: |
|
||||
Gets info on currently logged in user. Info is obtained using
|
||||
@@ -44,88 +94,35 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates info on currently logged in user.
|
||||
/groups/{groupId}:
|
||||
get:
|
||||
summary: Retrieves users
|
||||
description: |
|
||||
Updates info on currently logged in user. Info is updated using
|
||||
authorization token and the new received info.
|
||||
Retrieves a list of users that belong to a group. Due to performance concerns, data
|
||||
is retrieved in subsets. The API things must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UserUpdateReq"
|
||||
responses:
|
||||
'200':
|
||||
description: User updated.
|
||||
'400':
|
||||
description: Failed due to malformed JSON.
|
||||
'404':
|
||||
description: Failed due to non existing user.
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/users/{userId}/groups:
|
||||
get:
|
||||
summary: Get groups that user belongs to
|
||||
description: Retrieves a list of groups that user belongs to.
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- Authorization: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/UserID"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/GroupId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/GroupsRes'
|
||||
'403':
|
||||
$ref: "#/components/responses/UsersPageRes"
|
||||
'400':
|
||||
description: Failed due to malformed query parameters.
|
||||
'401':
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
description: A non-existent entity request.
|
||||
'422':
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
/groups:
|
||||
post:
|
||||
summary: Create users group
|
||||
description: |
|
||||
Create users group.
|
||||
tags:
|
||||
- groups
|
||||
security:
|
||||
- Authorization: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/CreateGroupReq'
|
||||
responses:
|
||||
'200':
|
||||
description: Group created.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Group'
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
get:
|
||||
summary: Get users groups
|
||||
description: |
|
||||
Get all users groups
|
||||
tags:
|
||||
- groups
|
||||
security:
|
||||
- Authorization: []
|
||||
responses:
|
||||
'200':
|
||||
description: Groups retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupsPage'
|
||||
'403':
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/tokens:
|
||||
post:
|
||||
summary: User authentication
|
||||
@@ -256,24 +253,6 @@ components:
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
GroupReqObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Unique name of the group. Group name matching `"^[a-zA-Z0-9]+$"` regexp.
|
||||
parent_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Parent Group unique identifier.
|
||||
description:
|
||||
type: string
|
||||
description: Group description.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
required:
|
||||
- name
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
@@ -290,48 +269,15 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
Group:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: Group unique identifier.
|
||||
name:
|
||||
type: string
|
||||
example: "MainflxGroup"
|
||||
description: Group name matching `"^[a-zA-Z0-9]+$"` regexp.
|
||||
description:
|
||||
type: string
|
||||
description: Description free form text describing a group.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded group's data.
|
||||
UsersPage:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: User unique identifier.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
UserMetadata:
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
GroupsPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
things:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Group"
|
||||
$ref: "#/components/schemas/User"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
@@ -341,14 +287,29 @@ components:
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- things
|
||||
UserMetadata:
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
format: jwt
|
||||
required: true
|
||||
Referer:
|
||||
name: Referer
|
||||
description: Host being sent by browser.
|
||||
@@ -372,6 +333,14 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
GroupId:
|
||||
name: groupId
|
||||
description: Unique group identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
@@ -407,13 +376,6 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserMetadata"
|
||||
CreateGroupReq:
|
||||
description: JSON-formated document describing the new group to be created.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupReqObj'
|
||||
RequestPasswordReset:
|
||||
description: Initiate password request procedure.
|
||||
required: true
|
||||
@@ -482,12 +444,11 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
GroupsRes:
|
||||
UsersPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupsPage'
|
||||
|
||||
$ref: "#/components/schemas/UsersPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
|
||||
@@ -1,454 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
var (
|
||||
errDeleteGroupDB = errors.New("delete group failed")
|
||||
errSelectDb = errors.New("select group from db error")
|
||||
|
||||
errFK = "foreign_key_violation"
|
||||
errInvalid = "invalid_text_representation"
|
||||
errTruncation = "string_data_right_truncation"
|
||||
)
|
||||
|
||||
var _ users.GroupRepository = (*groupRepository)(nil)
|
||||
|
||||
type groupRepository struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// NewGroupRepo instantiates a PostgreSQL implementation of group
|
||||
// repository.
|
||||
func NewGroupRepo(db Database) users.GroupRepository {
|
||||
return &groupRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (gr groupRepository) Save(ctx context.Context, group users.Group) (users.Group, error) {
|
||||
var id string
|
||||
q := `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata) VALUES (:name, :description, :id, :owner_id, :parent_id, :metadata) RETURNING id`
|
||||
if group.ParentID == "" {
|
||||
q = `INSERT INTO groups (name, description, id, owner_id, metadata) VALUES (:name, :description, :id, :owner_id, :metadata) RETURNING id`
|
||||
}
|
||||
|
||||
dbu, err := toDBGroup(group)
|
||||
if err != nil {
|
||||
return users.Group{}, err
|
||||
}
|
||||
|
||||
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return users.Group{}, errors.Wrap(users.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return users.Group{}, errors.Wrap(users.ErrGroupConflict, err)
|
||||
}
|
||||
}
|
||||
|
||||
return users.Group{}, errors.Wrap(users.ErrCreateGroup, err)
|
||||
}
|
||||
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
if err := row.Scan(&id); err != nil {
|
||||
return users.Group{}, err
|
||||
}
|
||||
group.ID = id
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Update(ctx context.Context, group users.Group) error {
|
||||
q := `UPDATE groups SET name = :name, metadata = :metadata, description = :description WHERE id = :id;`
|
||||
dbu, err := toDBGroup(group)
|
||||
if err != nil {
|
||||
return errors.Wrap(users.ErrUpdateGroup, err)
|
||||
}
|
||||
|
||||
if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
|
||||
return errors.Wrap(users.ErrUpdateGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
|
||||
qd := `DELETE FROM groups WHERE id = :id`
|
||||
dbg, err := toDBGroup(users.Group{ID: groupID})
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateDB, err)
|
||||
}
|
||||
|
||||
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDeleteGroupDB, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(errDeleteGroupDB, err)
|
||||
}
|
||||
|
||||
if cnt != 1 {
|
||||
return errors.Wrap(users.ErrDeleteGroupMissing, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
|
||||
q := `SELECT id, name, owner_id, parent_id, description, metadata FROM groups WHERE id = $1`
|
||||
dbu := dbGroup{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return users.Group{}, errors.Wrap(users.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return users.Group{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
return toGroup(dbu), nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
|
||||
q := `SELECT id, name, description, metadata FROM groups WHERE name = $1`
|
||||
|
||||
dbu := dbGroup{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if err := gr.db.QueryRowxContext(ctx, q, name).StructScan(&dbu); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return users.Group{}, errors.Wrap(users.ErrNotFound, err)
|
||||
|
||||
}
|
||||
return users.Group{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
group := toGroup(dbu)
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
_, mq, err := getGroupsMetadataQuery(um)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("WHERE %s", mq)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf("SELECT COUNT(*) FROM groups %s", mq)
|
||||
sq := fmt.Sprintf("SELECT id, owner_id, parent_id, name, description, metadata FROM groups %s", mq)
|
||||
q := fmt.Sprintf("%s ORDER BY id LIMIT :limit OFFSET :offset", sq)
|
||||
|
||||
if groupID != "" {
|
||||
sq = fmt.Sprintf(
|
||||
`WITH RECURSIVE subordinates AS (
|
||||
SELECT id, owner_id, parent_id, name, description, metadata
|
||||
FROM groups
|
||||
WHERE id = :id
|
||||
UNION
|
||||
SELECT groups.id, groups.owner_id, groups.parent_id, groups.name, groups.description, groups.metadata
|
||||
FROM groups
|
||||
INNER JOIN subordinates s ON s.id = groups.parent_id %s
|
||||
)`, mq)
|
||||
q = fmt.Sprintf("%s SELECT * FROM subordinates ORDER BY id LIMIT :limit OFFSET :offset", sq)
|
||||
cq = fmt.Sprintf("%s SELECT COUNT(*) FROM subordinates", sq)
|
||||
}
|
||||
|
||||
dbPage, err := toDBGroupPage("", groupID, offset, limit, um)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []users.Group
|
||||
for rows.Next() {
|
||||
dbgr := dbGroup{}
|
||||
if err := rows.StructScan(&dbgr); err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
gr := toGroup(dbgr)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
|
||||
total, err := total(ctx, gr.db, cq, dbPage)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := users.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: users.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) RetrieveMemberships(ctx context.Context, userID string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
m, mq, err := getGroupsMetadataQuery(um)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf("AND %s", mq)
|
||||
}
|
||||
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
|
||||
FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.user_id = :userID
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"userID": userID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"metadata": m,
|
||||
}
|
||||
|
||||
rows, err := gr.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []users.Group
|
||||
for rows.Next() {
|
||||
dbgr := dbGroup{}
|
||||
if err := rows.StructScan(&dbgr); err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
gr := toGroup(dbgr)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, err
|
||||
}
|
||||
items = append(items, gr)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*)
|
||||
FROM group_relations gr, groups g
|
||||
WHERE gr.group_id = g.id and gr.user_id = :userID %s;`, mq)
|
||||
|
||||
total, err := total(ctx, gr.db, cq, params)
|
||||
if err != nil {
|
||||
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := users.GroupPage{
|
||||
Groups: items,
|
||||
PageMetadata: users.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Assign(ctx context.Context, userID, groupID string) error {
|
||||
dbr, err := toDBGroupRelation(userID, groupID)
|
||||
if err != nil {
|
||||
return errors.Wrap(users.ErrAssignUserToGroup, err)
|
||||
}
|
||||
|
||||
qIns := `INSERT INTO group_relations (group_id, user_id) VALUES (:group_id, :user_id)`
|
||||
_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return errors.Wrap(users.ErrMalformedEntity, err)
|
||||
case errDuplicate:
|
||||
return errors.Wrap(users.ErrGroupConflict, err)
|
||||
case errFK:
|
||||
return errors.Wrap(users.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(users.ErrAssignUserToGroup, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
|
||||
q := `DELETE FROM group_relations WHERE user_id = :user_id AND group_id = :group_id`
|
||||
dbr, err := toDBGroupRelation(userID, groupID)
|
||||
if err != nil {
|
||||
return errors.Wrap(users.ErrNotFound, err)
|
||||
}
|
||||
if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
|
||||
return errors.Wrap(users.ErrConflict, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbGroup struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
ParentID uuid.NullUUID `db:"parent_id"`
|
||||
Description string `db:"description"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
}
|
||||
|
||||
type dbGroupPage struct {
|
||||
ID uuid.NullUUID `db:"id"`
|
||||
OwnerID uuid.NullUUID `db:"owner_id"`
|
||||
ParentID uuid.NullUUID `db:"parent_id"`
|
||||
Metadata dbMetadata `db:"metadata"`
|
||||
Limit uint64
|
||||
Offset uint64
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func toUUID(id string) (uuid.NullUUID, error) {
|
||||
var parentID uuid.NullUUID
|
||||
if err := parentID.Scan(id); err != nil {
|
||||
if id != "" {
|
||||
return parentID, err
|
||||
}
|
||||
if err := parentID.Scan(nil); err != nil {
|
||||
return parentID, err
|
||||
}
|
||||
}
|
||||
return parentID, nil
|
||||
}
|
||||
|
||||
func toDBGroup(g users.Group) (dbGroup, error) {
|
||||
parentID := ""
|
||||
if g.ParentID != "" {
|
||||
parentID = g.ParentID
|
||||
}
|
||||
parent, err := toUUID(parentID)
|
||||
if err != nil {
|
||||
return dbGroup{}, err
|
||||
}
|
||||
owner, err := toUUID(g.OwnerID)
|
||||
if err != nil {
|
||||
return dbGroup{}, err
|
||||
}
|
||||
|
||||
return dbGroup{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
ParentID: parent,
|
||||
OwnerID: owner,
|
||||
Description: g.Description,
|
||||
Metadata: g.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toDBGroupPage(ownerID, groupID string, offset, limit uint64, um users.Metadata) (dbGroupPage, error) {
|
||||
owner, err := toUUID(ownerID)
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
}
|
||||
group, err := toUUID(groupID)
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
}
|
||||
if err != nil {
|
||||
return dbGroupPage{}, err
|
||||
}
|
||||
return dbGroupPage{
|
||||
ID: group,
|
||||
Metadata: dbMetadata(um),
|
||||
OwnerID: owner,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGroup(dbu dbGroup) users.Group {
|
||||
return users.Group{
|
||||
ID: dbu.ID,
|
||||
Name: dbu.Name,
|
||||
ParentID: dbu.ParentID.UUID.String(),
|
||||
OwnerID: dbu.OwnerID.UUID.String(),
|
||||
Description: dbu.Description,
|
||||
Metadata: dbu.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
type dbGroupRelation struct {
|
||||
Group uuid.UUID `db:"group_id"`
|
||||
User uuid.UUID `db:"user_id"`
|
||||
}
|
||||
|
||||
func toDBGroupRelation(userID, groupID string) (dbGroupRelation, error) {
|
||||
group, err := uuid.FromString(groupID)
|
||||
if err != nil {
|
||||
return dbGroupRelation{}, err
|
||||
}
|
||||
user, err := uuid.FromString(userID)
|
||||
if err != nil {
|
||||
return dbGroupRelation{}, err
|
||||
}
|
||||
return dbGroupRelation{
|
||||
Group: group,
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getGroupsMetadataQuery(um users.Metadata) ([]byte, string, error) {
|
||||
mq := ""
|
||||
mb := []byte("{}")
|
||||
if len(um) > 0 {
|
||||
mq = `groups.metadata @> :metadata`
|
||||
|
||||
b, err := json.Marshal(um)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
mb = b
|
||||
}
|
||||
return mb, mq, nil
|
||||
}
|
||||
|
||||
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
|
||||
rows, err := db.NamedQueryContext(ctx, query, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
total := uint64(0)
|
||||
if rows.Next() {
|
||||
if err := rows.Scan(&total); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/users/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNameSize = 254
|
||||
maxDescSize = 1024
|
||||
groupName = "Mainflux"
|
||||
password = "12345678"
|
||||
)
|
||||
|
||||
var (
|
||||
invalidName = strings.Repeat("m", maxNameSize+1)
|
||||
invalidDesc = strings.Repeat("m", maxDescSize+1)
|
||||
)
|
||||
|
||||
func TestGroupSave(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.NewGroupRepo(dbMiddleware)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("user id unexpected error: %s", err))
|
||||
user := users.User{
|
||||
ID: uid,
|
||||
Email: "TestGroupSave@mainflux.com",
|
||||
Password: password,
|
||||
}
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
|
||||
|
||||
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
|
||||
|
||||
uid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group := users.Group{
|
||||
ID: uid,
|
||||
Name: "TestGroupSave",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "create new group",
|
||||
group: group,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group that already exist",
|
||||
group: group,
|
||||
err: users.ErrGroupConflict,
|
||||
},
|
||||
{
|
||||
desc: "create thing with invalid name",
|
||||
group: users.Group{
|
||||
Name: "x^%",
|
||||
},
|
||||
err: users.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(context.Background(), tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupRetrieveByID(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.NewGroupRepo(dbMiddleware)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
user := users.User{
|
||||
ID: uid,
|
||||
Email: "TestGroupRetrieveByID@mainflux.com",
|
||||
Password: password,
|
||||
}
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
|
||||
|
||||
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
|
||||
|
||||
gid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "TestGroupRetrieveByID1",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
gid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group2 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "TestGroupRetrieveByID2",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
g1, err := repo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
g2, err := repo.Save(context.Background(), group2)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
g2.ID, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("failed to generate id error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve group for valid id",
|
||||
group: g1,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve group for invalid id",
|
||||
group: g2,
|
||||
err: users.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(context.Background(), tc.group.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupUpdate(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
|
||||
gid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName,
|
||||
}
|
||||
|
||||
_, err = groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update group for existing id",
|
||||
group: users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "-1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update group for non-existing id",
|
||||
group: users.Group{
|
||||
ID: "wrong",
|
||||
Name: groupName + "-2",
|
||||
},
|
||||
err: users.ErrUpdateGroup,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid name",
|
||||
group: users.Group{
|
||||
ID: gid,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: users.ErrUpdateGroup,
|
||||
},
|
||||
{
|
||||
desc: "update group for invalid description",
|
||||
group: users.Group{
|
||||
ID: gid,
|
||||
Description: invalidDesc,
|
||||
},
|
||||
err: users.ErrUpdateGroup,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := groupRepo.Update(context.Background(), tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupDelete(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.NewGroupRepo(dbMiddleware)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
user := users.User{
|
||||
ID: uid,
|
||||
Email: "TestGroupDelete@mainflux.com",
|
||||
Password: password,
|
||||
}
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
|
||||
|
||||
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
|
||||
|
||||
gid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "TestGroupDelete1",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
g1, err := repo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
err = repo.Assign(context.Background(), user.ID, g1.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to assign user to a group: %s", err))
|
||||
|
||||
gid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group2 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "TestGroupDelete2",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
g2, err := repo.Save(context.Background(), group2)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "delete group for existing id",
|
||||
group: g2,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "delete group for non-existing id",
|
||||
group: g2,
|
||||
err: users.ErrDeleteGroupMissing,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Delete(context.Background(), tc.group.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignUser(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.NewGroupRepo(dbMiddleware)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
user := users.User{
|
||||
ID: uid,
|
||||
Email: "TestAssignUser@mainflux.com",
|
||||
Password: password,
|
||||
}
|
||||
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
|
||||
|
||||
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
|
||||
|
||||
gid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "TestAssignUser1",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
g1, err := repo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
gid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group2 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "TestAssignUser2",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
g2, err := repo.Save(context.Background(), group2)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
gid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id generating error: %s", err))
|
||||
g3 := users.Group{
|
||||
ID: gid,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "assign user to existing group",
|
||||
group: g1,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "assign user to another existing group",
|
||||
group: g2,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "assign user to non existing group",
|
||||
group: g3,
|
||||
err: users.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Assign(context.Background(), user.ID, tc.group.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUnassignUser(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
repo := postgres.NewGroupRepo(dbMiddleware)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
user := users.User{
|
||||
ID: uid,
|
||||
Email: "UnassignUser1@mainflux.com",
|
||||
Password: password,
|
||||
}
|
||||
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
|
||||
|
||||
user1, err := userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
|
||||
|
||||
uid, err = idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
user = users.User{
|
||||
ID: uid,
|
||||
Email: "UnassignUser2@mainflux.com",
|
||||
Password: password,
|
||||
}
|
||||
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
|
||||
|
||||
user2, err := userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
|
||||
|
||||
gid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
|
||||
group1 := users.Group{
|
||||
ID: gid,
|
||||
Name: groupName + "UnassignUser1",
|
||||
OwnerID: user.ID,
|
||||
}
|
||||
|
||||
g1, err := repo.Save(context.Background(), group1)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
err = repo.Assign(context.Background(), user1.ID, group1.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("failed to assign user: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
user users.User
|
||||
err error
|
||||
}{
|
||||
{desc: "remove user from a group", group: g1, user: user1, err: nil},
|
||||
{desc: "remove already removed user from a group", group: g1, user: user1, err: nil},
|
||||
{desc: "remove non existing user from a group", group: g1, user: user2, err: nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Unassign(context.Background(), tc.user.ID, tc.group.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -76,24 +76,6 @@ func migrateDB(db *sqlx.DB) error {
|
||||
`ALTER TABLE IF EXISTS users DROP CONSTRAINT users_pkey`,
|
||||
`ALTER TABLE IF EXISTS users ADD CONSTRAINT users_email_key UNIQUE (email)`,
|
||||
`ALTER TABLE IF EXISTS users ADD PRIMARY KEY (id)`,
|
||||
`CREATE TABLE IF NOT EXISTS groups (
|
||||
id UUID NOT NULL,
|
||||
parent_id UUID,
|
||||
owner_id UUID,
|
||||
name VARCHAR(254) UNIQUE NOT NULL,
|
||||
description VARCHAR(1024),
|
||||
metadata JSONB,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS group_relations (
|
||||
user_id UUID NOT NULL,
|
||||
group_id UUID NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (user_id, group_id)
|
||||
)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+40
-76
@@ -9,15 +9,23 @@ import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/auth"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
const (
|
||||
errInvalid = "invalid_text_representation"
|
||||
errTruncation = "string_data_right_truncation"
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveUserDB = errors.New("Save user to DB failed")
|
||||
errUpdateDB = errors.New("Update user email to DB failed")
|
||||
errSelectDb = errors.New("Select from DB failed")
|
||||
errUpdateUserDB = errors.New("Update user metadata to DB failed")
|
||||
errRetrieveDB = errors.New("Retreiving from DB failed")
|
||||
errUpdatePasswordDB = errors.New("Update password to DB failed")
|
||||
@@ -141,7 +149,7 @@ func (ur userRepository) RetrieveByID(ctx context.Context, id string) (users.Use
|
||||
return toUser(dbu)
|
||||
}
|
||||
|
||||
func (ur userRepository) RetrieveAll(ctx context.Context, offset, limit uint64, email string, um users.Metadata) (users.UserPage, error) {
|
||||
func (ur userRepository) RetrieveAll(ctx context.Context, offset, limit uint64, userIDs []string, email string, um users.Metadata) (users.UserPage, error) {
|
||||
eq, ep, err := createEmailQuery("", email)
|
||||
if err != nil {
|
||||
return users.UserPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
@@ -152,19 +160,22 @@ func (ur userRepository) RetrieveAll(ctx context.Context, offset, limit uint64,
|
||||
return users.UserPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
emq := ""
|
||||
if eq != "" && mq == "" {
|
||||
emq = fmt.Sprintf("WHERE %s", eq)
|
||||
var query []string
|
||||
var emq string
|
||||
if eq != "" {
|
||||
query = append(query, eq)
|
||||
}
|
||||
if eq == "" && mq != "" {
|
||||
emq = fmt.Sprintf("WHERE %s", mq)
|
||||
if mq != "" {
|
||||
query = append(query, mq)
|
||||
}
|
||||
if eq != "" && mq != "" {
|
||||
emq = fmt.Sprintf("WHERE %s AND %s", eq, mq)
|
||||
if len(userIDs) > 0 {
|
||||
query = append(query, fmt.Sprintf("id IN ('%s')", strings.Join(userIDs, "','")))
|
||||
}
|
||||
if len(query) > 0 {
|
||||
emq = fmt.Sprintf(" WHERE %s", strings.Join(query, " AND "))
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, email, metadata FROM users %s ORDER BY email LIMIT :limit OFFSET :offset;`, emq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
@@ -227,68 +238,6 @@ func (ur userRepository) UpdatePassword(ctx context.Context, email, password str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur userRepository) RetrieveMembers(ctx context.Context, groupID string, offset, limit uint64, um users.Metadata) (users.UserPage, error) {
|
||||
mq, mp, err := createMetadataQuery("users.", um)
|
||||
if err != nil {
|
||||
return users.UserPage{}, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
|
||||
if mq != "" {
|
||||
mq = fmt.Sprintf(" AND %s", mq)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT u.id, u.email, u.metadata FROM users u, group_relations g
|
||||
WHERE u.id = g.user_id AND g.group_id = :group
|
||||
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"group": groupID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"metadata": mp,
|
||||
}
|
||||
|
||||
rows, err := ur.db.NamedQueryContext(ctx, q, params)
|
||||
if err != nil {
|
||||
return users.UserPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []users.User
|
||||
for rows.Next() {
|
||||
dbusr := dbUser{}
|
||||
if err := rows.StructScan(&dbusr); err != nil {
|
||||
return users.UserPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
user, err := toUser(dbusr)
|
||||
if err != nil {
|
||||
return users.UserPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, user)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) FROM users u, group_relations g
|
||||
WHERE u.id = g.user_id AND g.group_id = :group %s;`, mq)
|
||||
|
||||
total, err := total(ctx, ur.db, cq, params)
|
||||
if err != nil {
|
||||
return users.UserPage{}, errors.Wrap(errSelectDb, err)
|
||||
}
|
||||
|
||||
page := users.UserPage{
|
||||
Users: items,
|
||||
PageMetadata: users.PageMetadata{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
},
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// dbMetadata type for handling metadata properly in database/sql
|
||||
type dbMetadata map[string]interface{}
|
||||
|
||||
@@ -324,11 +273,11 @@ func (m dbMetadata) Value() (driver.Value, error) {
|
||||
}
|
||||
|
||||
type dbUser struct {
|
||||
ID string `db:"id"`
|
||||
Email string `db:"email"`
|
||||
Password string `db:"password"`
|
||||
Metadata []byte `db:"metadata"`
|
||||
Groups []users.Group `db:"groups"`
|
||||
ID string `db:"id"`
|
||||
Email string `db:"email"`
|
||||
Password string `db:"password"`
|
||||
Metadata []byte `db:"metadata"`
|
||||
Groups []auth.Group `db:"groups"`
|
||||
}
|
||||
|
||||
func toDBUser(u users.User) (dbUser, error) {
|
||||
@@ -349,6 +298,21 @@ func toDBUser(u users.User) (dbUser, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
|
||||
rows, err := db.NamedQueryContext(ctx, query, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
total := uint64(0)
|
||||
if rows.Next() {
|
||||
if err := rows.Scan(&total); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func toUser(dbu dbUser) (users.User, error) {
|
||||
var metadata map[string]interface{}
|
||||
if dbu.Metadata != nil {
|
||||
|
||||
@@ -90,72 +90,21 @@ func TestSingleUserRetrieval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveMembers(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
groupRepo := postgres.NewGroupRepo(dbMiddleware)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
var nUsers = uint64(10)
|
||||
var usrs []users.User
|
||||
for i := uint64(0); i < nUsers; i++ {
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
email := fmt.Sprintf("TestRetrieveMembers%d@example.com", i)
|
||||
user := users.User{
|
||||
ID: uid,
|
||||
Email: email,
|
||||
Password: "pass",
|
||||
}
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("saving user error: %s", err))
|
||||
u, _ := userRepo.RetrieveByEmail(context.Background(), user.Email)
|
||||
usrs = append(usrs, u)
|
||||
}
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("user uuid error: %s", err))
|
||||
group := users.Group{
|
||||
ID: uid,
|
||||
Name: "TestMembers",
|
||||
}
|
||||
|
||||
g, err := groupRepo.Save(context.Background(), group)
|
||||
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
|
||||
|
||||
for _, u := range usrs {
|
||||
err := groupRepo.Assign(context.Background(), u.ID, g.ID)
|
||||
require.Nil(t, err, fmt.Sprintf("group user assign got unexpected error: %s", err))
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
group string
|
||||
offset uint64
|
||||
limit uint64
|
||||
size uint64
|
||||
total uint64
|
||||
metadata users.Metadata
|
||||
}{
|
||||
"retrieve all users for existing group": {
|
||||
group: g.ID,
|
||||
offset: 0,
|
||||
limit: nUsers,
|
||||
size: nUsers,
|
||||
total: nUsers,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := userRepo.RetrieveMembers(context.Background(), tc.group, tc.offset, tc.limit, tc.metadata)
|
||||
size := uint64(len(usrs))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
dbMiddleware := postgres.NewDatabase(db)
|
||||
userRepo := postgres.NewUserRepo(dbMiddleware)
|
||||
metaNum := uint64(2)
|
||||
var nUsers = uint64(10)
|
||||
|
||||
meta := users.Metadata{
|
||||
"admin": "true",
|
||||
}
|
||||
|
||||
wrongMeta := users.Metadata{
|
||||
"wrong": "true",
|
||||
}
|
||||
|
||||
var ids []string
|
||||
for i := uint64(0); i < nUsers; i++ {
|
||||
uid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
@@ -165,6 +114,10 @@ func TestRetrieveAll(t *testing.T) {
|
||||
Email: email,
|
||||
Password: "pass",
|
||||
}
|
||||
if i < metaNum {
|
||||
user.Metadata = meta
|
||||
}
|
||||
ids = append(ids, uid)
|
||||
_, err = userRepo.Save(context.Background(), user)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
}
|
||||
@@ -175,6 +128,7 @@ func TestRetrieveAll(t *testing.T) {
|
||||
limit uint64
|
||||
size uint64
|
||||
total uint64
|
||||
ids []string
|
||||
metadata users.Metadata
|
||||
}{
|
||||
"retrieve all users filtered by email": {
|
||||
@@ -191,10 +145,60 @@ func TestRetrieveAll(t *testing.T) {
|
||||
size: 5,
|
||||
total: nUsers,
|
||||
},
|
||||
"retrieve all users by metadata": {
|
||||
email: "All",
|
||||
offset: 0,
|
||||
limit: nUsers,
|
||||
size: metaNum,
|
||||
total: nUsers,
|
||||
metadata: meta,
|
||||
},
|
||||
"retrieve users by metadata and ids": {
|
||||
email: "All",
|
||||
offset: 0,
|
||||
limit: nUsers,
|
||||
size: 1,
|
||||
total: nUsers,
|
||||
metadata: meta,
|
||||
ids: []string{ids[0]},
|
||||
},
|
||||
"retrieve users by wrong metadata": {
|
||||
email: "All",
|
||||
offset: 0,
|
||||
limit: nUsers,
|
||||
size: 0,
|
||||
total: nUsers,
|
||||
metadata: wrongMeta,
|
||||
},
|
||||
"retrieve users by wrong metadata and ids": {
|
||||
email: "All",
|
||||
offset: 0,
|
||||
limit: nUsers,
|
||||
size: 0,
|
||||
total: nUsers,
|
||||
metadata: wrongMeta,
|
||||
ids: []string{ids[0]},
|
||||
},
|
||||
"retrieve all users by list of ids with limit and offset": {
|
||||
email: "All",
|
||||
offset: 2,
|
||||
limit: 5,
|
||||
size: 5,
|
||||
total: nUsers,
|
||||
ids: ids,
|
||||
},
|
||||
"retrieve all users by list of ids with limit and offset and metadata": {
|
||||
email: "All",
|
||||
offset: 1,
|
||||
limit: 5,
|
||||
size: 1,
|
||||
total: nUsers,
|
||||
ids: ids[0:5],
|
||||
metadata: meta,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := userRepo.RetrieveAll(context.Background(), tc.offset, tc.limit, tc.email, tc.metadata)
|
||||
page, err := userRepo.RetrieveAll(context.Background(), tc.offset, tc.limit, tc.ids, tc.email, tc.metadata)
|
||||
size := uint64(len(page.Users))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
|
||||
+27
-117
@@ -13,8 +13,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
groupRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$")
|
||||
|
||||
// ErrConflict indicates usage of the existing email during account
|
||||
// registration.
|
||||
ErrConflict = errors.New("email already taken")
|
||||
@@ -55,18 +53,6 @@ var (
|
||||
// ErrCreateUser indicates error in creating user.
|
||||
ErrCreateUser = errors.New("failed to create user")
|
||||
|
||||
// ErrCreateGroup indicates error in creating group.
|
||||
ErrCreateGroup = errors.New("failed to create group")
|
||||
|
||||
// ErrUpdateGroup indicates error in updating group.
|
||||
ErrUpdateGroup = errors.New("failed to update group")
|
||||
|
||||
// ErrDeleteGroupMissing indicates in delete operation that group doesnt exist.
|
||||
ErrDeleteGroupMissing = errors.New("group is not existing, already deleted")
|
||||
|
||||
// ErrAssignUserToGroup indicates an error in assigning user to a group.
|
||||
ErrAssignUserToGroup = errors.New("failed assigning user to a group")
|
||||
|
||||
// ErrPasswordFormat indicates weak password.
|
||||
ErrPasswordFormat = errors.New("password does not meet the requirements")
|
||||
)
|
||||
@@ -90,7 +76,7 @@ type Service interface {
|
||||
ViewProfile(ctx context.Context, token string) (User, error)
|
||||
|
||||
// ListUsers retrieves users list for a valid admin token.
|
||||
ListUsers(ctx context.Context, token string, offset, limit uint64, email string, m Metadata) (UserPage, error)
|
||||
ListUsers(ctx context.Context, token string, offset, limit uint64, email string, meta Metadata) (UserPage, error)
|
||||
|
||||
// UpdateUser updates the user metadata.
|
||||
UpdateUser(ctx context.Context, token string, user User) error
|
||||
@@ -109,33 +95,8 @@ type Service interface {
|
||||
//SendPasswordReset sends reset password link to email.
|
||||
SendPasswordReset(ctx context.Context, host, email, token string) error
|
||||
|
||||
// CreateGroup creates new user group.
|
||||
CreateGroup(ctx context.Context, token string, group Group) (Group, error)
|
||||
|
||||
// UpdateGroup updates the group identified by the provided ID.
|
||||
UpdateGroup(ctx context.Context, token string, group Group) error
|
||||
|
||||
// ViewGroup retrieves data about the group identified by ID.
|
||||
ViewGroup(ctx context.Context, token, id string) (Group, error)
|
||||
|
||||
// ListGroups retrieves groups that are children to group identified by parenID
|
||||
// if parentID is empty all groups are listed.
|
||||
ListGroups(ctx context.Context, token, parentID string, offset, limit uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// Members retrieves users that are assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, m Metadata) (UserPage, error)
|
||||
|
||||
// ListMemberships retrieves groups that user identified with userID belongs to.
|
||||
ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, m Metadata) (GroupPage, error)
|
||||
|
||||
// RemoveGroup removes the group identified with the provided ID.
|
||||
RemoveGroup(ctx context.Context, token, id string) error
|
||||
|
||||
// Assign adds user with userID into the group identified by groupID.
|
||||
Assign(ctx context.Context, token, userID, groupID string) error
|
||||
|
||||
// Unassign removes user with userID from group identified by groupID.
|
||||
Unassign(ctx context.Context, token, userID, groupID string) error
|
||||
// ListMembers retrieves everything that is assigned to a group identified by groupID.
|
||||
ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, meta Metadata) (UserPage, error)
|
||||
}
|
||||
|
||||
// PageMetadata contains page metadata that helps navigation.
|
||||
@@ -149,7 +110,7 @@ type PageMetadata struct {
|
||||
// GroupPage contains a page of groups.
|
||||
type GroupPage struct {
|
||||
PageMetadata
|
||||
Groups []Group
|
||||
Groups []auth.Group
|
||||
}
|
||||
|
||||
// UserPage contains a page of users.
|
||||
@@ -162,7 +123,6 @@ var _ Service = (*usersService)(nil)
|
||||
|
||||
type usersService struct {
|
||||
users UserRepository
|
||||
groups GroupRepository
|
||||
hasher Hasher
|
||||
email Emailer
|
||||
auth mainflux.AuthServiceClient
|
||||
@@ -171,13 +131,12 @@ type usersService struct {
|
||||
}
|
||||
|
||||
// New instantiates the users service implementation
|
||||
func New(users UserRepository, groups GroupRepository, hasher Hasher, auth mainflux.AuthServiceClient, m Emailer, idp mainflux.IDProvider, passRegex *regexp.Regexp) Service {
|
||||
func New(users UserRepository, hasher Hasher, auth mainflux.AuthServiceClient, e Emailer, idp mainflux.IDProvider, passRegex *regexp.Regexp) Service {
|
||||
return &usersService{
|
||||
users: users,
|
||||
groups: groups,
|
||||
hasher: hasher,
|
||||
auth: auth,
|
||||
email: m,
|
||||
email: e,
|
||||
idProvider: idp,
|
||||
passRegex: passRegex,
|
||||
}
|
||||
@@ -262,7 +221,7 @@ func (svc usersService) ListUsers(ctx context.Context, token string, offset, lim
|
||||
return UserPage{}, err
|
||||
}
|
||||
|
||||
return svc.users.RetrieveAll(ctx, offset, limit, email, m)
|
||||
return svc.users.RetrieveAll(ctx, offset, limit, nil, email, m)
|
||||
}
|
||||
|
||||
func (svc usersService) UpdateUser(ctx context.Context, token string, u User) error {
|
||||
@@ -340,82 +299,17 @@ func (svc usersService) SendPasswordReset(_ context.Context, host, email, token
|
||||
return svc.email.SendPasswordReset(to, host, token)
|
||||
}
|
||||
|
||||
func (svc usersService) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
||||
if group.Name == "" || !groupRegexp.MatchString(group.Name) {
|
||||
return Group{}, ErrMalformedEntity
|
||||
}
|
||||
|
||||
identity, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
|
||||
uid, err := svc.idProvider.ID()
|
||||
if err != nil {
|
||||
return Group{}, errors.Wrap(ErrCreateUser, err)
|
||||
}
|
||||
|
||||
group.ID = uid
|
||||
group.OwnerID = identity.GetId()
|
||||
|
||||
return svc.groups.Save(ctx, group)
|
||||
}
|
||||
|
||||
func (svc usersService) ListGroups(ctx context.Context, token string, parentID string, offset, limit uint64, m Metadata) (GroupPage, error) {
|
||||
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
return svc.groups.RetrieveAllWithAncestors(ctx, parentID, offset, limit, m)
|
||||
}
|
||||
|
||||
func (svc usersService) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, m Metadata) (UserPage, error) {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return UserPage{}, err
|
||||
}
|
||||
return svc.users.RetrieveMembers(ctx, groupID, offset, limit, m)
|
||||
}
|
||||
|
||||
func (svc usersService) RemoveGroup(ctx context.Context, token, id string) error {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return err
|
||||
userIDs, err := svc.members(ctx, token, groupID, offset, limit)
|
||||
if err != nil {
|
||||
return UserPage{}, err
|
||||
}
|
||||
return svc.groups.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc usersService) Unassign(ctx context.Context, token, userID, groupID string) error {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.groups.Unassign(ctx, userID, groupID)
|
||||
}
|
||||
|
||||
func (svc usersService) UpdateGroup(ctx context.Context, token string, group Group) error {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.groups.Update(ctx, group)
|
||||
}
|
||||
|
||||
func (svc usersService) ViewGroup(ctx context.Context, token, id string) (Group, error) {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return Group{}, err
|
||||
}
|
||||
return svc.groups.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (svc usersService) Assign(ctx context.Context, token, userID, groupID string) error {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.groups.Assign(ctx, userID, groupID)
|
||||
}
|
||||
|
||||
func (svc usersService) ListMemberships(ctx context.Context, token, userID string, offset, limit uint64, m Metadata) (GroupPage, error) {
|
||||
if _, err := svc.identify(ctx, token); err != nil {
|
||||
return GroupPage{}, err
|
||||
}
|
||||
return svc.groups.RetrieveMemberships(ctx, userID, offset, limit, m)
|
||||
return svc.users.RetrieveAll(ctx, offset, limit, userIDs, "", m)
|
||||
}
|
||||
|
||||
// Auth helpers
|
||||
@@ -434,3 +328,19 @@ func (svc usersService) identify(ctx context.Context, token string) (string, err
|
||||
}
|
||||
return identity.GetEmail(), nil
|
||||
}
|
||||
|
||||
func (svc usersService) members(ctx context.Context, token, groupID string, limit, offset uint64) ([]string, error) {
|
||||
req := mainflux.MembersReq{
|
||||
Token: token,
|
||||
GroupID: groupID,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Type: "users",
|
||||
}
|
||||
|
||||
res, err := svc.auth.Members(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Members, nil
|
||||
}
|
||||
|
||||
+1
-131
@@ -25,7 +25,6 @@ var (
|
||||
user = users.User{Email: "user@example.com", Password: "password", Metadata: map[string]interface{}{"role": "user"}}
|
||||
nonExistingUser = users.User{Email: "non-ex-user@example.com", Password: "password", Metadata: map[string]interface{}{"role": "user"}}
|
||||
host = "example.com"
|
||||
groupName = "Mainflux"
|
||||
|
||||
idProvider = uuid.New()
|
||||
passRegex = regexp.MustCompile("^.{8,}$")
|
||||
@@ -33,12 +32,11 @@ var (
|
||||
|
||||
func newService() users.Service {
|
||||
userRepo := mocks.NewUserRepository()
|
||||
groupRepo := mocks.NewGroupRepository()
|
||||
hasher := mocks.NewHasher()
|
||||
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
|
||||
e := mocks.NewEmailer()
|
||||
|
||||
return users.New(userRepo, groupRepo, hasher, auth, e, idProvider, passRegex)
|
||||
return users.New(userRepo, hasher, auth, e, idProvider, passRegex)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
@@ -369,131 +367,3 @@ func TestSendPasswordReset(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
_, err := svc.Register(context.Background(), user)
|
||||
assert.Nil(t, err, fmt.Sprintf("registering user expected to succeed: %s", err))
|
||||
|
||||
token, err := svc.Login(context.Background(), user)
|
||||
assert.Nil(t, err, fmt.Sprintf("authenticating user expected to succeed: %s", err))
|
||||
|
||||
id, err := idProvider.ID()
|
||||
assert.Nil(t, err, fmt.Sprintf("generating uuid expected to succeed: %s", err))
|
||||
|
||||
group := users.Group{
|
||||
ID: id,
|
||||
Name: groupName,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "create new group",
|
||||
group: group,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create group with existing name",
|
||||
group: group,
|
||||
err: users.ErrGroupConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.CreateGroup(context.Background(), token, tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
_, err := svc.Register(context.Background(), user)
|
||||
assert.Nil(t, err, fmt.Sprintf("registering user expected to succeed: %s", err))
|
||||
|
||||
token, err := svc.Login(context.Background(), user)
|
||||
assert.Nil(t, err, fmt.Sprintf("authenticating user expected to succeed: %s", err))
|
||||
|
||||
group := users.Group{
|
||||
Name: groupName,
|
||||
}
|
||||
|
||||
saved, err := svc.CreateGroup(context.Background(), token, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("generating uuid expected to succeed: %s", err))
|
||||
|
||||
group.Description = "test description"
|
||||
group.Name = "NewName"
|
||||
group.ID = saved.ID
|
||||
group.OwnerID = saved.OwnerID
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update group",
|
||||
group: group,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateGroup(context.Background(), token, tc.group)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
g, err := svc.ViewGroup(context.Background(), token, saved.ID)
|
||||
assert.Nil(t, err, fmt.Sprintf("retrieve group failed: %s", err))
|
||||
assert.Equal(t, tc.group.Description, g.Description, tc.desc, tc.err)
|
||||
assert.Equal(t, tc.group.Name, g.Name, tc.desc, tc.err)
|
||||
assert.Equal(t, tc.group.ID, g.ID, tc.desc, tc.err)
|
||||
assert.Equal(t, tc.group.OwnerID, g.OwnerID, tc.desc, tc.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveGroup(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
_, err := svc.Register(context.Background(), user)
|
||||
assert.Nil(t, err, fmt.Sprintf("registering user expected to succeed: %s", err))
|
||||
|
||||
token, err := svc.Login(context.Background(), user)
|
||||
assert.Nil(t, err, fmt.Sprintf("authenticating user expected to succeed: %s", err))
|
||||
|
||||
group := users.Group{
|
||||
Name: groupName,
|
||||
}
|
||||
|
||||
saved, err := svc.CreateGroup(context.Background(), token, group)
|
||||
assert.Nil(t, err, fmt.Sprintf("generating uuid expected to succeed: %s", err))
|
||||
|
||||
group.Description = "test description"
|
||||
group.Name = "NewName"
|
||||
group.ID = saved.ID
|
||||
group.OwnerID = saved.OwnerID
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
group users.Group
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove existing group",
|
||||
group: group,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove non existing group",
|
||||
group: group,
|
||||
err: users.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveGroup(context.Background(), token, tc.group.ID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing contains middlewares that will add spans
|
||||
// to existing traces.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux/users"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
assignUser = "assign_user"
|
||||
saveGroup = "save_group"
|
||||
deleteGroup = "delete_group"
|
||||
updateGroup = "update_group"
|
||||
retrieveGroupByID = "retrieve_group_by_id"
|
||||
retrieveAll = "retrieve_all_groups"
|
||||
retrieveByName = "retrieve_by_name"
|
||||
memberships = "memberships"
|
||||
unassignUser = "unassign_user"
|
||||
)
|
||||
|
||||
var _ users.GroupRepository = (*groupRepositoryMiddleware)(nil)
|
||||
|
||||
type groupRepositoryMiddleware struct {
|
||||
tracer opentracing.Tracer
|
||||
repo users.GroupRepository
|
||||
}
|
||||
|
||||
// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
|
||||
func GroupRepositoryMiddleware(repo users.GroupRepository, tracer opentracing.Tracer) users.GroupRepository {
|
||||
return groupRepositoryMiddleware{
|
||||
tracer: tracer,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
func (grm groupRepositoryMiddleware) Save(ctx context.Context, group users.Group) (users.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, saveGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Save(ctx, group)
|
||||
}
|
||||
func (grm groupRepositoryMiddleware) Update(ctx context.Context, group users.Group) error {
|
||||
span := createSpan(ctx, grm.tracer, updateGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Update(ctx, group)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, deleteGroup)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Delete(ctx, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveGroupByID)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveByID(ctx, id)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveByName)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveByName(ctx, name)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, retrieveAll)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveAllWithAncestors(ctx, groupID, offset, limit, um)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) RetrieveMemberships(ctx context.Context, userID string, offset, limit uint64, um users.Metadata) (users.GroupPage, error) {
|
||||
span := createSpan(ctx, grm.tracer, memberships)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.RetrieveMemberships(ctx, userID, offset, limit, um)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, userID, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, unassignUser)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Unassign(ctx, userID, groupID)
|
||||
}
|
||||
|
||||
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, userID, groupID string) error {
|
||||
span := createSpan(ctx, grm.tracer, assignUser)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return grm.repo.Assign(ctx, userID, groupID)
|
||||
}
|
||||
+2
-10
@@ -75,20 +75,12 @@ func (urm userRepositoryMiddleware) UpdatePassword(ctx context.Context, email, p
|
||||
return urm.repo.UpdatePassword(ctx, email, password)
|
||||
}
|
||||
|
||||
func (urm userRepositoryMiddleware) RetrieveAll(ctx context.Context, offset, limit uint64, email string, um users.Metadata) (users.UserPage, error) {
|
||||
func (urm userRepositoryMiddleware) RetrieveAll(ctx context.Context, offset, limit uint64, ids []string, email string, um users.Metadata) (users.UserPage, error) {
|
||||
span := createSpan(ctx, urm.tracer, members)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return urm.repo.RetrieveAll(ctx, offset, limit, email, um)
|
||||
}
|
||||
|
||||
func (urm userRepositoryMiddleware) RetrieveMembers(ctx context.Context, groupID string, offset, limit uint64, um users.Metadata) (users.UserPage, error) {
|
||||
span := createSpan(ctx, urm.tracer, members)
|
||||
defer span.Finish()
|
||||
ctx = opentracing.ContextWithSpan(ctx, span)
|
||||
|
||||
return urm.repo.RetrieveMembers(ctx, groupID, offset, limit, um)
|
||||
return urm.repo.RetrieveAll(ctx, offset, limit, ids, email, um)
|
||||
}
|
||||
|
||||
func createSpan(ctx context.Context, tracer opentracing.Tracer, opName string) opentracing.Span {
|
||||
|
||||
+2
-5
@@ -63,14 +63,11 @@ type UserRepository interface {
|
||||
// RetrieveByID retrieves user by its unique identifier ID.
|
||||
RetrieveByID(ctx context.Context, id string) (User, error)
|
||||
|
||||
// RetrieveAll retrieves all users
|
||||
RetrieveAll(ctx context.Context, offset, limit uint64, email string, m Metadata) (UserPage, error)
|
||||
// RetrieveAll retrieves all users for given array of userIDs.
|
||||
RetrieveAll(ctx context.Context, offset, limit uint64, userIDs []string, email string, m Metadata) (UserPage, error)
|
||||
|
||||
// UpdatePassword updates password for user with given email
|
||||
UpdatePassword(ctx context.Context, email, password string) error
|
||||
|
||||
// RetrieveMembers retrieves all users that belong to a group
|
||||
RetrieveMembers(ctx context.Context, groupID string, offset, limit uint64, m Metadata) (UserPage, error)
|
||||
}
|
||||
|
||||
func isEmail(email string) bool {
|
||||
|
||||
Reference in New Issue
Block a user