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:
Mirko Teodorovic
2021-03-04 10:29:03 +01:00
committed by GitHub
parent 6b1f4d54f8
commit 530f925c4d
91 changed files with 4536 additions and 5278 deletions
+18 -1
View File
@@ -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
View File
@@ -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
}
+9 -13
View File
@@ -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
}
}
+86 -4
View File
@@ -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")
}
}
+10 -9
View File
@@ -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
View File
@@ -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
}
+3
View File
@@ -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,
+146
View File
@@ -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"`
}
+400
View File
@@ -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"
+120
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,177 @@
package auth
import (
"context"
"errors"
"time"
)
const MaxLevel = uint64(5)
const MinLevel = uint64(1)
var (
// ErrMaxLevelExceeded malformed entity.
ErrMaxLevelExceeded = errors.New("level must be less than or equal 5")
// ErrBadGroupName malformed entity.
ErrBadGroupName = errors.New("incorrect group name")
// ErrGroupConflict group conflict.
ErrGroupConflict = errors.New("group already exists")
// ErrCreateGroup indicates failure to create group.
ErrCreateGroup = errors.New("failed to create group")
// ErrFetchGroups indicates failure to fetch groups.
ErrFetchGroups = errors.New("failed to fetch groups")
// ErrUpdateGroup indicates failure to update group.
ErrUpdateGroup = errors.New("failed to update group")
// ErrDeleteGroup indicates failure to delete group.
ErrDeleteGroup = errors.New("failed to delete group")
// ErrGroupNotFound indicates failure to find group.
ErrGroupNotFound = errors.New("failed to find group")
// ErrAssignToGroup indicates failure to assign member to a group.
ErrAssignToGroup = errors.New("failed to assign member to a group")
// ErrUnassignFromGroup indicates failure to unassign member from a group.
ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
ErrUnsupportedContentType = errors.New("unsupported content type")
// ErrFailedDecode indicates failed to decode request body
ErrFailedDecode = errors.New("failed to decode request body")
// ErrMissingParent indicates that parent can't be found
ErrMissingParent = errors.New("failed to retrieve parent")
// ErrGroupNotEmpty indicates group is not empty, can't be deleted.
ErrGroupNotEmpty = errors.New("group is not empty")
// ErrMemberAlreadyAssigned indicates that members is already assigned.
ErrMemberAlreadyAssigned = errors.New("member is already assigned")
// ErrSelectEntity indicates error while reading entity from database
ErrSelectEntity = errors.New("select entity from db error")
)
type GroupMetadata map[string]interface{}
type Member struct {
ID string
Type string
}
type Group struct {
ID string
OwnerID string
ParentID string
Name string
Description string
Metadata GroupMetadata
// Indicates a level in tree hierarchy.
// Root node is level 1.
Level int
// Path in a tree consisting of group ids
// parentID1.parentID2.childID1
// e.g. 01EXPM5Z8HRGFAEWTETR1X1441.01EXPKW2TVK74S5NWQ979VJ4PJ.01EXPKW2TVK74S5NWQ979VJ4PJ
Path string
Children []*Group
CreatedAt time.Time
UpdatedAt time.Time
}
type PageMetadata struct {
Total uint64
Offset uint64
Limit uint64
Size uint64
Level uint64
Name string
Type string
Metadata GroupMetadata
}
type GroupPage struct {
PageMetadata
Groups []Group
}
type MemberPage struct {
PageMetadata
Members []Member
}
type GroupService interface {
// CreateGroup creates new group.
CreateGroup(ctx context.Context, token string, g Group) (Group, error)
// UpdateGroup updates the group identified by the provided ID.
UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
// ViewGroup retrieves data about the group identified by ID.
ViewGroup(ctx context.Context, token, id string) (Group, error)
// ListGroups retrieves groups.
ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error)
// ListChildren retrieves groups that are children to group identified by parentID
ListChildren(ctx context.Context, token, parentID string, pm PageMetadata) (GroupPage, error)
// ListParents retrieves groups that are parent to group identified by childID.
ListParents(ctx context.Context, token, childID string, pm PageMetadata) (GroupPage, error)
// ListMembers retrieves everything that is assigned to a group identified by groupID.
ListMembers(ctx context.Context, token, groupID, groupType string, pm PageMetadata) (MemberPage, error)
// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
ListMemberships(ctx context.Context, token, memberID string, pm PageMetadata) (GroupPage, error)
// RemoveGroup removes the group identified with the provided ID.
RemoveGroup(ctx context.Context, token, id string) error
// Assign adds a member with memberID into the group identified by groupID.
Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) error
// Unassign removes member with memberID from group identified by groupID.
Unassign(ctx context.Context, token, groupID string, memberIDs ...string) error
}
type GroupRepository interface {
// Save group
Save(ctx context.Context, g Group) (Group, error)
// Update a group
Update(ctx context.Context, g Group) (Group, error)
// Delete a group
Delete(ctx context.Context, id string) error
// RetrieveByID retrieves group by its id
RetrieveByID(ctx context.Context, id string) (Group, error)
// RetrieveAll retrieves all groups.
RetrieveAll(ctx context.Context, pm PageMetadata) (GroupPage, error)
// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
RetrieveAllParents(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
RetrieveAllChildren(ctx context.Context, groupID string, pm PageMetadata) (GroupPage, error)
// Retrieves list of groups that member belongs to
Memberships(ctx context.Context, memberID string, pm PageMetadata) (GroupPage, error)
// Members retrieves everything that is assigned to a group identified by groupID.
Members(ctx context.Context, groupID, groupType string, pm PageMetadata) (MemberPage, error)
// Assign adds a member to group.
Assign(ctx context.Context, groupID, groupType string, memberIDs ...string) error
// Unassign removes a member from a group
Unassign(ctx context.Context, groupID string, memberIDs ...string) error
}
+199 -89
View File
@@ -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
View File
@@ -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
View File
@@ -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
+777
View File
@@ -0,0 +1,777 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/auth/postgres"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
maxNameSize = 254
maxDescSize = 1024
groupName = "Mainflux"
description = "description"
)
var (
invalidName = strings.Repeat("m", maxNameSize+1)
invalidDesc = strings.Repeat("m", maxDescSize+1)
metadata = auth.GroupMetadata{
"admin": "true",
}
)
func generateGroupID(t *testing.T) string {
grpID, err := ulidProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
return grpID
}
func TestGroupSave(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
usrID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := ulidProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
grpID := generateGroupID(t)
cases := []struct {
desc string
group auth.Group
err error
}{
{
desc: "create new group",
group: auth.Group{
ID: grpID,
OwnerID: usrID,
Name: groupName,
},
err: nil,
},
{
desc: "create new group with existing name",
group: auth.Group{
ID: grpID,
OwnerID: usrID,
Name: groupName,
},
err: auth.ErrGroupConflict,
},
{
desc: "create group with invalid name",
group: auth.Group{
ID: generateGroupID(t),
OwnerID: usrID,
Name: invalidName,
},
err: auth.ErrMalformedEntity,
},
{
desc: "create group with invalid description",
group: auth.Group{
ID: generateGroupID(t),
OwnerID: usrID,
Name: groupName,
Description: invalidDesc,
},
err: auth.ErrMalformedEntity,
},
{
desc: "create group with parent",
group: auth.Group{
ID: generateGroupID(t),
ParentID: grpID,
OwnerID: usrID,
Name: "withParent",
},
err: nil,
},
{
desc: "create group with parent and existing name",
group: auth.Group{
ID: generateGroupID(t),
ParentID: grpID,
OwnerID: usrID,
Name: groupName,
},
err: nil,
},
{
desc: "create group with wrong parent",
group: auth.Group{
ID: generateGroupID(t),
ParentID: wrongID,
OwnerID: usrID,
Name: "wrongParent",
},
err: auth.ErrCreateGroup,
},
}
for _, tc := range cases {
_, err := groupRepo.Save(context.Background(), tc.group)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestGroupRetrieveByID(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group1 := auth.Group{
ID: generateGroupID(t),
Name: groupName + "TestGroupRetrieveByID1",
OwnerID: uid,
}
_, err = groupRepo.Save(context.Background(), group1)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
retrieved, err := groupRepo.RetrieveByID(context.Background(), group1.ID)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.True(t, retrieved.ID == group1.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group1.ID, retrieved.ID))
// Round to milliseconds as otherwise saving and retriving from DB
// adds rounding error.
creationTime := time.Now().UTC().Round(time.Millisecond)
group2 := auth.Group{
ID: generateGroupID(t),
Name: groupName + "TestGroupRetrieveByID",
OwnerID: uid,
ParentID: group1.ID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
Description: description,
Metadata: metadata,
}
_, err = groupRepo.Save(context.Background(), group2)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
retrieved, err = groupRepo.RetrieveByID(context.Background(), group2.ID)
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.True(t, retrieved.ID == group2.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group2.ID, retrieved.ID))
assert.True(t, retrieved.CreatedAt.Equal(creationTime), fmt.Sprintf("Save group, CreatedAt: expected %s got %s\n", creationTime, retrieved.CreatedAt))
assert.True(t, retrieved.UpdatedAt.Equal(creationTime), fmt.Sprintf("Save group, UpdatedAt: expected %s got %s\n", creationTime, retrieved.UpdatedAt))
assert.True(t, retrieved.Level == 2, fmt.Sprintf("Save group, Level: expected %d got %d\n", retrieved.Level, 2))
assert.True(t, retrieved.ParentID == group1.ID, fmt.Sprintf("Save group, Level: expected %s got %s\n", group1.ID, retrieved.ParentID))
assert.True(t, retrieved.Description == description, fmt.Sprintf("Save group, Description: expected %v got %v\n", retrieved.Description, description))
assert.True(t, retrieved.Path == fmt.Sprintf("%s.%s", group1.ID, group2.ID), fmt.Sprintf("Save group, Path: expected %s got %s\n", fmt.Sprintf("%s.%s", group1.ID, group2.ID), retrieved.Path))
retrieved, err = groupRepo.RetrieveByID(context.Background(), generateGroupID(t))
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", auth.ErrGroupNotFound, err))
}
func TestGroupUpdate(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
updateTime := time.Now().UTC()
groupID := generateGroupID(t)
group := auth.Group{
ID: groupID,
Name: groupName + "TestGroupUpdate",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
Description: description,
Metadata: metadata,
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
retrieved, err := groupRepo.RetrieveByID(context.Background(), group.ID)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
cases := []struct {
desc string
groupUpdate auth.Group
groupExpected auth.Group
err error
}{
{
desc: "update group for existing id",
groupUpdate: auth.Group{
ID: groupID,
Name: groupName + "Updated",
UpdatedAt: updateTime,
Metadata: auth.GroupMetadata{"admin": "false"},
},
groupExpected: auth.Group{
Name: groupName + "Updated",
UpdatedAt: updateTime,
Metadata: auth.GroupMetadata{"admin": "false"},
CreatedAt: retrieved.CreatedAt,
Path: retrieved.Path,
ParentID: retrieved.ParentID,
ID: retrieved.ID,
Level: retrieved.Level,
},
err: nil,
},
{
desc: "update group for non-existing id",
groupUpdate: auth.Group{
ID: "wrong",
Name: groupName + "-2",
},
err: auth.ErrUpdateGroup,
},
{
desc: "update group for invalid name",
groupUpdate: auth.Group{
ID: groupID,
Name: invalidName,
},
err: auth.ErrMalformedEntity,
},
{
desc: "update group for invalid description",
groupUpdate: auth.Group{
ID: groupID,
Description: invalidDesc,
},
err: auth.ErrMalformedEntity,
},
}
for _, tc := range cases {
updated, err := groupRepo.Update(context.Background(), tc.groupUpdate)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if tc.desc == "update group for existing id" {
assert.True(t, updated.Level == tc.groupExpected.Level, fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Level, updated.Level))
assert.True(t, updated.Name == tc.groupExpected.Name, fmt.Sprintf("%s:Name: expected %s got %s\n", tc.desc, tc.groupExpected.Name, updated.Name))
assert.True(t, updated.Metadata["admin"] == tc.groupExpected.Metadata["admin"], fmt.Sprintf("%s:Level: expected %d got %d\n", tc.desc, tc.groupExpected.Metadata["admin"], updated.Metadata["admin"]))
}
}
}
func TestGroupDelete(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
groupParent := auth.Group{
ID: generateGroupID(t),
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
groupParent, err = groupRepo.Save(context.Background(), groupParent)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
creationTime = time.Now().UTC()
groupChild1 := auth.Group{
ID: generateGroupID(t),
ParentID: groupParent.ID,
Name: groupName + "child1",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
creationTime = time.Now().UTC()
groupChild2 := auth.Group{
ID: generateGroupID(t),
ParentID: groupParent.ID,
Name: groupName + "child2",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
meta := auth.PageMetadata{
Level: auth.MaxLevel,
}
groupChild1, err = groupRepo.Save(context.Background(), groupChild1)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
groupChild2, err = groupRepo.Save(context.Background(), groupChild2)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
gp, err := groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("Retrieve children for parent: expected %v got %v\n", nil, err))
assert.True(t, gp.Total == 3, fmt.Sprintf("Number of children + parent: expected %d got %d\n", 3, gp.Total))
thingID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("thing id create unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), groupChild1.ID, "things", thingID)
require.Nil(t, err, fmt.Sprintf("thing assign got unexpected error: %s", err))
err = groupRepo.Delete(context.Background(), groupChild1.ID)
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete non empty group: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
err = groupRepo.Delete(context.Background(), groupChild2.ID)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete empty group: expected %v got %v\n", nil, err))
err = groupRepo.Delete(context.Background(), groupParent.ID)
assert.True(t, errors.Contains(err, auth.ErrGroupNotEmpty), fmt.Sprintf("delete parent with children with members: expected %v got %v\n", auth.ErrGroupNotEmpty, err))
gp, err = groupRepo.RetrieveAllChildren(context.Background(), groupParent.ID, meta)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("retrieve children after one child removed: expected %v got %v\n", nil, err))
assert.True(t, gp.Total == 2, fmt.Sprintf("number of children + parent: expected %d got %d\n", 2, gp.Total))
err = groupRepo.Unassign(context.Background(), groupChild1.ID, thingID)
require.Nil(t, err, fmt.Sprintf("failed to remove thing from a group error: %s", err))
err = groupRepo.Delete(context.Background(), groupParent.ID)
assert.True(t, errors.Contains(err, nil), fmt.Sprintf("delete parent with children with no members: expected %v got %v\n", nil, err))
_, err = groupRepo.RetrieveByID(context.Background(), groupChild1.ID)
assert.True(t, errors.Contains(err, auth.ErrGroupNotFound), fmt.Sprintf("retrieve child after parent removed: expected %v got %v\n", nil, err))
}
func TestRetrieveAll(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
metadata := auth.PageMetadata{
Metadata: auth.GroupMetadata{
"field": "value",
},
Level: auth.MaxLevel,
}
wrongMeta := auth.PageMetadata{
Metadata: auth.GroupMetadata{
"wrong": "wrong",
},
Level: auth.MaxLevel,
}
metaNum := uint64(3)
n := uint64(auth.MaxLevel)
parentID := ""
for i := uint64(0); i < n; i++ {
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: fmt.Sprintf("%s-%d", groupName, i),
OwnerID: uid,
ParentID: parentID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
// Create Groups with metadata.
if i < metaNum {
group.Metadata = metadata.Metadata
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = group.ID
}
cases := map[string]struct {
Size uint64
Metadata auth.PageMetadata
}{
"retrieve all groups": {
Metadata: auth.PageMetadata{
Total: n,
Limit: n,
Level: auth.MaxLevel,
},
Size: n,
},
"retrieve groups with existing metadata": {
Metadata: auth.PageMetadata{
Total: metaNum,
Limit: n,
Level: auth.MaxLevel,
Metadata: metadata.Metadata,
},
Size: metaNum,
},
"retrieve groups with non-existing metadata": {
Metadata: auth.PageMetadata{
Total: uint64(0),
Limit: n,
Level: auth.MaxLevel,
Metadata: wrongMeta.Metadata,
},
Size: uint64(0),
},
"retrieve groups with hierarchy level depth": {
Metadata: auth.PageMetadata{
Total: uint64(metaNum),
Limit: n,
Level: auth.MaxLevel,
Metadata: metadata.Metadata,
},
Size: uint64(metaNum),
},
"retrieve groups with hierarchy level depth and existing metadata": {
Metadata: auth.PageMetadata{
Total: uint64(metaNum),
Limit: n,
Level: auth.MaxLevel,
Metadata: metadata.Metadata,
},
Size: uint64(metaNum),
},
}
for desc, tc := range cases {
page, err := groupRepo.RetrieveAll(context.Background(), tc.Metadata)
size := len(page.Groups)
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
assert.Equal(t, tc.Metadata.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Metadata.Total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
func TestRetrieveAllParents(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
metadata := auth.GroupMetadata{
"field": "value",
}
wrongMeta := auth.GroupMetadata{
"wrong": "wrong",
}
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Equal(t, uint64(0), p.Total, fmt.Sprintf("expected total %d got %d\n", 0, p.Total))
metaNum := uint64(3)
n := uint64(10)
parentID := ""
parentMiddle := ""
for i := uint64(0); i < n; i++ {
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: fmt.Sprintf("%s-%d", groupName, i),
OwnerID: uid,
ParentID: parentID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
// Create Groups with metadata.
if n-i <= metaNum {
group.Metadata = metadata
}
if i == n/2 {
parentMiddle = group.ID
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = group.ID
}
cases := map[string]struct {
level uint64
parentID string
Size uint64
Total uint64
Metadata auth.GroupMetadata
}{
"retrieve all parents": {
Total: n,
Size: auth.MaxLevel + 1,
level: auth.MaxLevel,
parentID: parentID,
},
"retrieve groups with existing metadata": {
Total: metaNum,
Size: metaNum,
Metadata: metadata,
parentID: parentID,
level: auth.MaxLevel,
},
"retrieve groups with non-existing metadata": {
Total: uint64(0),
Metadata: wrongMeta,
Size: uint64(0),
level: auth.MaxLevel,
parentID: parentID,
},
"retrieve groups with hierarchy level depth": {
Total: n,
Size: 2 + 1,
level: uint64(2),
parentID: parentID,
},
"retrieve groups with hierarchy level depth and existing metadata": {
Total: metaNum,
Size: metaNum,
level: 3,
Metadata: metadata,
parentID: parentID,
},
"retrieve parent groups from children in the middle": {
Total: n/2 + 1,
Size: n/2 + 1,
level: auth.MaxLevel,
parentID: parentMiddle,
},
}
for desc, tc := range cases {
page, err := groupRepo.RetrieveAllParents(context.Background(), tc.parentID, auth.PageMetadata{Level: tc.level, Metadata: tc.Metadata})
size := len(page.Groups)
assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size))
assert.Equal(t, tc.Total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.Total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
func TestRetrieveAllChildren(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
metadata := auth.GroupMetadata{
"field": "value",
}
wrongMeta := auth.GroupMetadata{
"wrong": "wrong",
}
metaNum := uint64(3)
n := uint64(10)
groupID := generateGroupID(t)
firstParentID := groupID
parentID := ""
parentMiddle := ""
for i := uint64(0); i < n; i++ {
creationTime := time.Now().UTC()
group := auth.Group{
ID: groupID,
Name: fmt.Sprintf("%s-%d", groupName, i),
OwnerID: uid,
ParentID: parentID,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
// Create Groups with metadata.
if i < metaNum {
group.Metadata = metadata
}
if i == n/2 {
parentMiddle = group.ID
}
_, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err))
parentID = group.ID
groupID = generateGroupID(t)
}
p, err := groupRepo.RetrieveAll(context.Background(), auth.PageMetadata{Level: auth.MaxLevel})
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Equal(t, n, p.Total, fmt.Sprintf("expected total %d got %d\n", n, p.Total))
cases := map[string]struct {
parentID string
size uint64
total uint64
metadata auth.PageMetadata
}{
"retrieve all children": {
size: auth.MaxLevel,
total: n,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
},
parentID: firstParentID,
},
"retrieve groups with existing metadata": {
size: metaNum,
total: metaNum,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
Metadata: metadata,
},
parentID: firstParentID,
},
"retrieve groups with non-existing metadata": {
total: 0,
size: 0,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
Metadata: wrongMeta,
},
parentID: firstParentID,
},
"retrieve groups with hierarchy level depth": {
total: n,
size: 2,
metadata: auth.PageMetadata{
Level: 2,
},
parentID: firstParentID,
},
"retrieve groups with hierarchy level depth and existing metadata": {
total: metaNum,
size: metaNum,
metadata: auth.PageMetadata{
Level: 3,
Metadata: metadata,
},
parentID: firstParentID,
},
"retrieve parent groups from children in the middle": {
total: n / 2,
size: n / 2,
metadata: auth.PageMetadata{
Level: auth.MaxLevel,
},
parentID: parentMiddle,
},
}
for desc, tc := range cases {
page, err := groupRepo.RetrieveAllChildren(context.Background(), tc.parentID, tc.metadata)
size := len(page.Groups)
assert.Equal(t, tc.size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
func TestAssign(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
pm := auth.PageMetadata{
Offset: 0,
Limit: 10,
}
group, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
mid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
assert.True(t, errors.Contains(err, auth.ErrMemberAlreadyAssigned), fmt.Sprintf("assign member again: expected %v got %v\n", auth.ErrMemberAlreadyAssigned, err))
}
func TestUnassign(t *testing.T) {
t.Cleanup(func() { cleanUp(t) })
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
uid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
creationTime := time.Now().UTC()
group := auth.Group{
ID: generateGroupID(t),
Name: groupName + "Updated",
OwnerID: uid,
CreatedAt: creationTime,
UpdatedAt: creationTime,
}
pm := auth.PageMetadata{
Offset: 0,
Limit: 10,
}
group, err = groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
mid, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
mid, err = idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
err = groupRepo.Assign(context.Background(), group.ID, "things", mid)
require.Nil(t, err, fmt.Sprintf("member assign unexpected error: %s", err))
mp, err := groupRepo.Members(context.Background(), group.ID, "things", pm)
require.Nil(t, err, fmt.Sprintf("member assign save unexpected error: %s", err))
assert.True(t, mp.Total == 2, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 2, mp.Total))
err = groupRepo.Unassign(context.Background(), group.ID, mid)
require.Nil(t, err, fmt.Sprintf("member unassign save unexpected error: %s", err))
mp, err = groupRepo.Members(context.Background(), group.ID, "things", pm)
require.Nil(t, err, fmt.Sprintf("members retrieve unexpected error: %s", err))
assert.True(t, mp.Total == 1, fmt.Sprintf("retrieve members of a group: expected %d got %d\n", 1, mp.Total))
}
func cleanUp(t *testing.T) {
_, err := db.Exec("delete from group_relations")
require.Nil(t, err, fmt.Sprintf("clean relations unexpected error: %s", err))
_, err = db.Exec("delete from groups")
require.Nil(t, err, fmt.Sprintf("clean groups unexpected error: %s", err))
}
+17 -23
View File
@@ -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`,
},
},
},
+4 -2
View File
@@ -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) {
-2
View File
@@ -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) {
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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...)
}
+1 -42
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+21 -1
View File
@@ -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};
+22 -1
View File
@@ -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};
-138
View File
@@ -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
}
-227
View File
@@ -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
}
-53
View File
@@ -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")
)
-116
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+1 -2
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+1 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+39
View File
@@ -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
}
+1 -6
View File
@@ -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"`
+38 -4
View File
@@ -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
}
+33 -76
View File
@@ -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),
+39
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -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
+8 -4
View File
@@ -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))
-601
View File
@@ -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
}
-22
View File
@@ -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);`,
},
},
},
+1 -4
View File
@@ -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) {
+67 -5
View File
@@ -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)
+21 -4
View File
@@ -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{
-1
View File
@@ -14,7 +14,6 @@ import (
)
const (
wrongID = 0
wrongValue = "wrong-value"
)
+2 -43
View File
@@ -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)
}
+15 -15
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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)
-1
View File
@@ -11,7 +11,6 @@ import (
)
const (
saveChannelOp = "save_channel"
saveChannelsOp = "save_channels"
updateChannelOp = "update_channel"
retrieveChannelByIDOp = "retrieve_channel_by_id"
-130
View File
@@ -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)
}
+8
View File
@@ -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
View File
@@ -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{
+1 -68
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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() {
-48
View File
@@ -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
}
-205
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-454
View File
@@ -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
}
-413
View File
@@ -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))
}
}
-18
View File
@@ -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
View File
@@ -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 {
+67 -63
View File
@@ -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
View File
@@ -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
View File
@@ -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))
}
}
-110
View File
@@ -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
View File
@@ -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
View File
@@ -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 {