Skip to content
This repository has been archived by the owner on Jan 27, 2021. It is now read-only.

roleIDs into context helper + roles cache + roles cache update middleware #59

Merged
merged 8 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/role-ids-from-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Change: Unwrap roleIDs from access-token into metadata context

We pass the RoleIDs from the access-token into the metadata context.

https://github.com/owncloud/ocis-pkg/pull/59
7 changes: 7 additions & 0 deletions changelog/unreleased/roles-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Change: Provide cache for roles

In order to work efficiently with permissions we provide a cache for roles and a
middleware to update the cache based on roleIDs from the metadata context. It can be
used to check permissions in service handlers.

https://github.com/owncloud/ocis-pkg/pull/59
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/micro/cli/v2 v2.1.2
github.com/micro/go-micro/v2 v2.9.1
github.com/micro/go-plugins/wrapper/trace/opencensus/v2 v2.9.1
github.com/owncloud/ocis-settings v0.3.1
github.com/prometheus/client_golang v1.7.1
github.com/restic/calens v0.2.0
github.com/rs/zerolog v1.19.0
Expand All @@ -23,3 +24,5 @@ require (
)

replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

replace github.com/owncloud/ocis-settings => ../ocis-settings
kulmann marked this conversation as resolved.
Show resolved Hide resolved
84 changes: 84 additions & 0 deletions go.sum

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions middleware/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func newAccountOptions(opts ...account.Option) account.Options {

// AccountID serves as key for the account uuid in the context
const AccountID string = "Account-Id"
// RoleIDs serves as key for the roles in the context
const RoleIDs string = "Role-Ids"
// UUIDKey serves as key for the account uuid in the context
// Deprecated: UUIDKey exists for compatibility reasons. Use AccountID instead.
var UUIDKey struct{}
Expand Down Expand Up @@ -54,7 +56,9 @@ func ExtractAccountUUID(opts ...account.Option) func(http.Handler) http.Handler
// Important: user.Id.OpaqueId is the AccountUUID. Set this way in the account uuid middleware in ocis-proxy.
// https://github.com/owncloud/ocis-proxy/blob/ea254d6036592cf9469d757d1295e0c4309d1e63/pkg/middleware/account_uuid.go#L109
ctx := context.WithValue(r.Context(), UUIDKey, user.Id.OpaqueId)
// TODO: implement token manager in cs3org/reva that uses generic metadata instead of access token from header.
ctx = metadata.Set(ctx, AccountID, user.Id.OpaqueId)
ctx = metadata.Set(ctx, RoleIDs, string(user.Opaque.Map["roles"].Value))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Expand Down
64 changes: 64 additions & 0 deletions middleware/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package middleware

import (
"context"
"encoding/json"
"net/http"

"github.com/micro/go-micro/v2/metadata"
"github.com/owncloud/ocis-pkg/v2/log"
"github.com/owncloud/ocis-pkg/v2/roles"
settings "github.com/owncloud/ocis-settings/pkg/proto/v0"
)

func Roles(log log.Logger, rs settings.RoleService, cache *roles.Cache) func(next http.Handler) http.Handler {
kulmann marked this conversation as resolved.
Show resolved Hide resolved
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// get roleIDs from context
roleIDs, ok := ReadRoleIDsFromContext(r.Context())
if !ok {
log.Debug().Msg("failed to read roleIDs from context")
next.ServeHTTP(w, r)
return
}

// check which roles are not cached, yet
lookup := make([]string, 0)
for _, roleID := range roleIDs {
if hit := cache.Get(roleID); hit == nil {
lookup = append(lookup, roleID)
}
}

// fetch roles
if len(lookup) > 0 {
request := &settings.ListBundlesRequest{
BundleIds: lookup,
}
res, err := rs.ListRoles(r.Context(), request)
if err != nil {
log.Debug().Err(err).Msg("failed to fetch roles by roleIDs")
next.ServeHTTP(w, r)
return
}
for _, role := range res.Bundles {
cache.Set(role.Id, role)
}
}
next.ServeHTTP(w, r)
})
}
}

// ReadRoleIDsFromContext extracts roleIDs from the metadata context and returns them as []string
func ReadRoleIDsFromContext(ctx context.Context) (roleIDs []string, ok bool) {
roleIDsJson, ok := metadata.Get(ctx, RoleIDs)
if !ok {
return nil, false
}
err := json.Unmarshal([]byte(roleIDsJson), &roleIDs)
if err != nil {
return nil, false
}
return roleIDs, true
}
92 changes: 92 additions & 0 deletions roles/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package roles

import (
"sync"
"time"

settings "github.com/owncloud/ocis-settings/pkg/proto/v0"
)

// entry extends a bundle and adds a TTL
type entry struct {
*settings.Bundle
inserted time.Time
}

// Cache is a barebones cache implementation.
kulmann marked this conversation as resolved.
Show resolved Hide resolved
type Cache struct {
entries map[string]entry
size int
ttl time.Duration
m sync.Mutex
}

// NewCache returns a new instance of Cache.
func NewCache(o ...Option) Cache {
opts := newOptions(o...)

return Cache{
size: opts.size,
ttl: opts.ttl,
entries: map[string]entry{},
}
}

// Get gets a role-bundle by a given `roleID`.
func (c *Cache) Get(roleID string) *settings.Bundle {
c.m.Lock()
defer c.m.Unlock()

if _, ok := c.entries[roleID]; ok {
return c.entries[roleID].Bundle
}
return nil
}

// FindPermissionById searches for a setting with the permissionID, but limited to the given roleIDs
func (c *Cache) FindPermissionById(roleIDs []string, permissionID string) *settings.Setting {
for _, roleID := range roleIDs {
role := c.Get(roleID)
if role != nil {
for _, setting := range role.Settings {
if setting.Id == permissionID {
return setting
}
}
}
}
return nil
}

// Set sets a roleID / role-bundle.
func (c *Cache) Set(roleID string, value *settings.Bundle) {
c.m.Lock()
defer c.m.Unlock()

if !c.fits() {
c.evict()
}

c.entries[roleID] = entry{
value,
time.Now(),
}
}

// Evict frees memory from the cache by removing invalid keys. It is a noop.
func (c *Cache) evict() {
for i := range c.entries {
if c.entries[i].inserted.Add(c.ttl).Before(time.Now()) {
delete(c.entries, i)
}
}
}

// Length returns the amount of entries per service key.
func (c *Cache) Length() int {
return len(c.entries)
}

func (c *Cache) fits() bool {
return c.size >= len(c.entries)
}
38 changes: 38 additions & 0 deletions roles/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package roles

import (
"time"
)

// Options are all the possible options.
type Options struct {
size int
ttl time.Duration
}

// Option mutates option
type Option func(*Options)

// Size configures the size of the cache in items.
func Size(s int) Option {
return func(o *Options) {
o.size = s
}
}

// TTL rebuilds the cache after the configured duration.
func TTL(ttl time.Duration) Option {
return func(o *Options) {
o.ttl = ttl
}
}

func newOptions(opts ...Option) Options {
o := Options{}

for _, v := range opts {
v(&o)
}

return o
}