NOISSUE - Implementation of Group removal (#161)

* add: group delete flow

Signed-off-by: Arvindh <arvindh91@gmail.com>

* sync with master

Signed-off-by: Arvindh <arvindh91@gmail.com>

* improved grpc error handling

Signed-off-by: Arvindh <arvindh91@gmail.com>

* gofumpt -ed

Signed-off-by: Arvindh <arvindh91@gmail.com>

* changed database unassign parent group id

Signed-off-by: Arvindh <arvindh91@gmail.com>

* seperate event for delete group

Signed-off-by: Arvindh <arvindh91@gmail.com>

* change group event name

Signed-off-by: Arvindh <arvindh91@gmail.com>

* update channel remove events

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix: channels event

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: sdk, sdk_tet, cli

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix: sdk mock

Signed-off-by: Arvindh <arvindh91@gmail.com>

* mockery

Signed-off-by: Arvindh <arvindh91@gmail.com>

* convert to inline case

Signed-off-by: Arvindh <arvindh91@gmail.com>

* change in group delete flow

Signed-off-by: Arvindh <arvindh91@gmail.com>

* change return error in delete

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: openapi

Signed-off-by: Arvindh <arvindh91@gmail.com>

* rename events: from channel delete to channel remove

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Arvindh
2023-12-19 17:31:24 +05:30
committed by GitHub
parent 0fa80ba186
commit 648655036e
35 changed files with 440 additions and 74 deletions
+23
View File
@@ -466,6 +466,26 @@ paths:
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
delete:
summary: Delete channel for given channel id.
description: |
Delete channel remove given channel id from repo
and removes all the policies related to channel.
tags:
- Channels
parameters:
- $ref: "#/components/parameters/chanID"
security:
- bearerAuth: []
responses:
"204":
$ref: "#/components/responses/ChannelDeleteRes"
"401":
description: Missing or invalid access token provided.
"403":
description: Unauthorized access to thing id.
"500":
$ref: "#/components/responses/ServiceError"
/channels/{chanID}/enable:
post:
@@ -1751,6 +1771,9 @@ components:
schema:
$ref: "#/components/schemas/Channel"
ChannelDeleteRes:
description: Channel Deleted.
ChannelPageRes:
description: Data retrieved.
content:
+24 -24
View File
@@ -175,7 +175,7 @@ func (client grpcClient) Issue(ctx context.Context, req *magistrala.IssueReq, _
res, err := client.issue(ctx, issueReq{userID: req.GetUserId(), domainID: req.GetDomainId(), keyType: auth.KeyType(req.Type)})
if err != nil {
return nil, decodeError(err)
return &magistrala.Token{}, decodeError(err)
}
return res.(*magistrala.Token), nil
}
@@ -195,7 +195,7 @@ func (client grpcClient) Refresh(ctx context.Context, req *magistrala.RefreshReq
res, err := client.refresh(ctx, refreshReq{refreshToken: req.GetRefreshToken(), domainID: req.GetDomainId()})
if err != nil {
return nil, decodeError(err)
return &magistrala.Token{}, decodeError(err)
}
return res.(*magistrala.Token), nil
}
@@ -215,7 +215,7 @@ func (client grpcClient) Identify(ctx context.Context, token *magistrala.Identit
res, err := client.identify(ctx, identityReq{token: token.GetToken()})
if err != nil {
return nil, err
return &magistrala.IdentityRes{}, decodeError(err)
}
ir := res.(identityRes)
return &magistrala.IdentityRes{Id: ir.id, UserId: ir.userID, DomainId: ir.domainID}, nil
@@ -246,11 +246,11 @@ func (client grpcClient) Authorize(ctx context.Context, req *magistrala.Authoriz
Object: req.GetObject(),
})
if err != nil {
return &magistrala.AuthorizeRes{}, err
return &magistrala.AuthorizeRes{}, decodeError(err)
}
ar := res.(authorizeRes)
return &magistrala.AuthorizeRes{Authorized: ar.authorized, Id: ar.id}, err
return &magistrala.AuthorizeRes{Authorized: ar.authorized, Id: ar.id}, nil
}
func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -288,11 +288,11 @@ func (client grpcClient) AddPolicy(ctx context.Context, in *magistrala.AddPolicy
Object: in.GetObject(),
})
if err != nil {
return &magistrala.AddPolicyRes{}, err
return &magistrala.AddPolicyRes{}, decodeError(err)
}
apr := res.(addPolicyRes)
return &magistrala.AddPolicyRes{Authorized: apr.authorized}, err
return &magistrala.AddPolicyRes{Authorized: apr.authorized}, nil
}
func decodeAddPolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -337,11 +337,11 @@ func (client grpcClient) AddPolicies(ctx context.Context, in *magistrala.AddPoli
res, err := client.addPolicies(ctx, r)
if err != nil {
return &magistrala.AddPoliciesRes{}, err
return &magistrala.AddPoliciesRes{}, decodeError(err)
}
apr := res.(addPoliciesRes)
return &magistrala.AddPoliciesRes{Authorized: apr.authorized}, err
return &magistrala.AddPoliciesRes{Authorized: apr.authorized}, nil
}
func decodeAddPoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -386,11 +386,11 @@ func (client grpcClient) DeletePolicy(ctx context.Context, in *magistrala.Delete
Object: in.GetObject(),
})
if err != nil {
return &magistrala.DeletePolicyRes{}, err
return &magistrala.DeletePolicyRes{}, decodeError(err)
}
dpr := res.(deletePolicyRes)
return &magistrala.DeletePolicyRes{Deleted: dpr.deleted}, err
return &magistrala.DeletePolicyRes{Deleted: dpr.deleted}, nil
}
func decodeDeletePolicyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -435,11 +435,11 @@ func (client grpcClient) DeletePolicies(ctx context.Context, in *magistrala.Dele
}
res, err := client.deletePolicies(ctx, r)
if err != nil {
return &magistrala.DeletePoliciesRes{}, err
return &magistrala.DeletePoliciesRes{}, decodeError(err)
}
dpr := res.(deletePoliciesRes)
return &magistrala.DeletePoliciesRes{Deleted: dpr.deleted}, err
return &magistrala.DeletePoliciesRes{Deleted: dpr.deleted}, nil
}
func decodeDeletePoliciesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -482,11 +482,11 @@ func (client grpcClient) ListObjects(ctx context.Context, in *magistrala.ListObj
Object: in.GetObject(),
})
if err != nil {
return &magistrala.ListObjectsRes{}, err
return &magistrala.ListObjectsRes{}, decodeError(err)
}
lpr := res.(listObjectsRes)
return &magistrala.ListObjectsRes{Policies: lpr.policies}, err
return &magistrala.ListObjectsRes{Policies: lpr.policies}, nil
}
func decodeListObjectsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -521,11 +521,11 @@ func (client grpcClient) ListAllObjects(ctx context.Context, in *magistrala.List
Object: in.GetObject(),
})
if err != nil {
return &magistrala.ListObjectsRes{}, err
return &magistrala.ListObjectsRes{}, decodeError(err)
}
lpr := res.(listObjectsRes)
return &magistrala.ListObjectsRes{Policies: lpr.policies}, err
return &magistrala.ListObjectsRes{Policies: lpr.policies}, nil
}
func (client grpcClient) CountObjects(ctx context.Context, in *magistrala.CountObjectsReq, opts ...grpc.CallOption) (*magistrala.CountObjectsRes, error) {
@@ -542,11 +542,11 @@ func (client grpcClient) CountObjects(ctx context.Context, in *magistrala.CountO
Object: in.GetObject(),
})
if err != nil {
return &magistrala.CountObjectsRes{}, err
return &magistrala.CountObjectsRes{}, decodeError(err)
}
cp := res.(countObjectsRes)
return &magistrala.CountObjectsRes{Count: int64(cp.count)}, err
return &magistrala.CountObjectsRes{Count: int64(cp.count)}, nil
}
func decodeCountObjectsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -582,11 +582,11 @@ func (client grpcClient) ListSubjects(ctx context.Context, in *magistrala.ListSu
NextPageToken: in.GetNextPageToken(),
})
if err != nil {
return &magistrala.ListSubjectsRes{}, err
return &magistrala.ListSubjectsRes{}, decodeError(err)
}
lpr := res.(listSubjectsRes)
return &magistrala.ListSubjectsRes{Policies: lpr.policies}, err
return &magistrala.ListSubjectsRes{Policies: lpr.policies}, nil
}
func decodeListSubjectsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
@@ -621,11 +621,11 @@ func (client grpcClient) ListAllSubjects(ctx context.Context, in *magistrala.Lis
Object: in.GetObject(),
})
if err != nil {
return &magistrala.ListSubjectsRes{}, err
return &magistrala.ListSubjectsRes{}, decodeError(err)
}
lpr := res.(listSubjectsRes)
return &magistrala.ListSubjectsRes{Policies: lpr.policies}, err
return &magistrala.ListSubjectsRes{Policies: lpr.policies}, nil
}
func (client grpcClient) CountSubjects(ctx context.Context, in *magistrala.CountSubjectsReq, opts ...grpc.CallOption) (*magistrala.CountSubjectsRes, error) {
@@ -681,7 +681,7 @@ func (client grpcClient) ListPermissions(ctx context.Context, in *magistrala.Lis
FilterPermissions: in.GetFilterPermissions(),
})
if err != nil {
return &magistrala.ListPermissionsRes{}, err
return &magistrala.ListPermissionsRes{}, decodeError(err)
}
lp := res.(listPermissionsRes)
-12
View File
@@ -94,18 +94,6 @@ type policyReq struct {
}
func (req policyReq) validate() error {
if req.Subject == "" || req.SubjectType == "" {
return apiutil.ErrMissingPolicySub
}
if req.Object == "" || req.ObjectType == "" {
return apiutil.ErrMissingPolicyObj
}
if req.Relation == "" && req.Permission == "" {
return apiutil.ErrMalformedPolicyRel
}
return nil
}
+2
View File
@@ -540,11 +540,13 @@ func encodeError(err error) error {
err == apiutil.ErrMalformedPolicyAct:
return status.Error(codes.InvalidArgument, err.Error())
case errors.Contains(err, errors.ErrAuthentication),
errors.Contains(err, svcerr.ErrAuthentication),
errors.Contains(err, auth.ErrKeyExpired),
err == apiutil.ErrMissingEmail,
err == apiutil.ErrBearerToken:
return status.Error(codes.Unauthenticated, err.Error())
case errors.Contains(err, errors.ErrAuthorization),
errors.Contains(err, svcerr.ErrAuthorization),
errors.Contains(err, errors.ErrDomainAuthorization):
return status.Error(codes.PermissionDenied, err.Error())
case errors.Contains(err, errors.ErrNotFound):
+1 -1
View File
@@ -153,7 +153,7 @@ func (svc service) Identify(ctx context.Context, token string) (Key, error) {
return Key{}, errors.Wrap(ErrAPIKeyExpired, err)
}
if err != nil {
return Key{}, errors.Wrap(errIdentify, err)
return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(errIdentify, err))
}
switch key.Type {
+3 -15
View File
@@ -17,7 +17,7 @@ const (
thingRemove = "thing.remove"
thingDisconnect = "policy.delete"
channelPrefix = "channel."
channelPrefix = "group."
channelUpdate = channelPrefix + "update"
channelRemove = channelPrefix + "remove"
)
@@ -95,20 +95,8 @@ func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
}
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
status := read(event, "status", "")
st, err := clients.ToStatus(status)
if err != nil {
return removeEvent{}
}
switch st {
case clients.EnabledStatus:
return removeEvent{}
case clients.DisabledStatus:
return removeEvent{
id: read(event, "id", ""),
}
default:
return removeEvent{}
return removeEvent{
id: read(event, "id", ""),
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ const (
thingUpdateConnections = thingPrefix + "update_connections"
thingDisconnect = thingPrefix + "disconnect"
channelPrefix = "channel."
channelPrefix = "group."
channelHandlerRemove = channelPrefix + "remove_handler"
channelUpdateHandler = channelPrefix + "update_handler"
+1 -1
View File
@@ -48,7 +48,7 @@ const (
thingUpdateConnections = thingPrefix + "update_connections"
thingDisconnect = thingPrefix + "disconnect"
channelPrefix = "channel."
channelPrefix = "group."
channelHandlerRemove = channelPrefix + "remove_handler"
channelUpdateHandler = channelPrefix + "update_handler"
+18
View File
@@ -81,6 +81,24 @@ var cmdChannels = []cobra.Command{
logJSON(c)
},
},
{
Use: "delete <channel_id> <user_auth_token>",
Short: "Delete channel",
Long: "Delete channel by id.\n" +
"Usage:\n" +
"\tmagistrala-cli channels delete <channel_id> $USERTOKEN - delete the given channel ID\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Use)
return
}
if err := sdk.DeleteChannel(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
{
Use: "update <channel_id> <JSON_string> <user_auth_token>",
Short: "Update channel",
+18
View File
@@ -141,6 +141,24 @@ var cmdGroups = []cobra.Command{
logJSON(t)
},
},
{
Use: "delete <group_id> <user_auth_token>",
Short: "Delete group",
Long: "Delete group by id.\n" +
"Usage:\n" +
"\tmagistrala-cli groups delete <group_id> $USERTOKEN - delete the given group ID\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Use)
return
}
if err := sdk.DeleteGroup(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
{
Use: "assign user <relation> <user_ids> <group_id> <user_auth_token>",
Short: "Assign user",
+8
View File
@@ -19,6 +19,7 @@ import (
redisclient "github.com/absmach/magistrala/internal/clients/redis"
mggroups "github.com/absmach/magistrala/internal/groups"
gapi "github.com/absmach/magistrala/internal/groups/api"
gevents "github.com/absmach/magistrala/internal/groups/events"
gpostgres "github.com/absmach/magistrala/internal/groups/postgres"
gtracing "github.com/absmach/magistrala/internal/groups/tracing"
"github.com/absmach/magistrala/internal/postgres"
@@ -58,6 +59,8 @@ const (
defDB = "things"
defSvcHTTPPort = "9000"
defSvcAuthGRPCPort = "7000"
streamID = "magistrala.things"
)
type config struct {
@@ -232,6 +235,11 @@ func newService(ctx context.Context, db *sqlx.DB, dbConfig pgclient.Config, auth
return nil, nil, err
}
gsvc, err = gevents.NewEventStoreMiddleware(ctx, gsvc, esURL, streamID)
if err != nil {
return nil, nil, err
}
csvc = ctracing.New(csvc, tracer)
csvc = api.LoggingMiddleware(csvc, logger)
counter, latency := internal.MakeMetrics(svcName, "api")
+3 -1
View File
@@ -55,6 +55,8 @@ const (
envPrefixAuth = "MG_AUTH_GRPC_"
defDB = "users"
defSvcHTTPPort = "9002"
streamID = "magistrala.users"
)
type config struct {
@@ -210,7 +212,7 @@ func newService(ctx context.Context, authClient magistrala.AuthServiceClient, db
if err != nil {
return nil, nil, err
}
gsvc, err = gevents.NewEventStoreMiddleware(ctx, gsvc, c.ESURL)
gsvc, err = gevents.NewEventStoreMiddleware(ctx, gsvc, c.ESURL, streamID)
if err != nil {
return nil, nil, err
}
+5 -3
View File
@@ -108,6 +108,7 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
switch {
case errors.Contains(err, apiutil.ErrInvalidSecret),
errors.Contains(err, svcerr.ErrMalformedEntity),
errors.Contains(err, errors.ErrMalformedEntity),
errors.Contains(err, apiutil.ErrMissingID),
errors.Contains(err, apiutil.ErrEmptyList),
errors.Contains(err, apiutil.ErrMissingMemberType),
@@ -121,13 +122,14 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
w.WriteHeader(http.StatusUnauthorized)
case errors.Contains(err, svcerr.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, svcerr.ErrConflict):
case errors.Contains(err, svcerr.ErrConflict),
errors.Contains(err, postgres.ErrMemberAlreadyAssigned),
errors.Contains(err, errors.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, svcerr.ErrAuthorization),
errors.Contains(err, errors.ErrAuthorization),
errors.Contains(err, errors.ErrDomainAuthorization):
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, postgres.ErrMemberAlreadyAssigned):
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, apiutil.ErrUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case errors.Contains(err, svcerr.ErrCreateEntity),
+13
View File
@@ -198,6 +198,19 @@ func UnassignMembersEndpoint(svc groups.Service, relation, memberKind string) en
}
}
func DeleteGroupEndpoint(svc groups.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(apiutil.ErrValidation, err)
}
if err := svc.DeleteGroup(ctx, req.token, req.id); err != nil {
return nil, err
}
return deleteGroupRes{}, nil
}
}
func buildGroupsResponseTree(page groups.Page) groupPageRes {
groupsMap := map[string]*groups.Group{}
// Parents' map keeps its array of children.
+12
View File
@@ -161,3 +161,15 @@ func (lm *loggingMiddleware) Unassign(ctx context.Context, token, groupID, relat
return lm.svc.Unassign(ctx, token, groupID, relation, memberKind, memberIDs...)
}
func (lm *loggingMiddleware) DeleteGroup(ctx context.Context, token, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method delete_group for group with id %s using token %s took %s to complete", id, 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.DeleteGroup(ctx, token, id)
}
+8
View File
@@ -119,3 +119,11 @@ func (ms *metricsMiddleware) Unassign(ctx context.Context, token, groupID, relat
return ms.svc.Unassign(ctx, token, groupID, relation, memberKind, memberIDs...)
}
func (ms *metricsMiddleware) DeleteGroup(ctx context.Context, token, id string) (err error) {
defer func(begin time.Time) {
ms.counter.With("method", "delete_group").Add(1)
ms.latency.With("method", "delete_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.DeleteGroup(ctx, token, id)
}
+14
View File
@@ -215,3 +215,17 @@ func (res listMembersRes) Headers() map[string]string {
func (res listMembersRes) Empty() bool {
return false
}
type deleteGroupRes struct{}
func (res deleteGroupRes) Code() int {
return http.StatusNoContent
}
func (res deleteGroupRes) Headers() map[string]string {
return map[string]string{}
}
func (res deleteGroupRes) Empty() bool {
return true
}
+18 -5
View File
@@ -15,18 +15,20 @@ const (
groupPrefix = "group."
groupCreate = groupPrefix + "create"
groupUpdate = groupPrefix + "update"
groupRemove = groupPrefix + "remove"
groupChangeStatus = groupPrefix + "change_status"
groupView = groupPrefix + "view"
groupViewPerms = groupPrefix + "view_perms"
groupList = groupPrefix + "list"
groupListMemberships = groupPrefix + "list_by_user"
groupRemove = groupPrefix + "remove"
)
var (
_ events.Event = (*createGroupEvent)(nil)
_ events.Event = (*updateGroupEvent)(nil)
_ events.Event = (*removeGroupEvent)(nil)
_ events.Event = (*changeStatusGroupEvent)(nil)
_ events.Event = (*viewGroupEvent)(nil)
_ events.Event = (*deleteGroupEvent)(nil)
_ events.Event = (*viewGroupEvent)(nil)
_ events.Event = (*listGroupEvent)(nil)
_ events.Event = (*listGroupMembershipEvent)(nil)
@@ -115,16 +117,16 @@ func (uge updateGroupEvent) Encode() (map[string]interface{}, error) {
return val, nil
}
type removeGroupEvent struct {
type changeStatusGroupEvent struct {
id string
status string
updatedAt time.Time
updatedBy string
}
func (rge removeGroupEvent) Encode() (map[string]interface{}, error) {
func (rge changeStatusGroupEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"operation": groupRemove,
"operation": groupChangeStatus,
"id": rge.id,
"status": rge.status,
"updated_at": rge.updatedAt,
@@ -242,3 +244,14 @@ func (lgme listGroupMembershipEvent) Encode() (map[string]interface{}, error) {
return val, nil
}
type deleteGroupEvent struct {
id string
}
func (rge deleteGroupEvent) Encode() (map[string]interface{}, error) {
return map[string]interface{}{
"operation": groupRemove,
"id": rge.id,
}, nil
}
+15 -7
View File
@@ -11,8 +11,6 @@ import (
"github.com/absmach/magistrala/pkg/groups"
)
const streamID = "magistrala.users"
var _ groups.Service = (*eventStore)(nil)
type eventStore struct {
@@ -22,7 +20,7 @@ type eventStore struct {
// NewEventStoreMiddleware returns wrapper around things service that sends
// events to event store.
func NewEventStoreMiddleware(ctx context.Context, svc groups.Service, url string) (groups.Service, error) {
func NewEventStoreMiddleware(ctx context.Context, svc groups.Service, url, streamID string) (groups.Service, error) {
publisher, err := store.NewPublisher(ctx, url, streamID)
if err != nil {
return nil, err
@@ -138,7 +136,7 @@ func (es eventStore) EnableGroup(ctx context.Context, token, id string) (groups.
return group, err
}
return es.delete(ctx, group)
return es.changeStatus(ctx, group)
}
func (es eventStore) Assign(ctx context.Context, token, groupID, relation, memberKind string, memberIDs ...string) error {
@@ -155,11 +153,11 @@ func (es eventStore) DisableGroup(ctx context.Context, token, id string) (groups
return group, err
}
return es.delete(ctx, group)
return es.changeStatus(ctx, group)
}
func (es eventStore) delete(ctx context.Context, group groups.Group) (groups.Group, error) {
event := removeGroupEvent{
func (es eventStore) changeStatus(ctx context.Context, group groups.Group) (groups.Group, error) {
event := changeStatusGroupEvent{
id: group.ID,
updatedAt: group.UpdatedAt,
updatedBy: group.UpdatedBy,
@@ -172,3 +170,13 @@ func (es eventStore) delete(ctx context.Context, group groups.Group) (groups.Gro
return group, nil
}
func (es eventStore) DeleteGroup(ctx context.Context, token, id string) error {
if err := es.svc.DeleteGroup(ctx, token, id); err != nil {
return err
}
if err := es.Publish(ctx, deleteGroupEvent{id}); err != nil {
return err
}
return nil
}
+8
View File
@@ -97,3 +97,11 @@ func (m *Repository) AssignParentGroup(ctx context.Context, parentGroupID string
return ret.Error(0)
}
func (m *Repository) Delete(ctx context.Context, groupID string) error {
ret := m.Called(ctx, groupID)
if groupID == WrongID {
return repoerr.ErrNotFound
}
return ret.Error(0)
}
+8
View File
@@ -303,6 +303,14 @@ func (repo groupRepository) UnassignParentGroup(ctx context.Context, parentGroup
return nil
}
func (repo groupRepository) Delete(ctx context.Context, groupID string) error {
q := "DELETE FROM groups AS g WHERE g.id = $1 ;"
if _, err := repo.db.ExecContext(ctx, q, groupID); err != nil {
return postgres.HandleError(repoerr.ErrRemoveEntity, err)
}
return nil
}
func buildHierachy(gm mggroups.Page) string {
query := ""
switch {
+1 -1
View File
@@ -26,7 +26,7 @@ func Migration() *migrate.MemoryMigrationSource {
updated_by VARCHAR(254),
status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0),
UNIQUE (owner_id, name),
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE SET NULL
)`,
},
Down: []string{
+54 -1
View File
@@ -463,7 +463,7 @@ func (svc service) assignParentGroup(ctx context.Context, domain, parentGroupID
var deletePolicies magistrala.DeletePoliciesReq
for _, group := range groupsPage.Groups {
if group.Parent != "" {
return fmt.Errorf("%s group already have parent", group.ID)
return errors.Wrap(errors.ErrConflict, fmt.Errorf("%s group already have parent", group.ID))
}
addPolicies.AddPoliciesReq = append(addPolicies.AddPoliciesReq, &magistrala.AddPolicyReq{
Domain: domain,
@@ -601,6 +601,59 @@ func (svc service) Unassign(ctx context.Context, token, groupID, relation, membe
return nil
}
func (svc service) DeleteGroup(ctx context.Context, token, groupID string) error {
res, err := svc.identify(ctx, token)
if err != nil {
return err
}
if _, err := svc.authorizeKind(ctx, auth.UserType, auth.UsersKind, res.GetId(), auth.DeletePermission, auth.GroupType, groupID); err != nil {
return err
}
// Remove policy of child groups
if _, err := svc.auth.DeletePolicy(ctx, &magistrala.DeletePolicyReq{
SubjectType: auth.GroupType,
Subject: groupID,
ObjectType: auth.GroupType,
}); err != nil {
return err
}
// Remove policy of things
if _, err := svc.auth.DeletePolicy(ctx, &magistrala.DeletePolicyReq{
SubjectType: auth.GroupType,
Subject: groupID,
ObjectType: auth.ThingType,
}); err != nil {
return err
}
// Remove policy from domain
if _, err := svc.auth.DeletePolicy(ctx, &magistrala.DeletePolicyReq{
SubjectType: auth.DomainType,
Object: groupID,
ObjectType: auth.GroupType,
}); err != nil {
return err
}
// Remove group from database
if err := svc.groups.Delete(ctx, groupID); err != nil {
return err
}
// Remove policy of users
if _, err := svc.auth.DeletePolicy(ctx, &magistrala.DeletePolicyReq{
SubjectType: auth.UserType,
Object: groupID,
ObjectType: auth.GroupType,
}); err != nil {
return err
}
return nil
}
func (svc service) filterAllowedGroupIDsOfUserID(ctx context.Context, userID, permission string, groupIDs []string) ([]string, error) {
var ids []string
allowedIDs, err := svc.listAllGroupsOfUserID(ctx, userID, permission)
+8
View File
@@ -102,3 +102,11 @@ func (tm *tracingMiddleware) Unassign(ctx context.Context, token, groupID, relat
return tm.gsvc.Unassign(ctx, token, groupID, relation, memberKind, memberIDs...)
}
// DeleteGroup traces the "DeleteGroup" operation of the wrapped groups.Service.
func (tm *tracingMiddleware) DeleteGroup(ctx context.Context, token, id string) error {
ctx, span := tm.tracer.Start(ctx, "svc_delete_group", trace.WithAttributes(attribute.String("id", id)))
defer span.End()
return tm.gsvc.DeleteGroup(ctx, token, id)
}
+1 -1
View File
@@ -24,7 +24,7 @@ const (
thingConnect = thingPrefix + "connect"
thingDisconnect = thingPrefix + "disconnect"
channelPrefix = "channel."
channelPrefix = "group."
channelCreate = channelPrefix + "create"
channelUpdate = channelPrefix + "update"
channelRemove = channelPrefix + "remove"
+1 -1
View File
@@ -24,7 +24,7 @@ const (
thingConnect = thingPrefix + "connect"
thingDisconnect = thingPrefix + "disconnect"
channelPrefix = "channel."
channelPrefix = "group."
channelCreate = channelPrefix + "create"
channelUpdate = channelPrefix + "update"
channelRemove = channelPrefix + "remove"
+8
View File
@@ -85,9 +85,14 @@ type Repository interface {
// ChangeStatus changes groups status to active or inactive
ChangeStatus(ctx context.Context, group Group) (Group, error)
// AssignParentGroup assigns parent group id to a given group id
AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error
// UnassignParentGroup unassign parent group id fr given group id
UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error
// Delete a group
Delete(ctx context.Context, groupID string) error
}
type Service interface {
@@ -115,6 +120,9 @@ type Service interface {
// DisableGroup logically disables the group identified with the provided ID.
DisableGroup(ctx context.Context, token, id string) (Group, error)
// DeleteGroup delete the given group id
DeleteGroup(ctx context.Context, token, id string) error
// Assign member to group
Assign(ctx context.Context, token, groupID, relation, memberKind string, memberIDs ...string) (err error)
+6
View File
@@ -295,6 +295,12 @@ func (sdk mgSDK) DisableChannel(id, token string) (Channel, errors.SDKError) {
return sdk.changeChannelStatus(id, disableEndpoint, token)
}
func (sdk mgSDK) DeleteChannel(id, token string) errors.SDKError {
url := fmt.Sprintf("%s/%s/%s", sdk.thingsURL, channelsEndpoint, id)
_, _, sdkerr := sdk.processRequest(http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) changeChannelStatus(id, status, token string) (Channel, errors.SDKError) {
url := fmt.Sprintf("%s/%s/%s/%s", sdk.thingsURL, channelsEndpoint, id, status)
+42
View File
@@ -755,6 +755,48 @@ func TestDisableChannel(t *testing.T) {
repoCall2.Unset()
}
func TestDeleteChannel(t *testing.T) {
ts, grepo, auth := newChannelsServer()
defer ts.Close()
conf := sdk.Config{
ThingsURL: ts.URL,
}
mgsdk := sdk.NewSDK(conf)
creationTime := time.Now().UTC()
channel := sdk.Channel{
ID: generateUUID(t),
Name: gName,
OwnerID: generateUUID(t),
CreatedAt: creationTime,
UpdatedAt: creationTime,
Status: mgclients.Enabled,
}
repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil)
repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, nil)
repoCall2 := grepo.On("Delete", mock.Anything, mock.Anything).Return(nil)
err := mgsdk.DeleteChannel("wrongID", validToken)
assert.Equal(t, err, errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), fmt.Sprintf("Delete channel with wrong id: expected %v got %v", svcerr.ErrNotFound, err))
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
repoCall = auth.On("DeletePolicy", mock.Anything, mock.Anything, mock.Anything).Return(&magistrala.DeletePolicyRes{Deleted: true}, nil)
repoCall1 = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil)
repoCall2 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil)
repoCall3 := grepo.On("Delete", mock.Anything, mock.Anything).Return(nil)
err = mgsdk.DeleteChannel(channel.ID, validToken)
assert.Nil(t, err, fmt.Sprintf("Delete channel with correct id: expected %v got %v", nil, err))
ok := repoCall3.Parent.AssertCalled(t, "Delete", mock.Anything, channel.ID)
assert.True(t, ok, "Delete was not called on deleting channel with correct id")
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
repoCall3.Unset()
}
func toIDs(objects interface{}) []string {
v := reflect.ValueOf(objects)
if v.Kind() != reflect.Slice {
+6
View File
@@ -220,6 +220,12 @@ func (sdk mgSDK) ListGroupChannels(groupID string, pm PageMetadata, token string
return gp, nil
}
func (sdk mgSDK) DeleteGroup(id, token string) errors.SDKError {
url := fmt.Sprintf("%s/%s/%s", sdk.usersURL, groupsEndpoint, id)
_, _, sdkerr := sdk.processRequest(http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) changeGroupStatus(id, status, token string) (Group, errors.SDKError) {
url := fmt.Sprintf("%s/%s/%s/%s", sdk.usersURL, groupsEndpoint, id, status)
+42
View File
@@ -889,3 +889,45 @@ func TestDisableGroup(t *testing.T) {
repoCall1.Unset()
repoCall2.Unset()
}
func TestDeleteGroup(t *testing.T) {
ts, grepo, auth := newGroupsServer()
defer ts.Close()
conf := sdk.Config{
UsersURL: ts.URL,
}
mgsdk := sdk.NewSDK(conf)
creationTime := time.Now().UTC()
group := sdk.Group{
ID: generateUUID(t),
Name: gName,
OwnerID: generateUUID(t),
CreatedAt: creationTime,
UpdatedAt: creationTime,
Status: clients.Enabled,
}
repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil)
repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, nil)
repoCall2 := grepo.On("Delete", mock.Anything, mock.Anything).Return(nil)
err := mgsdk.DeleteGroup("wrongID", validToken)
assert.Equal(t, err, errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), fmt.Sprintf("Delete group with wrong id: expected %v got %v", svcerr.ErrNotFound, err))
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
repoCall = auth.On("DeletePolicy", mock.Anything, mock.Anything, mock.Anything).Return(&magistrala.DeletePolicyRes{Deleted: true}, nil)
repoCall1 = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil)
repoCall2 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil)
repoCall3 := grepo.On("Delete", mock.Anything, mock.Anything).Return(nil)
err = mgsdk.DeleteGroup(group.ID, validToken)
assert.Nil(t, err, fmt.Sprintf("Delete group with correct id: expected %v got %v", nil, err))
ok := repoCall3.Parent.AssertCalled(t, "Delete", mock.Anything, group.ID)
assert.True(t, ok, "Delete was not called on deleting group with correct id")
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
repoCall3.Unset()
}
+14
View File
@@ -590,6 +590,13 @@ type SDK interface {
// fmt.Println(groups)
ListGroupChannels(groupID string, pm PageMetadata, token string) (GroupsPage, errors.SDKError)
// DeleteGroup delete given group id.
//
// example:
// err := sdk.DeleteGroup("groupID", "token")
// fmt.Println(err)
DeleteGroup(id, token string) errors.SDKError
// CreateChannel creates new channel and returns its id.
//
// example:
@@ -756,6 +763,13 @@ type SDK interface {
// fmt.Println(groups)
ListChannelUserGroups(channelID string, pm PageMetadata, token string) (GroupsPage, errors.SDKError)
// DeleteChannel delete given group id.
//
// example:
// err := sdk.DeleteChannel("channelID", "token")
// fmt.Println(err)
DeleteChannel(id, token string) errors.SDKError
// Connect bulk connects things to channels specified by id.
//
// example:
+40
View File
@@ -700,6 +700,46 @@ func (_m *SDK) CreateUser(user sdk.User, token string) (sdk.User, errors.SDKErro
return r0, r1
}
// DeleteChannel provides a mock function with given fields: id, token
func (_m *SDK) DeleteChannel(id string, token string) errors.SDKError {
ret := _m.Called(id, token)
if len(ret) == 0 {
panic("no return value specified for DeleteChannel")
}
var r0 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string) errors.SDKError); ok {
r0 = rf(id, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// DeleteGroup provides a mock function with given fields: id, token
func (_m *SDK) DeleteGroup(id string, token string) errors.SDKError {
ret := _m.Called(id, token)
if len(ret) == 0 {
panic("no return value specified for DeleteGroup")
}
var r0 errors.SDKError
if rf, ok := ret.Get(0).(func(string, string) errors.SDKError); ok {
r0 = rf(id, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// DeleteInvitation provides a mock function with given fields: userID, domainID, token
func (_m *SDK) DeleteInvitation(userID string, domainID string, token string) error {
ret := _m.Called(userID, domainID, token)
+7
View File
@@ -40,6 +40,13 @@ func groupsHandler(svc groups.Service, r *chi.Mux, logger mglog.Logger) http.Han
opts...,
), "view_channel").ServeHTTP)
r.Delete("/{groupID}", otelhttp.NewHandler(kithttp.NewServer(
gapi.DeleteGroupEndpoint(svc),
gapi.DecodeGroupRequest,
api.EncodeResponse,
opts...,
), "delete_channel").ServeHTTP)
r.Get("/{groupID}/permissions", otelhttp.NewHandler(kithttp.NewServer(
gapi.ViewGroupPermsEndpoint(svc),
gapi.DecodeGroupPermsRequest,
+7
View File
@@ -42,6 +42,13 @@ func groupsHandler(svc groups.Service, r *chi.Mux, logger mglog.Logger) http.Han
opts...,
), "view_group").ServeHTTP)
r.Delete("/{groupID}", otelhttp.NewHandler(kithttp.NewServer(
gapi.DeleteGroupEndpoint(svc),
gapi.DecodeGroupRequest,
api.EncodeResponse,
opts...,
), "delete_group").ServeHTTP)
r.Get("/{groupID}/permissions", otelhttp.NewHandler(kithttp.NewServer(
gapi.ViewGroupPermsEndpoint(svc),
gapi.DecodeGroupPermsRequest,