absmach / magistrala

Industrial IoT Messaging and Device Management Platform
https://www.abstractmachines.fr/magistrala.html
Apache License 2.0
2.43k stars 663 forks source link

Feature: Permission / Relations for Subscribe and Publish to channels #1960

Open arvindh123 opened 6 months ago

arvindh123 commented 6 months ago

Is your feature request related to a problem? Please describe.

Things could not be related to groups as subscribe or publish relation , Instead group related to thing as publish or subscribe relation . In present case , thing is object and group becomes subject.

Subject is Group(channel) Object is Thing

Because of this we could not check thing (Subject) have publish permission on group (Object)

At present it will work like group (Subject) have publish permission on thing (Object)

Describe the feature you are requesting, as well as the possible use case(s) for it.

Actually, thing should be subject and group should be object.

So if we have like above we can do permission check like thing have publish permission on group

This changes will lead to change in service logic for List thing channels and List channel things

Indicate the importance of this feature to you.

Must-have

Anything else?

No response

arvindh123 commented 2 weeks ago

@felixgateru We can achieve this Ticket with following changes

Change 1

Changing the spiceDB definition for things like given below File location : docker/spicedb/schema.zed

definition thing {
    relation administrator: user
-   relation group: group
+     relation publisher: group
+     relation subscriber: group
    relation domain: domain

    permission admin = administrator + group->admin + domain->admin
    permission delete = admin
    permission edit = admin + group->edit + domain->edit
    permission view = edit + group->view  + domain->view
    permission share = edit
    permission publish = group
    permission subscribe = group
+      permission connect = publish + subscribe 

    // These permission are made for only list purpose. It helps to list users have only particular permission excluding other higher and lower permission.
    permission admin_only = admin
    permission edit_only = edit - admin
    permission viewer_only = view - edit

    // These permission are made for only list purpose. It helps to list users from external, users who are not in group but have permission on the group through parent group
    permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group
}

Change 2

Then we need add new optional json field with name relation in thing channel connect request . relation field should be a string array and support only two options publish and subscribe

So we need to modify exactly at connectChannelThingRequest in things/api/http/requests.go, and also it validation function

type connectChannelThingRequest struct {
    token     string
    ThingID   string `json:"thing_id,omitempty"`
    ChannelID string `json:"channel_id,omitempty"`
+      Relation string `json:"relation,omitempty"`
}

func (req *connectChannelThingRequest) validate() error {
    if req.ThingID == "" || req.ChannelID == "" {
        return apiutil.ErrMissingID
    }
+   if req.Relation != auth.Publisher || req.Relation != auth.Publisher {
+       return apiutil.ErrInvalidRelation
+   }
    return nil
}

Change 3

Then we need to modify connectChannelThingEndpoint and connectEndpoint in things/api/http/endpoints.go

func connectChannelThingEndpoint(svc groups.Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(connectChannelThingRequest)
        if err := req.validate(); err != nil {
            return nil, errors.Wrap(apiutil.ErrValidation, err)
        }

-       if err := svc.Assign(ctx, req.token, req.ChannelID, auth.GroupRelation, auth.ThingsKind, req.ThingID); err != nil {
+       if err := svc.Assign(ctx, req.token, req.ChannelID, req,Relation, auth.ThingsKind, req.ThingID); err != nil {
            return nil, err
        }

        return connectChannelThingRes{}, nil
    }
}

func connectEndpoint(svc groups.Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(connectChannelThingRequest)
        if err := req.validate(); err != nil {
            return nil, errors.Wrap(apiutil.ErrValidation, err)
        }

-       if err := svc.Assign(ctx, req.token, req.ChannelID, auth.GroupRelation, auth.ThingsKind, req.ThingID); err != nil {
+       if err := svc.Assign(ctx, req.token, req.ChannelID, req.Relation, auth.ThingsKind, req.ThingID); err != nil {
            return nil, err
        }

        return connectChannelThingRes{}, nil
    }
}

Change 4

Then we need to change the Thing relation assign logic in internal/groups/service.go

func (svc service) Assign(ctx context.Context, token, groupID, relation, memberKind string, memberIDs ...string) error {
    res, err := svc.identify(ctx, token)
    if err != nil {
        return err
    }
    if _, err := svc.authorizeKind(ctx, res.GetDomainId(), auth.UserType, auth.UsersKind, res.GetId(), auth.EditPermission, auth.GroupType, groupID); err != nil {
        return err
    }

    policies := magistrala.AddPoliciesReq{}
    switch memberKind {
    case auth.ThingsKind:
+       var thingRelations []string = []string{auth.PublisherRelation, auth.SubscriberRelation}
+       if relation == "" {
+           thingRelations = []string{relation}
+       }
+       for _, relation := range thingRelations {
            for _, memberID := range memberIDs {
                policies.AddPoliciesReq = append(policies.AddPoliciesReq, &magistrala.AddPolicyReq{
                    Domain:      res.GetDomainId(),
                    SubjectType: auth.GroupType,
                    SubjectKind: auth.ChannelsKind,
                    Subject:     groupID,
                    Relation:    relation,
                    ObjectType:  auth.ThingType,
                    Object:      memberID,
                })
            }
+       }
    case auth.ChannelsKind:
        for _, memberID := range memberIDs {
            policies.AddPoliciesReq = append(policies.AddPoliciesReq, &magistrala.AddPolicyReq{
                Domain:      res.GetDomainId(),
                SubjectType: auth.GroupType,
                Subject:     memberID,
                Relation:    relation,
                ObjectType:  auth.GroupType,
                Object:      groupID,
            })
        }
    case auth.GroupsKind:
        return svc.assignParentGroup(ctx, res.GetDomainId(), groupID, memberIDs)

    case auth.UsersKind:
        for _, memberID := range memberIDs {
            policies.AddPoliciesReq = append(policies.AddPoliciesReq, &magistrala.AddPolicyReq{
                Domain:      res.GetDomainId(),
                SubjectType: auth.UserType,
                Subject:     auth.EncodeDomainUserID(res.GetDomainId(), memberID),
                Relation:    relation,
                ObjectType:  auth.GroupType,
                Object:      groupID,
            })
        }
    default:
        return errMemberKind
    }

    if _, err := svc.auth.AddPolicies(ctx, &policies); err != nil {
        return errors.Wrap(svcerr.ErrAddPolicies, err)
    }

    return nil
}

Same way we need to do for Unassign