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 all 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
1 change: 1 addition & 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.2-0.20200828091056-47af10a0e872
github.com/prometheus/client_golang v1.7.1
github.com/restic/calens v0.2.0
github.com/rs/zerolog v1.19.0
Expand Down
86 changes: 86 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
66 changes: 66 additions & 0 deletions middleware/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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"
)

// Roles manages a roles.Cache by fetching and inserting roles unknown by the cache.
// Relevant roleIDs are extracted from the metadata context, i.e. the roleIDs of the authenticated user.
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
}
93 changes: 93 additions & 0 deletions roles/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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 cache implementation for roles, keyed by roleIDs.
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 permission-setting by 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 entries that exceeded the cache TTL.
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.
func (c *Cache) Length() int {
return len(c.entries)
}

// fits returns whether the cache is at full capacity.
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
}