// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package groups import ( "context" "fmt" "time" "github.com/mainflux/mainflux" "github.com/mainflux/mainflux/internal/apiutil" mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" "github.com/mainflux/mainflux/pkg/groups" ) var errParentUnAuthz = errors.New("failed to authorize parent group") const ( ownerRelation = "owner" channelRelation = "channel" groupRelation = "group" parentGroupRelation = "parent_group" usersKind = "users" groupsKind = "groups" thingsKind = "things" channelsKind = "channels" userType = "user" groupType = "group" thingType = "thing" channelType = "channel" adminPermission = "admin" ownerPermission = "delete" deletePermission = "delete" sharePermission = "share" editPermission = "edit" disconnectPermission = "disconnect" connectPermission = "connect" viewPermission = "view" memberPermission = "member" tokenKind = "token" ) type service struct { groups groups.Repository auth mainflux.AuthServiceClient idProvider mainflux.IDProvider } // NewService returns a new Clients service implementation. func NewService(g groups.Repository, idp mainflux.IDProvider, auth mainflux.AuthServiceClient) groups.Service { return service{ groups: g, idProvider: idp, auth: auth, } } func (svc service) CreateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) { ownerID, err := svc.identify(ctx, token) if err != nil { return groups.Group{}, err } groupID, err := svc.idProvider.ID() if err != nil { return groups.Group{}, err } if g.Status != mfclients.EnabledStatus && g.Status != mfclients.DisabledStatus { return groups.Group{}, apiutil.ErrInvalidStatus } if g.Owner == "" { g.Owner = ownerID } g.ID = groupID g.CreatedAt = time.Now() if g.Parent != "" { _, err := svc.authorize(ctx, userType, token, editPermission, groupType, g.Parent) if err != nil { return groups.Group{}, errors.Wrap(errParentUnAuthz, err) } } g, err = svc.groups.Save(ctx, g) if err != nil { return groups.Group{}, err } policy := mainflux.AddPolicyReq{ SubjectType: userType, Subject: ownerID, Relation: ownerRelation, ObjectType: groupType, Object: g.ID, } if _, err := svc.auth.AddPolicy(ctx, &policy); err != nil { return groups.Group{}, err } if g.Parent != "" { policy = mainflux.AddPolicyReq{ SubjectType: groupType, Subject: g.Parent, Relation: parentGroupRelation, ObjectType: groupType, Object: g.ID, } if _, err := svc.auth.AddPolicy(ctx, &policy); err != nil { return groups.Group{}, err } } return g, nil } func (svc service) ViewGroup(ctx context.Context, token string, id string) (groups.Group, error) { _, err := svc.authorize(ctx, userType, token, viewPermission, groupType, id) if err != nil { return groups.Group{}, err } return svc.groups.RetrieveByID(ctx, id) } func (svc service) ListGroups(ctx context.Context, token string, memberKind, memberID string, gm groups.Page) (groups.Page, error) { var ids []string userID, err := svc.identify(ctx, token) if err != nil { return groups.Page{}, err } switch memberKind { case thingsKind: if _, err := svc.authorizeKind(ctx, userType, usersKind, userID, viewPermission, thingType, memberID); err != nil { return groups.Page{}, err } cids, err := svc.auth.ListAllSubjects(ctx, &mainflux.ListSubjectsReq{ SubjectType: groupType, Permission: groupRelation, ObjectType: thingType, Object: memberID, }) if err != nil { return groups.Page{}, err } ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, userID, gm.Permission, cids.Policies) if err != nil { return groups.Page{}, err } case groupsKind: if _, err := svc.authorizeKind(ctx, userType, usersKind, userID, gm.Permission, groupType, memberID); err != nil { return groups.Page{}, err } gids, err := svc.auth.ListAllObjects(ctx, &mainflux.ListObjectsReq{ SubjectType: groupType, Subject: memberID, Permission: parentGroupRelation, ObjectType: groupType, }) if err != nil { return groups.Page{}, err } ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, userID, gm.Permission, gids.Policies) if err != nil { return groups.Page{}, err } case channelsKind: if _, err := svc.authorizeKind(ctx, userType, usersKind, userID, viewPermission, groupType, memberID); err != nil { return groups.Page{}, err } gids, err := svc.auth.ListAllSubjects(ctx, &mainflux.ListSubjectsReq{ SubjectType: groupType, Permission: parentGroupRelation, ObjectType: groupType, Object: memberID, }) if err != nil { return groups.Page{}, err } ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, userID, gm.Permission, gids.Policies) if err != nil { return groups.Page{}, err } case usersKind: if memberID != "" && userID != memberID { if _, err := svc.authorizeKind(ctx, userType, usersKind, userID, ownerRelation, userType, memberID); err != nil { return groups.Page{}, err } gids, err := svc.auth.ListAllObjects(ctx, &mainflux.ListObjectsReq{ SubjectType: userType, Subject: memberID, Permission: gm.Permission, ObjectType: groupType, }) if err != nil { return groups.Page{}, err } ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, userID, gm.Permission, gids.Policies) if err != nil { return groups.Page{}, err } } else { ids, err = svc.listAllGroupsOfUserID(ctx, userID, gm.Permission) if err != nil { return groups.Page{}, err } } default: return groups.Page{}, fmt.Errorf("invalid member kind") } if len(ids) == 0 { return groups.Page{ PageMeta: gm.PageMeta, }, nil } return svc.groups.RetrieveByIDs(ctx, gm, ids...) } func (svc service) ListMembers(ctx context.Context, token, groupID, permission, memberKind string) (groups.MembersPage, error) { _, err := svc.authorize(ctx, userType, token, viewPermission, groupType, groupID) if err != nil { return groups.MembersPage{}, err } switch memberKind { case thingsKind: tids, err := svc.auth.ListAllObjects(ctx, &mainflux.ListObjectsReq{ SubjectType: groupType, Subject: groupID, Relation: groupRelation, ObjectType: thingType, }) if err != nil { return groups.MembersPage{}, err } members := []groups.Member{} for _, id := range tids.Policies { members = append(members, groups.Member{ ID: id, Type: thingType, }) } return groups.MembersPage{ Total: uint64(len(members)), Offset: 0, Limit: uint64(len(members)), Members: members, }, nil case usersKind: uids, err := svc.auth.ListAllSubjects(ctx, &mainflux.ListSubjectsReq{ SubjectType: userType, Permission: permission, Object: groupID, ObjectType: groupType, }) if err != nil { return groups.MembersPage{}, err } members := []groups.Member{} for _, id := range uids.Policies { members = append(members, groups.Member{ ID: id, Type: userType, }) } return groups.MembersPage{ Total: uint64(len(members)), Offset: 0, Limit: uint64(len(members)), Members: members, }, nil default: return groups.MembersPage{}, fmt.Errorf("invalid member_kind") } } func (svc service) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) { id, err := svc.authorize(ctx, userType, token, editPermission, groupType, g.ID) if err != nil { return groups.Group{}, err } g.UpdatedAt = time.Now() g.UpdatedBy = id return svc.groups.Update(ctx, g) } func (svc service) EnableGroup(ctx context.Context, token, id string) (groups.Group, error) { group := groups.Group{ ID: id, Status: mfclients.EnabledStatus, UpdatedAt: time.Now(), } group, err := svc.changeGroupStatus(ctx, token, group) if err != nil { return groups.Group{}, err } return group, nil } func (svc service) DisableGroup(ctx context.Context, token, id string) (groups.Group, error) { group := groups.Group{ ID: id, Status: mfclients.DisabledStatus, UpdatedAt: time.Now(), } group, err := svc.changeGroupStatus(ctx, token, group) if err != nil { return groups.Group{}, err } return group, nil } func (svc service) Assign(ctx context.Context, token, groupID, relation, memberKind string, memberIDs ...string) error { _, err := svc.authorize(ctx, userType, token, editPermission, groupType, groupID) if err != nil { return err } prs := []*mainflux.AddPolicyReq{} switch memberKind { case thingsKind: for _, memberID := range memberIDs { prs = append(prs, &mainflux.AddPolicyReq{ SubjectType: groupType, Subject: groupID, Relation: relation, ObjectType: thingType, Object: memberID, }) } case groupsKind: for _, memberID := range memberIDs { prs = append(prs, &mainflux.AddPolicyReq{ SubjectType: groupType, Subject: memberID, Relation: relation, ObjectType: groupType, Object: groupID, }) } case usersKind: for _, memberID := range memberIDs { prs = append(prs, &mainflux.AddPolicyReq{ SubjectType: userType, Subject: memberID, Relation: relation, ObjectType: groupType, Object: groupID, }) } default: return fmt.Errorf("invalid member kind") } for _, pr := range prs { if _, err := svc.auth.AddPolicy(ctx, pr); err != nil { return fmt.Errorf("failed to add policies : %w", err) } } return nil } func (svc service) Unassign(ctx context.Context, token, groupID, relation, memberKind string, memberIDs ...string) error { _, err := svc.authorize(ctx, userType, token, editPermission, groupType, groupID) if err != nil { return err } prs := []*mainflux.DeletePolicyReq{} switch memberKind { case thingsKind: for _, memberID := range memberIDs { prs = append(prs, &mainflux.DeletePolicyReq{ SubjectType: groupType, Subject: groupID, Relation: relation, ObjectType: thingType, Object: memberID, }) } case groupsKind: for _, memberID := range memberIDs { prs = append(prs, &mainflux.DeletePolicyReq{ SubjectType: groupType, Subject: memberID, Relation: relation, ObjectType: groupType, Object: groupID, }) } case usersKind: for _, memberID := range memberIDs { prs = append(prs, &mainflux.DeletePolicyReq{ SubjectType: userType, Subject: memberID, Relation: relation, ObjectType: groupType, Object: groupID, }) } default: return fmt.Errorf("invalid member kind") } for _, pr := range prs { if _, err := svc.auth.DeletePolicy(ctx, pr); err != nil { return fmt.Errorf("failed to delete policies : %w", err) } } return nil } func (svc service) filterAllowedGroupIDsOfUserID(ctx context.Context, userID string, permission string, groupIDs []string) ([]string, error) { var ids []string allowedIDs, err := svc.listAllGroupsOfUserID(ctx, userID, permission) if err != nil { return []string{}, err } for _, gid := range groupIDs { for _, id := range allowedIDs { if id == gid { ids = append(ids, id) } } } return ids, nil } func (svc service) listAllGroupsOfUserID(ctx context.Context, userID string, permission string) ([]string, error) { allowedIDs, err := svc.auth.ListAllObjects(ctx, &mainflux.ListObjectsReq{ SubjectType: userType, Subject: userID, Permission: permission, ObjectType: groupType, }) if err != nil { return []string{}, err } return allowedIDs.Policies, nil } func (svc service) changeGroupStatus(ctx context.Context, token string, group groups.Group) (groups.Group, error) { id, err := svc.authorize(ctx, userType, token, editPermission, groupType, group.ID) if err != nil { return groups.Group{}, err } dbGroup, err := svc.groups.RetrieveByID(ctx, group.ID) if err != nil { return groups.Group{}, err } if dbGroup.Status == group.Status { return groups.Group{}, mfclients.ErrStatusAlreadyAssigned } group.UpdatedBy = id return svc.groups.ChangeStatus(ctx, group) } func (svc service) identify(ctx context.Context, token string) (string, error) { user, err := svc.auth.Identify(ctx, &mainflux.IdentityReq{Token: token}) if err != nil { return "", err } return user.GetId(), nil } func (svc service) authorize(ctx context.Context, subjectType, subject, permission, objectType, object string) (string, error) { req := &mainflux.AuthorizeReq{ SubjectType: subjectType, SubjectKind: tokenKind, Subject: subject, Permission: permission, Object: object, ObjectType: objectType, } res, err := svc.auth.Authorize(ctx, req) if err != nil { return "", errors.Wrap(errors.ErrAuthorization, err) } if !res.GetAuthorized() { return "", errors.ErrAuthorization } return res.GetId(), nil } func (svc service) authorizeKind(ctx context.Context, subjectType, subjectKind, subject, permission, objectType, object string) (string, error) { req := &mainflux.AuthorizeReq{ SubjectType: subjectType, SubjectKind: subjectKind, Subject: subject, Permission: permission, Object: object, ObjectType: objectType, } res, err := svc.auth.Authorize(ctx, req) if err != nil { return "", errors.Wrap(errors.ErrAuthorization, err) } if !res.GetAuthorized() { return "", errors.ErrAuthorization } return res.GetId(), nil }