MF-1901 - Return `ThingID` on Authorize (#1902)

* Combine actions and thingID to cache

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* Change signature of things cache

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* Fix Tests

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* Add cached policy stuct

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* Change contents of cached policy

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* Set all values for cachedPolicy after missing values in cache

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

---------

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
This commit is contained in:
b1ackd0t 2023-09-06 16:07:47 +03:00 committed by GitHub
parent a008440dcc
commit 65b6374235
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 54 deletions

View File

@ -497,10 +497,6 @@ func TestListMembers(t *testing.T) {
}
for _, tc := range cases {
fmt.Println()
fmt.Println(tc.desc)
fmt.Println()
repoCall := pRepo.On("CheckAdmin", mock.Anything, mock.Anything).Return(nil)
repoCall1 := cRepo.On("Members", mock.Anything, tc.groupID, mock.Anything).Return(mfclients.MembersPage{Members: convertClients(tc.response)}, tc.err)
membersPage, err := mfsdk.Members(tc.groupID, tc.page, tc.token)

View File

@ -5,7 +5,6 @@ package mocks
import (
"context"
"fmt"
"strings"
"sync"
@ -13,6 +12,8 @@ import (
"github.com/mainflux/mainflux/things/policies"
)
const separator = ":"
type cacheMock struct {
mu sync.Mutex
policies map[string]string
@ -25,34 +26,75 @@ func NewCache() policies.Cache {
}
}
func (ccm *cacheMock) Put(_ context.Context, policy policies.Policy) error {
func (ccm *cacheMock) Put(_ context.Context, policy policies.CachedPolicy) error {
ccm.mu.Lock()
defer ccm.mu.Unlock()
ccm.policies[fmt.Sprintf("%s:%s", policy.Subject, policy.Object)] = strings.Join(policy.Actions, ":")
key, value := kv(policy)
ccm.policies[key] = value
return nil
}
func (ccm *cacheMock) Get(_ context.Context, policy policies.Policy) (policies.Policy, error) {
func (ccm *cacheMock) Get(_ context.Context, policy policies.CachedPolicy) (policies.CachedPolicy, error) {
ccm.mu.Lock()
defer ccm.mu.Unlock()
actions := ccm.policies[fmt.Sprintf("%s:%s", policy.Subject, policy.Object)]
if actions != "" {
return policies.Policy{
Subject: policy.Subject,
Object: policy.Object,
Actions: strings.Split(actions, ":"),
}, nil
key, _ := kv(policy)
val := ccm.policies[key]
if val == "" {
return policies.CachedPolicy{}, errors.ErrNotFound
}
return policies.Policy{}, errors.ErrNotFound
thingID := extractThingID(val)
if thingID == "" {
return policies.CachedPolicy{}, errors.ErrNotFound
}
policy.Actions = separateActions(val)
policy.ThingID = thingID
return policy, nil
}
func (ccm *cacheMock) Remove(_ context.Context, policy policies.Policy) error {
func (ccm *cacheMock) Remove(_ context.Context, policy policies.CachedPolicy) error {
ccm.mu.Lock()
defer ccm.mu.Unlock()
delete(ccm.policies, fmt.Sprintf("%s:%s", policy.Subject, policy.Object))
key, _ := kv(policy)
delete(ccm.policies, key)
return nil
}
// kv is used to create a key-value pair for caching.
func kv(p policies.CachedPolicy) (string, string) {
key := p.ThingKey + separator + p.ChannelID
val := strings.Join(p.Actions, separator)
if p.ThingID != "" {
val += separator + p.ThingID
}
return key, val
}
// separateActions is used to separate the actions from the cache values.
func separateActions(actions string) []string {
return strings.Split(actions, separator)
}
// extractThingID is used to extract the thingID from the cache values.
func extractThingID(actions string) string {
var lastIdx = strings.LastIndex(actions, separator)
thingID := actions[lastIdx+1:]
// check if the thingID is a valid UUID
if len(thingID) != 36 {
return ""
}
return thingID
}

View File

@ -95,16 +95,23 @@ type Service interface {
ListPolicies(ctx context.Context, token string, p Page) (PolicyPage, error)
}
type CachedPolicy struct {
ThingID string
ThingKey string
ChannelID string
Actions []string
}
// Cache contains channel-thing connection caching interface.
type Cache interface {
// Put adds policy to cahce.
Put(ctx context.Context, policy Policy) error
Put(ctx context.Context, policy CachedPolicy) error
// Get retrieves policy from cache.
Get(ctx context.Context, policy Policy) (Policy, error)
Get(ctx context.Context, policy CachedPolicy) (CachedPolicy, error)
// Remove deletes a policy from cache.
Remove(ctx context.Context, policy Policy) error
Remove(ctx context.Context, policy CachedPolicy) error
}
// validate returns an error if policy representation is invalid.

View File

@ -5,7 +5,6 @@ package redis
import (
"context"
"fmt"
"strings"
"time"
@ -25,47 +24,85 @@ type pcache struct {
// NewCache returns redis policy cache implementation.
func NewCache(client *redis.Client, duration time.Duration) policies.Cache {
return pcache{
return &pcache{
client: client,
keyDuration: duration,
}
}
func (pc pcache) Put(ctx context.Context, policy policies.Policy) error {
k, v := kv(policy)
if err := pc.client.Set(ctx, k, v, pc.keyDuration).Err(); err != nil {
func (pc *pcache) Put(ctx context.Context, policy policies.CachedPolicy) error {
key, value := kv(policy)
if err := pc.client.Set(ctx, key, value, pc.keyDuration).Err(); err != nil {
return errors.Wrap(errors.ErrCreateEntity, err)
}
return nil
}
func (pc pcache) Get(ctx context.Context, policy policies.Policy) (policies.Policy, error) {
k, _ := kv(policy)
res := pc.client.Get(ctx, k)
func (pc *pcache) Get(ctx context.Context, policy policies.CachedPolicy) (policies.CachedPolicy, error) {
key, _ := kv(policy)
res := pc.client.Get(ctx, key)
// Nil response indicates non-existent key in Redis client.
if res == nil || res.Err() == redis.Nil {
return policies.Policy{}, errors.ErrNotFound
return policies.CachedPolicy{}, errors.ErrNotFound
}
if err := res.Err(); err != nil {
return policies.Policy{}, err
return policies.CachedPolicy{}, err
}
actions, err := res.Result()
val, err := res.Result()
if err != nil {
return policies.Policy{}, err
return policies.CachedPolicy{}, err
}
policy.Actions = strings.Split(actions, separator)
thingID := extractThingID(val)
if thingID == "" {
return policies.CachedPolicy{}, errors.ErrNotFound
}
policy.ThingID = thingID
policy.Actions = separateActions(val)
return policy, nil
}
func (pc pcache) Remove(ctx context.Context, policy policies.Policy) error {
obj, _ := kv(policy)
if err := pc.client.Del(ctx, obj).Err(); err != nil {
func (pc *pcache) Remove(ctx context.Context, policy policies.CachedPolicy) error {
key, _ := kv(policy)
if err := pc.client.Del(ctx, key).Err(); err != nil {
return errors.Wrap(errors.ErrRemoveEntity, err)
}
return nil
}
// Generates key-value pair for Redis client.
func kv(p policies.Policy) (string, string) {
return fmt.Sprintf("%s%s%s", p.Subject, separator, p.Object), strings.Join(p.Actions, separator)
// kv is used to create a key-value pair for caching.
func kv(p policies.CachedPolicy) (string, string) {
key := p.ThingKey + separator + p.ChannelID
val := strings.Join(p.Actions, separator)
if p.ThingID != "" {
val += separator + p.ThingID
}
return key, val
}
// separateActions is used to separate the actions from the cache values.
func separateActions(actions string) []string {
return strings.Split(actions, separator)
}
// extractThingID is used to extract the thingID from the cache values.
func extractThingID(actions string) string {
var lastIdx = strings.LastIndex(actions, separator)
thingID := actions[lastIdx+1:]
// check if the thingID is a valid UUID
if len(thingID) != 36 {
return ""
}
return thingID
}

View File

@ -52,24 +52,32 @@ func NewService(auth upolicies.AuthServiceClient, p Repository, ccache Cache, id
func (svc service) Authorize(ctx context.Context, ar AccessRequest) (Policy, error) {
// Fetch from cache first.
policy := Policy{
Subject: ar.Subject,
Object: ar.Object,
cpolicy := CachedPolicy{
ThingKey: ar.Subject,
ChannelID: ar.Object,
}
policy, err := svc.policyCache.Get(ctx, policy)
cpolicy, err := svc.policyCache.Get(ctx, cpolicy)
if err == nil {
for _, action := range policy.Actions {
for _, action := range cpolicy.Actions {
if action == ar.Action {
var policy = Policy{
Subject: cpolicy.ThingID,
}
return policy, nil
}
}
return Policy{}, errors.ErrAuthorization
}
if !errors.Contains(err, errors.ErrNotFound) {
return Policy{}, err
}
// Fetch from repo as a fallback if not found in cache.
// Fetch from database as a fallback if not found in cache.
var policy Policy
switch ar.Entity {
case GroupEntityType:
policy, err = svc.policies.EvaluateGroupAccess(ctx, ar)
@ -84,14 +92,18 @@ func (svc service) Authorize(ctx context.Context, ar AccessRequest) (Policy, err
}
case ThingEntityType:
policy, err := svc.policies.EvaluateMessagingAccess(ctx, ar)
policy, err = svc.policies.EvaluateMessagingAccess(ctx, ar)
if err != nil {
return Policy{}, err
}
// Replace Subject since AccessRequest Subject is Thing Key,
// and Policy subject is Thing ID.
policy.Subject = ar.Subject
if err := svc.policyCache.Put(ctx, policy); err != nil {
cpolicy = CachedPolicy{
ThingID: policy.Subject,
ThingKey: ar.Subject,
ChannelID: ar.Object,
Actions: policy.Actions,
}
if err := svc.policyCache.Put(ctx, cpolicy); err != nil {
return policy, err
}
@ -126,7 +138,11 @@ func (svc service) AddPolicy(ctx context.Context, token string, external bool, p
p.UpdatedAt = time.Now()
p.UpdatedBy = userID
if err := svc.policyCache.Remove(ctx, p); err != nil {
var cpolicy = CachedPolicy{
ThingKey: p.Subject,
ChannelID: p.Object,
}
if err := svc.policyCache.Remove(ctx, cpolicy); err != nil {
return Policy{}, err
}
@ -193,7 +209,11 @@ func (svc service) UpdatePolicy(ctx context.Context, token string, p Policy) (Po
p.UpdatedAt = time.Now()
p.UpdatedBy = userID
if err := svc.policyCache.Remove(ctx, p); err != nil {
var cpolicy = CachedPolicy{
ThingKey: p.Subject,
ChannelID: p.Object,
}
if err := svc.policyCache.Remove(ctx, cpolicy); err != nil {
return Policy{}, err
}
@ -228,9 +248,14 @@ func (svc service) DeletePolicy(ctx context.Context, token string, p Policy) err
return err
}
if err := svc.policyCache.Remove(ctx, p); err != nil {
var cpolicy = CachedPolicy{
ThingKey: p.Subject,
ChannelID: p.Object,
}
if err := svc.policyCache.Remove(ctx, cpolicy); err != nil {
return err
}
return svc.policies.Delete(ctx, p)
}