diff --git a/auth/api/http/groups/endpoint.go b/auth/api/http/groups/endpoint.go index c7e65eb9..abeeb59c 100644 --- a/auth/api/http/groups/endpoint.go +++ b/auth/api/http/groups/endpoint.go @@ -210,7 +210,7 @@ func assignEndpoint(svc auth.Service) endpoint.Endpoint { func unassignEndpoint(svc auth.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignReq) + req := request.(unassignReq) if err := req.validate(); err != nil { return nil, err } diff --git a/auth/api/http/groups/requests.go b/auth/api/http/groups/requests.go index b4046d0f..56f4decc 100644 --- a/auth/api/http/groups/requests.go +++ b/auth/api/http/groups/requests.go @@ -128,6 +128,22 @@ func (req assignReq) validate() error { return nil } +type unassignReq struct { + assignReq +} + +func (req unassignReq) validate() error { + if req.token == "" { + return auth.ErrUnauthorizedAccess + } + + if req.groupID == "" || len(req.Members) == 0 { + return auth.ErrMalformedEntity + } + + return nil +} + type groupReq struct { token string id string diff --git a/auth/api/http/groups/transport.go b/auth/api/http/groups/transport.go index a34db010..d5d67f44 100644 --- a/auth/api/http/groups/transport.go +++ b/auth/api/http/groups/transport.go @@ -99,7 +99,7 @@ func MakeHandler(svc auth.Service, mux *bone.Mux, tracer opentracing.Tracer) *bo mux.Delete("/groups/:groupID/members", kithttp.NewServer( kitot.TraceServer(tracer, "unassign")(unassignEndpoint(svc)), - decodeAssignRequest, + decodeUnassignRequest, encodeResponse, opts..., )) @@ -282,6 +282,21 @@ func decodeAssignRequest(_ context.Context, r *http.Request) (interface{}, error return req, nil } +func decodeUnassignRequest(_ context.Context, r *http.Request) (interface{}, error) { + req := unassignReq{ + 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 encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { w.Header().Set("Content-Type", contentType) diff --git a/auth/api/logging.go b/auth/api/logging.go index a572b24b..89f64de9 100644 --- a/auth/api/logging.go +++ b/auth/api/logging.go @@ -199,9 +199,9 @@ func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID, gr return lm.svc.ListMembers(ctx, token, groupID, groupType, pm) } -func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, groupID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) { +func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) { defer func(begin time.Time) { - message := fmt.Sprintf("Method list_memberships for token %s and group id %s took %s to complete", token, groupID, time.Since(begin)) + message := fmt.Sprintf("Method list_memberships for token %s and member id %s took %s to complete", token, memberID, time.Since(begin)) if err != nil { lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err)) return @@ -209,7 +209,7 @@ 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, pm) + return lm.svc.ListMemberships(ctx, token, memberID, pm) } func (lm *loggingMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) { diff --git a/auth/api/metrics.go b/auth/api/metrics.go index ee3ed1d2..38b2def0 100644 --- a/auth/api/metrics.go +++ b/auth/api/metrics.go @@ -142,13 +142,13 @@ func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID, gr return ms.svc.ListMembers(ctx, token, groupID, groupType, pm) } -func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, groupID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) { +func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, memberID string, pm auth.PageMetadata) (gp auth.GroupPage, err error) { defer func(begin time.Time) { ms.counter.With("method", "list_memberships").Add(1) ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.ListMemberships(ctx, token, groupID, pm) + return ms.svc.ListMemberships(ctx, token, memberID, pm) } func (ms *metricsMiddleware) Assign(ctx context.Context, token, groupID, groupType string, memberIDs ...string) (err error) { diff --git a/auth/postgres/groups.go b/auth/postgres/groups.go index a5c3872e..37675968 100644 --- a/auth/postgres/groups.go +++ b/auth/postgres/groups.go @@ -56,7 +56,7 @@ func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, e RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at` } - dbg, err := gr.toDBGroup(g) + dbg, err := toDBGroup(g) if err != nil { return auth.Group{}, err } @@ -92,7 +92,7 @@ func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, 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) + dbu, err := toDBGroup(g) if err != nil { return auth.Group{}, errors.Wrap(auth.ErrUpdateGroup, err) } @@ -126,7 +126,7 @@ func (gr groupRepository) Delete(ctx context.Context, groupID string) error { group := auth.Group{ ID: groupID, } - dbg, err := gr.toDBGroup(group) + dbg, err := toDBGroup(group) if err != nil { return errors.Wrap(auth.ErrUpdateGroup, err) } @@ -315,7 +315,7 @@ func (gr groupRepository) Members(ctx context.Context, groupID, groupType string WHERE gr.group_id = :group_id %s`, mq) } - params, err := gr.toDBMemberPage("", groupID, groupType, pm) + params, err := toDBMemberPage("", groupID, groupType, pm) if err != nil { return auth.MemberPage{}, err } @@ -375,7 +375,7 @@ func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm a WHERE gr.group_id = g.id and gr.member_id = :member_id %s ORDER BY id LIMIT :limit OFFSET :offset;`, mq) - params, err := gr.toDBMemberPage("", "", "", pm) + params, err := toDBMemberPage(memberID, "", "", pm) if err != nil { return auth.GroupPage{}, err } @@ -562,7 +562,7 @@ func toString(id uuid.NullUUID) (string, error) { return "", errStringToUUID } -func (gr groupRepository) toDBGroup(g auth.Group) (dbGroup, error) { +func toDBGroup(g auth.Group) (dbGroup, error) { ownerID, err := toUUID(g.OwnerID) if err != nil { return dbGroup{}, err @@ -604,7 +604,7 @@ func toDBGroupPage(id, path string, pm auth.PageMetadata) (dbGroupPage, error) { }, nil } -func (gr groupRepository) toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) { +func toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) { return dbMemberPage{ GroupID: groupID, MemberID: memberID, diff --git a/cli/groups.go b/cli/groups.go index 2a2e4ac8..747936a4 100644 --- a/cli/groups.go +++ b/cli/groups.go @@ -45,7 +45,7 @@ var cmdGroups = []cobra.Command{ }, cobra.Command{ Use: "get", - Short: "get [all | children | group_id] ", + Short: "get [all | children | parents | group_id] ", Long: `Get all users groups, group children or group by id. all - lists all groups children - lists all children groups of @@ -56,7 +56,11 @@ var cmdGroups = []cobra.Command{ return } if args[0] == "all" { - l, err := sdk.Groups(args[1], uint64(Offset), uint64(Limit), "") + if len(args) > 2 { + logUsage(cmd.Short) + return + } + l, err := sdk.Groups(uint64(Offset), uint64(Limit), args[1]) if err != nil { logError(err) return @@ -65,7 +69,11 @@ var cmdGroups = []cobra.Command{ return } if args[0] == "children" { - l, err := sdk.Groups(args[2], uint64(Offset), uint64(Limit), args[1]) + if len(args) > 3 { + logUsage(cmd.Short) + return + } + l, err := sdk.Children(args[1], uint64(Offset), uint64(Limit), args[2]) if err != nil { logError(err) return @@ -73,6 +81,23 @@ var cmdGroups = []cobra.Command{ logJSON(l) return } + if args[0] == "parents" { + if len(args) > 3 { + logUsage(cmd.Short) + return + } + l, err := sdk.Parents(args[1], uint64(Offset), uint64(Limit), args[2]) + if err != nil { + logError(err) + return + } + logJSON(l) + return + } + if len(args) > 2 { + logUsage(cmd.Short) + return + } t, err := sdk.Group(args[0], args[1]) if err != nil { logError(err) @@ -83,14 +108,20 @@ var cmdGroups = []cobra.Command{ }, cobra.Command{ Use: "assign", - Short: "assign ", - Long: `Assign user to a group.`, + Short: "assign ", + Long: `Assign members to a group. + member_ids - '["member_id",...]`, Run: func(cmd *cobra.Command, args []string) { - if len(args) != 3 { + if len(args) != 4 { logUsage(cmd.Short) return } - if err := sdk.Assign(args[0], args[1], args[2]); err != nil { + var ids []string + if err := json.Unmarshal([]byte(args[0]), &ids); err != nil { + logError(err) + return + } + if err := sdk.Assign(ids, args[1], args[2], args[3]); err != nil { logError(err) return } @@ -99,14 +130,20 @@ var cmdGroups = []cobra.Command{ }, cobra.Command{ Use: "unassign", - Short: "unassign ", - Long: `Unassign user from a group.`, + Short: "unassign ", + Long: `Unassign members from a group + member_ids - '["member_id",...]`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 3 { logUsage(cmd.Short) return } - if err := sdk.Unassign(args[0], args[1], args[2]); err != nil { + var ids []string + if err := json.Unmarshal([]byte(args[0]), &ids); err != nil { + logError(err) + return + } + if err := sdk.Unassign(args[2], args[1], ids...); err != nil { logError(err) return } @@ -116,7 +153,7 @@ var cmdGroups = []cobra.Command{ cobra.Command{ Use: "delete", Short: "delete ", - Long: `Delete users group.`, + Long: `Delete group.`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 2 { logUsage(cmd.Short) @@ -132,7 +169,7 @@ var cmdGroups = []cobra.Command{ cobra.Command{ Use: "members", Short: "members ", - Long: `Lists all user members of a group.`, + Long: `Lists all members of a group.`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 2 { logUsage(cmd.Short) @@ -148,8 +185,8 @@ var cmdGroups = []cobra.Command{ }, cobra.Command{ Use: "membership", - Short: "membership ", - Long: `List user groups membership`, + Short: "membership ", + Long: `List member group's membership`, Run: func(cmd *cobra.Command, args []string) { if len(args) != 2 { logUsage(cmd.Short) @@ -170,7 +207,7 @@ func NewGroupsCmd() *cobra.Command { cmd := cobra.Command{ Use: "groups", Short: "Groups management", - Long: `Groups management: create groups and assigns user to groups"`, + Long: `Groups management: create groups and assigns member to groups"`, Run: func(cmd *cobra.Command, args []string) { logUsage("groups [create | get | delete | assign | unassign | members | membership]") }, diff --git a/pkg/sdk/go/groups.go b/pkg/sdk/go/groups.go index 77396061..6ea499e7 100644 --- a/pkg/sdk/go/groups.go +++ b/pkg/sdk/go/groups.go @@ -11,11 +11,17 @@ import ( "net/http" "strings" + "github.com/mainflux/mainflux/auth" "github.com/mainflux/mainflux/pkg/errors" ) const groupsEndpoint = "groups" +type assignRequest struct { + Type string `json:"type,omitempty"` + Members []string `json:"members"` +} + func (sdk mfSDK) CreateGroup(g Group, token string) (string, error) { data, err := json.Marshal(g) if err != nil { @@ -65,10 +71,23 @@ func (sdk mfSDK) DeleteGroup(id, token string) error { return nil } -func (sdk mfSDK) Assign(userID, groupID, token string) error { - endpoint := fmt.Sprintf("%s/%s/users/%s", groupsEndpoint, groupID, userID) +func (sdk mfSDK) Assign(memberIDs []string, memberType, groupID string, token string) error { + var ids []string + endpoint := fmt.Sprintf("%s/%s/members", groupsEndpoint, groupID) url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) - req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader([]byte{})) + + ids = append(ids, memberIDs...) + assignReq := assignRequest{ + Type: memberType, + Members: ids, + } + + data, err := json.Marshal(assignReq) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) if err != nil { return err } @@ -78,21 +97,33 @@ func (sdk mfSDK) Assign(userID, groupID, token string) error { return err } - if resp.StatusCode != http.StatusNoContent { - return errors.Wrap(ErrFailedUserAdd, errors.New(resp.Status)) + if resp.StatusCode != http.StatusOK { + return errors.Wrap(ErrMemberAdd, errors.New(resp.Status)) } return nil } -func (sdk mfSDK) Unassign(userID, groupID, token string) error { - endpoint := fmt.Sprintf("%s/%s/users/%s", groupsEndpoint, groupID, userID) +func (sdk mfSDK) Unassign(token, groupID string, memberIDs ...string) error { + var ids []string + endpoint := fmt.Sprintf("%s/%s/members", groupsEndpoint, groupID) url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) - req, err := http.NewRequest(http.MethodDelete, url, bytes.NewReader([]byte{})) + ids = append(ids, memberIDs...) + assignReq := assignRequest{ + Members: ids, + } + + data, err := json.Marshal(assignReq) if err != nil { return err } + + req, err := http.NewRequest(http.MethodDelete, url, bytes.NewReader(data)) + if err != nil { + return err + } + resp, err := sdk.sendRequest(req, token, string(CTJSON)) if err != nil { return err @@ -105,69 +136,80 @@ func (sdk mfSDK) Unassign(userID, groupID, token string) error { return nil } -func (sdk mfSDK) Members(groupID, token string, offset, limit uint64) (UsersPage, error) { - endpoint := fmt.Sprintf("%s/%s/users?offset=%d&limit=%d&", groupsEndpoint, groupID, offset, limit) +func (sdk mfSDK) Members(groupID, token string, offset, limit uint64) (auth.MemberPage, error) { + endpoint := fmt.Sprintf("%s/%s/members?offset=%d&limit=%d&", groupsEndpoint, groupID, offset, limit) url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - return UsersPage{}, err + return auth.MemberPage{}, err } resp, err := sdk.sendRequest(req, token, string(CTJSON)) if err != nil { - return UsersPage{}, err + return auth.MemberPage{}, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return UsersPage{}, err + return auth.MemberPage{}, err } if resp.StatusCode != http.StatusOK { - return UsersPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status)) + return auth.MemberPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status)) } - var tp UsersPage + var tp auth.MemberPage if err := json.Unmarshal(body, &tp); err != nil { - return UsersPage{}, err + return auth.MemberPage{}, err } return tp, nil } -func (sdk mfSDK) Groups(token string, offset, limit uint64, id string) (GroupsPage, error) { - endpoint := fmt.Sprintf("%s?offset=%d&limit=%d", groupsEndpoint, offset, limit) - if id != "" { - endpoint = fmt.Sprintf("%s/%s/groups?offset=%d&limit=%d", groupsEndpoint, id, offset, limit) - } +func (sdk mfSDK) Groups(offset, limit uint64, token string) (auth.GroupPage, error) { + endpoint := fmt.Sprintf("%s?offset=%d&limit=%d&tree=false", groupsEndpoint, offset, limit) url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) + return sdk.getGroups(token, url) +} +func (sdk mfSDK) Parents(id string, offset, limit uint64, token string) (auth.GroupPage, error) { + endpoint := fmt.Sprintf("%s/%s/parents?offset=%d&limit=%d&tree=false&level=%d", groupsEndpoint, id, offset, limit, auth.MaxLevel) + url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) + return sdk.getGroups(token, url) +} + +func (sdk mfSDK) Children(id string, offset, limit uint64, token string) (auth.GroupPage, error) { + endpoint := fmt.Sprintf("%s/%s/children?offset=%d&limit=%d&tree=false&level=%d", groupsEndpoint, id, offset, limit, auth.MaxLevel) + url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) + return sdk.getGroups(token, url) +} + +func (sdk mfSDK) getGroups(token, url string) (auth.GroupPage, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - return GroupsPage{}, err + return auth.GroupPage{}, err } resp, err := sdk.sendRequest(req, token, string(CTJSON)) if err != nil { - return GroupsPage{}, err + return auth.GroupPage{}, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return GroupsPage{}, err + return auth.GroupPage{}, err } if resp.StatusCode != http.StatusOK { - return GroupsPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status)) + return auth.GroupPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status)) } - var tp GroupsPage + var tp auth.GroupPage if err := json.Unmarshal(body, &tp); err != nil { - return GroupsPage{}, err + return auth.GroupPage{}, err } - return tp, nil } diff --git a/pkg/sdk/go/sdk.go b/pkg/sdk/go/sdk.go index e057e78b..e54730f2 100644 --- a/pkg/sdk/go/sdk.go +++ b/pkg/sdk/go/sdk.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" "net/http" + + "github.com/mainflux/mainflux/auth" ) const ( @@ -70,8 +72,8 @@ var ( // ErrFailedCertUpdate failed to update certs in bootstrap config ErrFailedCertUpdate = errors.New("failed to update certs in bootstrap config") - // ErrFailedUserAdd failed to add user to a group. - ErrFailedUserAdd = errors.New("failed to add user to group") + // ErrMemberAdd failed to add member to a group. + ErrMemberAdd = errors.New("failed to add member to group") ) // ContentType represents all possible content types. @@ -158,19 +160,25 @@ type SDK interface { DeleteGroup(id, token string) error // Groups returns page of users groups. - Groups(token string, offset, limit uint64, name string) (GroupsPage, error) + Groups(offset, limit uint64, token string) (auth.GroupPage, error) + + // Parents returns page of users groups. + Parents(id string, offset, limit uint64, token string) (auth.GroupPage, error) + + // Children returns page of users groups. + Children(id string, offset, limit uint64, token string) (auth.GroupPage, error) // Group returns users group object by id. Group(id, token string) (Group, error) - // Assign assigns user to a group. - Assign(userID, groupID, token string) error + // Assign assigns member of member type (thing or user) to a group. + Assign(memberIDs []string, memberType, groupID string, token string) error - // Unassign removes user from a group. - Unassign(userID, groupID, token string) error + // Unassign removes member from a group. + Unassign(token, groupID string, memberIDs ...string) error - // Members lists member users of a group. - Members(groupID, token string, offset, limit uint64) (UsersPage, error) + // Members lists members of a group. + Members(groupID, token string, offset, limit uint64) (auth.MemberPage, error) // Memberships lists groups for user. Memberships(userID, token string, offset, limit uint64) (GroupsPage, error) diff --git a/pkg/sdk/go/users.go b/pkg/sdk/go/users.go index 6e95dddf..26bd98f0 100644 --- a/pkg/sdk/go/users.go +++ b/pkg/sdk/go/users.go @@ -18,6 +18,7 @@ const ( usersEndpoint = "users" tokensEndpoint = "tokens" passwordEndpoint = "password" + membersEndpoint = "members" ) func (sdk mfSDK) CreateUser(u User) (string, error) { @@ -156,8 +157,8 @@ func (sdk mfSDK) UpdatePassword(oldPass, newPass, token string) error { return nil } -func (sdk mfSDK) Memberships(userID, token string, offset, limit uint64) (GroupsPage, error) { - endpoint := fmt.Sprintf("%s/%s/groups?offset=%d&limit=%d&", usersEndpoint, userID, offset, limit) +func (sdk mfSDK) Memberships(memberID, token string, offset, limit uint64) (GroupsPage, error) { + endpoint := fmt.Sprintf("%s/%s/groups?offset=%d&limit=%d&", membersEndpoint, memberID, offset, limit) url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil {