Skip to content

Commit

Permalink
all: Add unary hook that preloads rights in the context
Browse files Browse the repository at this point in the history
  • Loading branch information
gomezjdaniel authored and johanstokking committed Apr 4, 2018
1 parent 0e88969 commit 60ab25e
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 210 deletions.
@@ -1,6 +1,6 @@
// Copyright © 2018 The Things Network Foundation, distributed under the MIT license (see LICENSE file)

package identityserver
package rights

import "github.com/TheThingsNetwork/ttn/pkg/ttnpb"

Expand All @@ -11,5 +11,5 @@ import "github.com/TheThingsNetwork/ttn/pkg/ttnpb"
type cache interface {
// GetOrFetch gets the entry with the given key and loads it in the cache using
// the given `fetch` method if it is not found.
GetOrFetch(auth, entityID string, fetch func() ([]ttnpb.Right, error)) ([]ttnpb.Right, error)
GetOrFetch(key string, fetch func() ([]ttnpb.Right, error)) ([]ttnpb.Right, error)
}
@@ -1,12 +1,12 @@
// Copyright © 2018 The Things Network Foundation, distributed under the MIT license (see LICENSE file)

package identityserver
package rights

import "github.com/TheThingsNetwork/ttn/pkg/ttnpb"

// noopCache implements cache and caches nothing.
type noopCache struct{}

