Files
supermq/auth/spicedb/policies.go
T
b1ackd0t a0c40ba462 NOISSUE - Update Copyright Notice (#39)
* chore(license): update copyright notices

Add CI check for non go files to check that the files contain a license

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* fix(ci): log failed files

When the CI fails during check for license header, log the failed file to console so that someone can check on the actual file. Also simplify the grep check to make it more human readable and understandable

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
2023-11-17 12:37:30 +01:00

657 lines
21 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package spicedb
import (
"context"
"fmt"
"io"
"github.com/absmach/magistrala/auth"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pkg/errors"
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
"github.com/authzed/authzed-go/v1"
)
const defRetrieveAllLimit = 1000
var (
errInvalidSubject = errors.New("invalid subject kind")
errAddPolicies = errors.New("failed to add policies")
errRetrievePolicies = errors.New("failed to retrieve policies")
errRemovePolicies = errors.New("failed to remove the policies")
errNoPolicies = errors.New("no policies provided")
errPermission = errors.New("failed to check permission")
)
type policyAgent struct {
client *authzed.Client
permissionClient v1.PermissionsServiceClient
logger mglog.Logger
}
func NewPolicyAgent(client *authzed.Client, logger mglog.Logger) auth.PolicyAgent {
return &policyAgent{
client: client,
permissionClient: client.PermissionsServiceClient,
logger: logger,
}
}
func (pa *policyAgent) CheckPolicy(ctx context.Context, pr auth.PolicyReq) error {
checkReq := v1.CheckPermissionRequest{
Resource: &v1.ObjectReference{ObjectType: pr.ObjectType, ObjectId: pr.Object},
Permission: pr.Permission,
Subject: &v1.SubjectReference{Object: &v1.ObjectReference{ObjectType: pr.SubjectType, ObjectId: pr.Subject}, OptionalRelation: pr.SubjectRelation},
}
resp, err := pa.permissionClient.CheckPermission(ctx, &checkReq)
if err != nil {
return errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errPermission, err))
}
if resp.Permissionship == v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION {
return nil
}
if reason, ok := v1.CheckPermissionResponse_Permissionship_name[int32(resp.Permissionship)]; ok {
return errors.Wrap(errors.ErrAuthorization, errors.New(reason))
}
return errors.ErrAuthorization
}
func (pa *policyAgent) AddPolicies(ctx context.Context, prs []auth.PolicyReq) error {
updates := []*v1.RelationshipUpdate{}
var preconds []*v1.Precondition
for _, pr := range prs {
precond, err := pa.addPolicyPreCondition(ctx, pr)
if err != nil {
return err
}
preconds = append(preconds, precond...)
updates = append(updates, &v1.RelationshipUpdate{
Operation: v1.RelationshipUpdate_OPERATION_CREATE,
Relationship: &v1.Relationship{
Resource: &v1.ObjectReference{ObjectType: pr.ObjectType, ObjectId: pr.Object},
Relation: pr.Relation,
Subject: &v1.SubjectReference{Object: &v1.ObjectReference{ObjectType: pr.SubjectType, ObjectId: pr.Subject}, OptionalRelation: pr.SubjectRelation},
},
})
}
if len(updates) == 0 {
return errNoPolicies
}
_, err := pa.permissionClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{Updates: updates, OptionalPreconditions: preconds})
if err != nil {
return errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errAddPolicies, err))
}
return nil
}
func (pa *policyAgent) AddPolicy(ctx context.Context, pr auth.PolicyReq) error {
precond, err := pa.addPolicyPreCondition(ctx, pr)
if err != nil {
return err
}
updates := []*v1.RelationshipUpdate{
{
Operation: v1.RelationshipUpdate_OPERATION_CREATE,
Relationship: &v1.Relationship{
Resource: &v1.ObjectReference{ObjectType: pr.ObjectType, ObjectId: pr.Object},
Relation: pr.Relation,
Subject: &v1.SubjectReference{Object: &v1.ObjectReference{ObjectType: pr.SubjectType, ObjectId: pr.Subject}, OptionalRelation: pr.SubjectRelation},
},
},
}
_, err = pa.permissionClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{Updates: updates, OptionalPreconditions: precond})
if err != nil {
return errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errAddPolicies, err))
}
return nil
}
func (pa *policyAgent) DeletePolicies(ctx context.Context, prs []auth.PolicyReq) error {
updates := []*v1.RelationshipUpdate{}
for _, pr := range prs {
updates = append(updates, &v1.RelationshipUpdate{
Operation: v1.RelationshipUpdate_OPERATION_DELETE,
Relationship: &v1.Relationship{
Resource: &v1.ObjectReference{ObjectType: pr.ObjectType, ObjectId: pr.Object},
Relation: pr.Relation,
Subject: &v1.SubjectReference{Object: &v1.ObjectReference{ObjectType: pr.SubjectType, ObjectId: pr.Subject}, OptionalRelation: pr.SubjectRelation},
},
})
}
if len(updates) == 0 {
return errNoPolicies
}
_, err := pa.permissionClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{Updates: updates})
if err != nil {
return errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errRemovePolicies, err))
}
return nil
}
func (pa *policyAgent) DeletePolicy(ctx context.Context, pr auth.PolicyReq) error {
req := &v1.DeleteRelationshipsRequest{
RelationshipFilter: &v1.RelationshipFilter{
ResourceType: pr.ObjectType,
OptionalResourceId: pr.Object,
OptionalRelation: pr.Relation,
OptionalSubjectFilter: &v1.SubjectFilter{
OptionalSubjectId: pr.Subject,
SubjectType: pr.SubjectType,
OptionalRelation: &v1.SubjectFilter_RelationFilter{
Relation: pr.SubjectRelation,
},
},
},
}
if _, err := pa.permissionClient.DeleteRelationships(ctx, req); err != nil {
return errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errRemovePolicies, err))
}
return nil
}
// RetrieveObjects - Listing of things.
func (pa *policyAgent) RetrieveObjects(ctx context.Context, pr auth.PolicyReq, nextPageToken string, limit int32) ([]auth.PolicyRes, string, error) {
resourceReq := &v1.LookupResourcesRequest{
ResourceObjectType: pr.ObjectType,
Permission: pr.Permission,
Subject: &v1.SubjectReference{Object: &v1.ObjectReference{ObjectType: pr.SubjectType, ObjectId: pr.Subject}, OptionalRelation: pr.SubjectRelation},
OptionalLimit: uint32(limit),
}
if nextPageToken != "" {
resourceReq.OptionalCursor = &v1.Cursor{Token: nextPageToken}
}
stream, err := pa.permissionClient.LookupResources(ctx, resourceReq)
if err != nil {
return nil, "", errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errRetrievePolicies, err))
}
resources := []*v1.LookupResourcesResponse{}
var token string
for {
resp, err := stream.Recv()
switch err {
case nil:
resources = append(resources, resp)
case io.EOF:
if len(resources) > 0 && resources[len(resources)-1].AfterResultCursor != nil {
token = resources[len(resources)-1].AfterResultCursor.Token
}
return objectsToAuthPolicies(resources), token, nil
default:
if len(resources) > 0 && resources[len(resources)-1].AfterResultCursor != nil {
token = resources[len(resources)-1].AfterResultCursor.Token
}
return objectsToAuthPolicies(resources), token, errors.Wrap(errors.ErrViewEntity, err)
}
}
}
func (pa *policyAgent) RetrieveAllObjects(ctx context.Context, pr auth.PolicyReq) ([]auth.PolicyRes, error) {
resourceReq := &v1.LookupResourcesRequest{
ResourceObjectType: pr.ObjectType,
Permission: pr.Permission,
Subject: &v1.SubjectReference{Object: &v1.ObjectReference{ObjectType: pr.SubjectType, ObjectId: pr.Subject}, OptionalRelation: pr.SubjectRelation},
}
stream, err := pa.permissionClient.LookupResources(ctx, resourceReq)
if err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errRetrievePolicies, err))
}
tuples := []auth.PolicyRes{}
for {
resp, err := stream.Recv()
switch {
case errors.Contains(err, io.EOF):
return tuples, nil
case err != nil:
return tuples, err
default:
tuples = append(tuples, auth.PolicyRes{Object: resp.ResourceObjectId})
}
}
}
func (pa *policyAgent) RetrieveAllObjectsCount(ctx context.Context, pr auth.PolicyReq) (int, error) {
var count int
nextPageToken := ""
for {
relationTuples, npt, err := pa.RetrieveObjects(ctx, pr, nextPageToken, defRetrieveAllLimit)
if err != nil {
return count, err
}
count = count + len(relationTuples)
if npt == "" {
break
}
nextPageToken = npt
}
return count, nil
}
func (pa *policyAgent) RetrieveSubjects(ctx context.Context, pr auth.PolicyReq, nextPageToken string, limit int32) ([]auth.PolicyRes, string, error) {
subjectsReq := v1.LookupSubjectsRequest{
Resource: &v1.ObjectReference{ObjectType: pr.ObjectType, ObjectId: pr.Object},
Permission: pr.Permission,
SubjectObjectType: pr.SubjectType,
OptionalSubjectRelation: pr.SubjectRelation,
OptionalConcreteLimit: uint32(limit),
WildcardOption: v1.LookupSubjectsRequest_WILDCARD_OPTION_INCLUDE_WILDCARDS,
}
if nextPageToken != "" {
subjectsReq.OptionalCursor = &v1.Cursor{Token: nextPageToken}
}
stream, err := pa.permissionClient.LookupSubjects(ctx, &subjectsReq)
if err != nil {
return nil, "", errors.Wrap(errors.ErrMalformedEntity, errors.Wrap(errRetrievePolicies, err))
}
subjects := []*v1.LookupSubjectsResponse{}
var token string
for {
resp, err := stream.Recv()
switch err {
case nil:
subjects = append(subjects, resp)
case io.EOF:
if len(subjects) > 0 && subjects[len(subjects)-1].AfterResultCursor != nil {
token = subjects[len(subjects)-1].AfterResultCursor.Token
}
return subjectsToAuthPolicies(subjects), token, nil
default:
if len(subjects) > 0 && subjects[len(subjects)-1].AfterResultCursor != nil {
token = subjects[len(subjects)-1].AfterResultCursor.Token
}
return subjectsToAuthPolicies(subjects), token, errors.Wrap(errors.ErrViewEntity, err)
}
}
}
func (pa *policyAgent) RetrieveAllSubjects(ctx context.Context, pr auth.PolicyReq) ([]auth.PolicyRes, error) {
var tuples []auth.PolicyRes
nextPageToken := ""
for i := 0; ; i++ {
relationTuples, npt, err := pa.RetrieveSubjects(ctx, pr, nextPageToken, defRetrieveAllLimit)
if err != nil {
return tuples, err
}
tuples = append(tuples, relationTuples...)
if npt == "" || (len(tuples) < defRetrieveAllLimit) {
break
}
nextPageToken = npt
}
return tuples, nil
}
func (pa *policyAgent) RetrieveAllSubjectsCount(ctx context.Context, pr auth.PolicyReq) (int, error) {
var count int
nextPageToken := ""
for {
relationTuples, npt, err := pa.RetrieveSubjects(ctx, pr, nextPageToken, defRetrieveAllLimit)
if err != nil {
return count, err
}
count = count + len(relationTuples)
if npt == "" {
break
}
nextPageToken = npt
}
return count, nil
}
func objectsToAuthPolicies(objects []*v1.LookupResourcesResponse) []auth.PolicyRes {
var policies []auth.PolicyRes
for _, obj := range objects {
policies = append(policies, auth.PolicyRes{
Object: obj.GetResourceObjectId(),
})
}
return policies
}
func subjectsToAuthPolicies(subjects []*v1.LookupSubjectsResponse) []auth.PolicyRes {
var policies []auth.PolicyRes
for _, sub := range subjects {
policies = append(policies, auth.PolicyRes{
Subject: sub.Subject.GetSubjectObjectId(),
})
}
return policies
}
func (pa *policyAgent) Watch(continueToken string) {
stream, err := pa.client.WatchServiceClient.Watch(context.Background(), &v1.WatchRequest{
OptionalObjectTypes: []string{},
OptionalStartCursor: &v1.ZedToken{Token: continueToken},
})
if err != nil {
pa.logger.Error(fmt.Sprintf("got error while watching: %s", err.Error()))
}
for {
watchResp, err := stream.Recv()
switch err {
case nil:
pa.publishToStream(watchResp)
case io.EOF:
pa.logger.Info("got EOF while watch streaming")
return
default:
pa.logger.Error(fmt.Sprintf("got error while watch streaming : %s", err.Error()))
return
}
}
}
func (pa *policyAgent) publishToStream(resp *v1.WatchResponse) {
pa.logger.Info(fmt.Sprintf("Publish next token %s", resp.ChangesThrough.Token))
for _, update := range resp.Updates {
operation := v1.RelationshipUpdate_Operation_name[int32(update.Operation)]
objectType := update.Relationship.Resource.ObjectType
objectID := update.Relationship.Resource.ObjectId
relation := update.Relationship.Relation
subjectType := update.Relationship.Subject.Object.ObjectType
subjectRelation := update.Relationship.Subject.OptionalRelation
subjectID := update.Relationship.Subject.Object.ObjectId
pa.logger.Info(fmt.Sprintf(`
Operation : %s object_type: %s object_id: %s relation: %s subject_type: %s subject_relation: %s subject_id: %s
`, operation, objectType, objectID, relation, subjectType, subjectRelation, subjectID))
}
}
func (pa *policyAgent) addPolicyPreCondition(ctx context.Context, pr auth.PolicyReq) ([]*v1.Precondition, error) {
// Checks are required for following ( -> means adding)
// 1.) user -> group (both user groups and channels)
// 2.) user -> thing
// 3.) group -> group (both for adding parent_group and channels)
// 4.) group (channel) -> thing
switch {
// 1.) user -> group (both user groups and channels)
// Checks :
// - USER with ANY RELATION to DOMAIN
// - GROUP with DOMAIN RELATION to DOMAIN
case pr.SubjectType == auth.UserType && pr.ObjectType == auth.GroupType:
return pa.userGroupPreConditions(ctx, pr)
// 2.) user -> thing
// Checks :
// - USER with ANY RELATION to DOMAIN
// - THING with DOMAIN RELATION to DOMAIN
case pr.SubjectType == auth.UserType && pr.ObjectType == auth.ThingType:
return pa.userThingPreConditions(ctx, pr)
// 3.) group -> group (both for adding parent_group and channels)
// Checks :
// - CHILD_GROUP with out PARENT_GROUP RELATION with any GROUP
case pr.SubjectType == auth.GroupType && pr.ObjectType == auth.GroupType:
return groupPreConditions(pr)
// 4.) group (channel) -> thing
// Checks :
// - GROUP (channel) with DOMAIN RELATION to DOMAIN
// - NO GROUP should not have PARENT_GROUP RELATION with GROUP (channel)
// - THING with DOMAIN RELATION to DOMAIN
case pr.SubjectType == auth.GroupType && pr.ObjectType == auth.ThingType:
return channelThingPreCondition(pr)
// Check thing and group not belongs to other domain before adding to domain
case pr.SubjectType == auth.DomainType && pr.Relation == auth.DomainRelation && (pr.ObjectType == auth.ThingType || pr.ObjectType == auth.GroupType):
preconds := []*v1.Precondition{
{
Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: pr.ObjectType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
},
},
},
}
return preconds, nil
}
return nil, nil
}
func (pa *policyAgent) userGroupPreConditions(ctx context.Context, pr auth.PolicyReq) ([]*v1.Precondition, error) {
var preconds []*v1.Precondition
isSuperAdmin := false
if err := pa.CheckPolicy(ctx, auth.PolicyReq{
Subject: pr.Subject,
SubjectType: pr.SubjectType,
Permission: auth.AdminPermission,
Object: auth.MagistralaObject,
ObjectType: auth.PlatformType,
}); err == nil {
isSuperAdmin = true
}
if !isSuperAdmin {
preconds = append(preconds, &v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.DomainType,
OptionalResourceId: pr.Domain,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.UserType,
OptionalSubjectId: pr.Subject,
},
},
})
}
switch {
case pr.ObjectKind == auth.NewGroupKind || pr.ObjectKind == auth.NewChannelKind:
preconds = append(preconds,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
},
},
},
)
default:
preconds = append(preconds,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
OptionalSubjectId: pr.Domain,
},
},
},
)
}
return preconds, nil
}
func (pa *policyAgent) userThingPreConditions(ctx context.Context, pr auth.PolicyReq) ([]*v1.Precondition, error) {
var preconds []*v1.Precondition
isSuperAdmin := false
if err := pa.CheckPolicy(ctx, auth.PolicyReq{
Subject: pr.Subject,
SubjectType: pr.SubjectType,
Permission: auth.AdminPermission,
Object: auth.MagistralaObject,
ObjectType: auth.PlatformType,
}); err == nil {
isSuperAdmin = true
}
if !isSuperAdmin {
preconds = append(preconds, &v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.DomainType,
OptionalResourceId: pr.Domain,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.UserType,
OptionalSubjectId: pr.Subject,
},
},
})
}
switch {
// For New thing
// - THING without DOMAIN RELATION to ANY DOMAIN
case pr.ObjectKind == auth.NewThingKind:
preconds = append(preconds,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.ThingType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
},
},
},
)
default:
// For existing thing
// - THING without DOMAIN RELATION to ANY DOMAIN
preconds = append(preconds,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.ThingType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
OptionalSubjectId: pr.Domain,
},
},
},
)
}
return preconds, nil
}
func groupPreConditions(pr auth.PolicyReq) ([]*v1.Precondition, error) {
// - PARENT_GROUP (subject) with DOMAIN RELATION to DOMAIN
precond := []*v1.Precondition{
{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Subject,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
OptionalSubjectId: pr.Domain,
},
},
},
}
if pr.ObjectKind != auth.ChannelsKind {
precond = append(precond,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.ParentGroupRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.GroupType,
},
},
},
)
}
switch {
// - NEW CHILD_GROUP (object) with out DOMAIN RELATION to ANY DOMAIN
case pr.ObjectType == auth.GroupType && pr.ObjectKind == auth.NewGroupKind:
precond = append(precond,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
},
},
},
)
default:
// - CHILD_GROUP (object) with DOMAIN RELATION to DOMAIN
precond = append(precond,
&v1.Precondition{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
OptionalSubjectId: pr.Domain,
},
},
},
)
}
return precond, nil
}
func channelThingPreCondition(pr auth.PolicyReq) ([]*v1.Precondition, error) {
if pr.SubjectKind != auth.ChannelsKind {
return nil, errInvalidSubject
}
precond := []*v1.Precondition{
{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalResourceId: pr.Subject,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
OptionalSubjectId: pr.Domain,
},
},
},
{
Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.GroupType,
OptionalRelation: auth.ParentGroupRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.GroupType,
OptionalSubjectId: pr.Subject,
},
},
},
{
Operation: v1.Precondition_OPERATION_MUST_MATCH,
Filter: &v1.RelationshipFilter{
ResourceType: auth.ThingType,
OptionalResourceId: pr.Object,
OptionalRelation: auth.DomainRelation,
OptionalSubjectFilter: &v1.SubjectFilter{
SubjectType: auth.DomainType,
OptionalSubjectId: pr.Domain,
},
},
},
}
return precond, nil
}