func (c *noopCache) GetOrFetch(auth, entityID string, fetch func() ([]ttnpb.Right, error)) ([]ttnpb.Right, error) {
func (c *noopCache) GetOrFetch(key string, fetch func() ([]ttnpb.Right, error)) ([]ttnpb.Right, error) {
return fetch()
}
@@ -1,37 +1,41 @@
// Copyright © 2018 The Things Network Foundation, distributed under the MIT license (see LICENSE file)

package identityserver
package rights

import (
"fmt"
"context"
"sync"
"time"

"github.com/TheThingsNetwork/ttn/pkg/log"
"github.com/TheThingsNetwork/ttn/pkg/ttnpb"
)

// ttlCache is a cache where all entries have a TTL and are garbage collected.
type ttlCache struct {
ctx context.Context
logger log.Interface
ttl time.Duration
mu sync.RWMutex
entries map[string]*entry
}

// newTTLCache creates a new instance of ttlCache.
func newTTLCache(ttl time.Duration) *ttlCache {
// newTTLCache creates a new instance of ttlCache and starts a goroutine for
// the garbage collector.
func newTTLCache(ctx context.Context, ttl time.Duration) *ttlCache {
c := &ttlCache{
ctx: ctx,
logger: log.FromContext(ctx),
ttl: ttl,
entries: make(map[string]*entry),
}
go c.garbageCollector()
return c
}

// GetOrFetch returns the rights of the given key or uses the given `fetch` function
// to loads them in the cache and returns it if the key is expired or not found.
func (c *ttlCache) GetOrFetch(auth, entityID string, fetch func() ([]ttnpb.Right, error)) ([]ttnpb.Right, error) {
key := fmt.Sprintf("%s:%s", auth, entityID)

// GetOrFetch returns the cached rights of the given key or calls `fetch` to load
// them if they cache entry is expired or not found.
func (c *ttlCache) GetOrFetch(key string, fetch func() ([]ttnpb.Right, error)) ([]ttnpb.Right, error) {
c.mu.Lock()
defer c.mu.Unlock()

Expand All @@ -40,7 +44,7 @@ func (c *ttlCache) GetOrFetch(auth, entityID string, fetch func() ([]ttnpb.Right
return e.value, nil
}

// otherwise fetch
// Otherwise fetch.
rights, err := fetch()
if err != nil {
return nil, err
Expand All @@ -58,14 +62,19 @@ func (c *ttlCache) garbageCollector() {
ticker := time.NewTicker(c.ttl)
defer ticker.Stop()
for {
<-ticker.C
c.mu.Lock()
for key, e := range c.entries {
if e.IsExpired(c.ttl) {
delete(c.entries, key)
select {
case <-c.ctx.Done():
c.logger.WithError(c.ctx.Err()).Info("TTL cache garbage collector has been stopped")
return
case <-ticker.C:
c.mu.Lock()
for key, e := range c.entries {
if e.IsExpired(c.ttl) {
delete(c.entries, key)
}
}
c.mu.Unlock()
}
c.mu.Unlock()
}
}

Expand Down
68 changes: 68 additions & 0 deletions pkg/auth/rights/cache_ttl_test.go
@@ -0,0 +1,68 @@
// Copyright © 2018 The Things Network Foundation, distributed under the MIT license (see LICENSE file)

package rights

import (
"context"
"testing"
"time"

"github.com/TheThingsNetwork/ttn/pkg/log"
"github.com/TheThingsNetwork/ttn/pkg/ttnpb"
"github.com/TheThingsNetwork/ttn/pkg/util/test"
"github.com/smartystreets/assertions"
"github.com/smartystreets/assertions/should"
)

// Make sure in compile time that noopCache and ttlCache implements cache.
var (
_ cache = new(noopCache)
_ cache = new(ttlCache)
)

func TestCacheTTL(t *testing.T) {
a := assertions.New(t)
ctx, cancel := context.WithCancel(log.NewContextWithField(log.NewContext(context.Background(), test.GetLogger(t)), "hook", "rights"))
cache := newTTLCache(ctx, time.Duration(time.Millisecond*200))

key := "foo"

rights, err := cache.GetOrFetch(key, func() ([]ttnpb.Right, error) {
return []ttnpb.Right{ttnpb.Right(1)}, nil
})
a.So(err, should.BeNil)
a.So(rights, should.Resemble, []ttnpb.Right{ttnpb.Right(1)})
a.So(cache.entries, should.HaveLength, 1)

// Although fetch function is different the previous response is still cached.
rights, err = cache.GetOrFetch(key, func() ([]ttnpb.Right, error) {
return []ttnpb.Right{ttnpb.Right(2)}, nil
})
a.So(err, should.BeNil)
a.So(rights, should.Resemble, []ttnpb.Right{ttnpb.Right(1)})
a.So(cache.entries, should.HaveLength, 1)

// Sleep for 250 milliseconds so the entry expires.
time.Sleep(time.Millisecond * 250)

// Entry has been garbage collected.
a.So(cache.entries, should.HaveLength, 0)

// Stop the garbage collector.
cancel()

// Fetch again with different response.
rights, err = cache.GetOrFetch(key, func() ([]ttnpb.Right, error) {
return []ttnpb.Right{ttnpb.Right(2)}, nil
})
a.So(err, should.BeNil)
a.So(rights, should.Resemble, []ttnpb.Right{ttnpb.Right(2)})
a.So(cache.entries, should.HaveLength, 1)

// Sleep for 250 milliseconds again.
time.Sleep(time.Millisecond * 250)

// Check that the entry has not been garbage collected.
a.So(cache.entries, should.HaveLength, 1)
a.So(cache.entries, should.ContainKey, key)
}
26 changes: 26 additions & 0 deletions pkg/auth/rights/context.go
@@ -0,0 +1,26 @@
// Copyright © 2018 The Things Network Foundation, distributed under the MIT license (see LICENSE file)

package rights

import (
"context"

"github.com/TheThingsNetwork/ttn/pkg/ttnpb"
)

type rightsKey int

const key rightsKey = 1

// NewContext returns ctx with the given rights within.
func NewContext(ctx context.Context, rights []ttnpb.Right) context.Context {
return context.WithValue(ctx, key, rights)
}

// FromContext returns the rights from ctx, otherwise empty slice if they are not found.
func FromContext(ctx context.Context) []ttnpb.Right {
if r, ok := ctx.Value(key).([]ttnpb.Right); ok {
return r
}
return make([]ttnpb.Right, 0)
}
48 changes: 48 additions & 0 deletions pkg/auth/rights/doc.go
@@ -0,0 +1,48 @@
// Copyright © 2018 The Things Network Foundation, distributed under the MIT license (see LICENSE file)

/*
Package rights implements a gRPC Unary hook that preload rights in the context.
In order to preload the rights the following steps are taken:
1. The hook looks if the request message implements one of either interfaces:
type applicationIdentifiersGetters interface {
GetApplicationID() string
}
type gatewayIdentifiersGetters interface {
GetGatewayID() string
}
If the message implements both interfaces only applicationIdentifiers is taken
into account.
2. The hook gets a gRPC connection to an Identity Server through the
IdentityServerConnector interface and then calls either the ListApplicationRights
or ListGatewayRights method of the Identity Server using the authorization
value of the original request.
Optionally the hook can set up a TTL cache whenever the Config.TTL value is
different to its zero value.
3. The resulting rights are put in the context.
Lastly, the way to check the rights in the protected Unary method is a matter of:
import (
"github.com/TheThingsNetwork/ttn/pkg/auth"
"github.com/TheThingsNetwork/ttn/pkg/auth/rights"
"github.com/TheThingsNetwork/ttn/pkg/ttnpb"
)
func (a *ApplicationServer) MyMethod(ctx context.Context, ids *ttnpb.ApplicationIdentifiers) (*pbtypes.Empty, error) {
if !ttnpb.IncludesRights(rights.FromContext(ctx), ttnpb.RIGHT_APPLICATION_TRAFFIC_READ) {
return nil, auth.ErrNotAuthorized.New(nil)
}
...
....
}
*/
package rights

0 comments on commit 60ab25e

Please sign in to comment.