Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mint scope-based access tokens for RBAC #1669

Merged
merged 13 commits into from
May 7, 2021
16 changes: 16 additions & 0 deletions changelog/unreleased/scope-based-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Enhancement: Mint scope-based access tokens for RBAC

Primarily, this PR is meant to introduce the concept of scopes into our tokens.
At the moment, it addresses those cases where we impersonate other users without
allowing the full scope of what the actual user has access to.

A short explanation for how it works for public shares:
- We get the public share using the token provided by the client.
- In the public share, we know the resource ID, so we can add this to the
allowed scope, but not the path.
- However, later OCDav tries to access by path as well. Now this is not allowed
at the moment. However, from the allowed scope, we have the resource ID and
we're allowed to stat that. We stat the resource ID, get the path and if the
path matches the one passed by OCDav, we allow the request to go through.

https://github.com/cs3org/reva/pull/1669
6 changes: 6 additions & 0 deletions examples/storage-references/gateway.toml
Original file line number Diff line number Diff line change
@@ -9,10 +9,16 @@ home_provider = "/home"
[grpc.services.storageregistry.drivers.static.rules]
"/home" = {"address" = "localhost:17000"}
"/reva" = {"address" = "localhost:18000"}
"/public" = {"address" = "localhost:16000"}
"123e4567-e89b-12d3-a456-426655440000" = {"address" = "localhost:18000"}

[grpc.services.authprovider]
[grpc.services.authregistry]

[grpc.services.authregistry.drivers.static.rules]
basic = "localhost:19000"
publicshares = "localhost:16000"

[grpc.services.userprovider]
[grpc.services.usershareprovider]
[grpc.services.groupprovider]
15 changes: 15 additions & 0 deletions examples/storage-references/storage-public.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[grpc]
address = "0.0.0.0:16000"

[grpc.services.publicstorageprovider]
driver = "localhome"
mount_path = "/public"
mount_id = "123e4567-e89b-12d3-a456-426655440000"
data_server_url = "http://localhost:16001/data"
gateway_addr = "localhost:19000"

[grpc.services.authprovider]
auth_manager = "publicshares"

[grpc.services.authprovider.auth_managers.publicshares]
gateway_addr = "localhost:19000"
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ require (
github.com/cheggaaa/pb v1.0.29
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535
github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59
github.com/go-ldap/ldap/v3 v3.3.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -144,8 +144,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535 h1:555D8A3ddKqb4OyK9v5mdphw2zDLWKGXOkcnf1RQwTA=
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4 h1:lihiUwqal+sO+57VTHGRvHbI9baN+D85fPZG2N1Sk6s=
github.com/cs3org/go-cs3apis v0.0.0-20210507060801-f176760d55f4/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
116 changes: 102 additions & 14 deletions internal/grpc/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
@@ -20,10 +20,18 @@ package auth

import (
"context"
"strings"

userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/cs3org/reva/pkg/token"
tokenmgr "github.com/cs3org/reva/pkg/token/manager/registry"
"github.com/cs3org/reva/pkg/user"
@@ -41,6 +49,7 @@ type config struct {
// for SkipMethods.
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
GatewayAddr string `mapstructure:"gateway_addr"`
}

func parseConfig(m map[string]interface{}) (*config, error) {
@@ -64,6 +73,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
conf.GatewayAddr = sharedconf.GetGatewaySVC(conf.GatewayAddr)

h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
@@ -88,7 +98,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
// to decide the storage provider.
tkn, ok := token.ContextGetToken(ctx)
if ok {
u, err := tokenManager.DismantleToken(ctx, tkn)
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr)
if err == nil {
ctx = user.ContextSetUser(ctx, u)
}
@@ -105,10 +115,10 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
return nil, status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}

// validate the token
u, err := tokenManager.DismantleToken(ctx, tkn)
// validate the token and ensure access to the resource is allowed
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr)
if err != nil {
log.Warn().Msg("access token is invalid")
log.Warn().Err(err).Msg("access token is invalid")
return nil, status.Errorf(codes.Unauthenticated, "auth: core access token is invalid")
}

@@ -159,7 +169,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
// to decide the storage provider.
tkn, ok := token.ContextGetToken(ctx)
if ok {
u, err := tokenManager.DismantleToken(ctx, tkn)
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr)
if err == nil {
ctx = user.ContextSetUser(ctx, u)
ss = newWrappedServerStream(ctx, ss)
@@ -176,19 +186,13 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
return status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}

// validate the token
claims, err := tokenManager.DismantleToken(ctx, tkn)
// validate the token and ensure access to the resource is allowed
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr)
if err != nil {
log.Warn().Msg("access token invalid")
log.Warn().Err(err).Msg("access token is invalid")
return status.Errorf(codes.Unauthenticated, "auth: core access token is invalid")
}

u := &userpb.User{}
if err := mapstructure.Decode(claims, u); err != nil {
log.Warn().Msg("user claims invalid")
return status.Errorf(codes.Unauthenticated, "auth: claims are invalid")
}

// store user and core access token in context.
ctx = user.ContextSetUser(ctx, u)
wrapped := newWrappedServerStream(ctx, ss)
@@ -209,3 +213,87 @@ type wrappedServerStream struct {
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}

func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string) (*userpb.User, error) {
log := appctx.GetLogger(ctx)
u, tokenScope, err := mgr.DismantleToken(ctx, tkn)
if err != nil {
return nil, err
}

// Check if access to the resource is in the scope of the token
ok, err := scope.VerifyScope(tokenScope, req)
if err != nil {
return nil, errtypes.InternalError("error verifying scope of access token")
}
if ok {
return u, nil
}

// Check if req is of type *provider.Reference_Path
// If yes, the request might be coming from a share where the accessor is
// trying to impersonate the owner, since the share manager doesn't know the
// share path.
if ref, ok := extractRef(req); ok {
if ref.GetPath() != "" {

// Try to extract the resource ID from the scope resource.
// Currently, we only check for public shares, but this will be extended
// for OCM shares, guest accounts, etc.
log.Info().Msgf("resolving path reference to ID to check token scope %+v", ref.GetPath())
var share link.PublicShare
err = utils.UnmarshalJSONToProtoV1(tokenScope["publicshare"].Resource.Value, &share)
if err != nil {
return nil, err
}

client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return nil, err
}

// Since the public share is obtained from the scope, the current token
// has access to it.
statReq := &provider.StatRequest{
Ref: &provider.Reference{
Spec: &provider.Reference_Id{Id: share.ResourceId},
},
}

statResponse, err := client.Stat(ctx, statReq)
if err != nil || statResponse.Status.Code != rpc.Code_CODE_OK {
return nil, err
}

if strings.HasPrefix(ref.GetPath(), statResponse.Info.Path) {
// The path corresponds to the resource to which the token has access.
// We allow access to it.
return u, nil
}
}
}

return nil, err
}

func extractRef(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
case *registry.GetStorageProvidersRequest:
return v.GetRef(), true
case *provider.StatRequest:
return v.GetRef(), true
case *provider.ListContainerRequest:
return v.GetRef(), true
case *provider.CreateContainerRequest:
return v.GetRef(), true
case *provider.DeleteRequest:
return v.GetRef(), true
case *provider.MoveRequest:
return v.GetSource(), true
case *provider.InitiateFileDownloadRequest:
return v.GetRef(), true
case *provider.InitiateFileUploadRequest:
return v.GetRef(), true
}
return nil, false
}
4 changes: 4 additions & 0 deletions internal/grpc/services/appprovider/appprovider.go
Original file line number Diff line number Diff line change
@@ -275,3 +275,7 @@ func (s *service) OpenFileInAppProvider(ctx context.Context, req *providerpb.Ope
AppProviderUrl: appProviderURL,
}, nil
}

func (s *service) OpenInApp(ctx context.Context, req *providerpb.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
return nil, errtypes.NotSupported("Unimplemented")
}
7 changes: 4 additions & 3 deletions internal/grpc/services/authprovider/authprovider.go
Original file line number Diff line number Diff line change
@@ -108,13 +108,14 @@ func (s *service) Authenticate(ctx context.Context, req *provider.AuthenticateRe
username := req.ClientId
password := req.ClientSecret

u, err := s.authmgr.Authenticate(ctx, username, password)
u, scope, err := s.authmgr.Authenticate(ctx, username, password)
switch v := err.(type) {
case nil:
log.Info().Msgf("user %s authenticated", u.String())
return &provider.AuthenticateResponse{
Status: status.NewOK(ctx),
User: u,
Status: status.NewOK(ctx),
User: u,
TokenScope: scope,
}, nil
case errtypes.InvalidCredentials:
return &provider.AuthenticateResponse{
14 changes: 6 additions & 8 deletions internal/grpc/services/gateway/authprovider.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import (
"context"
"fmt"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -84,18 +85,15 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest
}, nil
}

uid := res.User.Id
if uid == nil {
if res.User.Id == nil {
err := errtypes.NotFound("gateway: uid after Authenticate is nil")
log.Err(err).Msg("user id is nil")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, err, "user id is nil"),
}, nil
}

user := res.User

token, err := s.tokenmgr.MintToken(ctx, user)
token, err := s.tokenmgr.MintToken(ctx, res.User, res.TokenScope)
if err != nil {
err = errors.Wrap(err, "authsvc: error in MintToken")
res := &gateway.AuthenticateResponse{
@@ -104,7 +102,7 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest
return res, nil
}

if s.c.DisableHomeCreationOnLogin {
if scope, ok := res.TokenScope["user"]; s.c.DisableHomeCreationOnLogin || !ok || scope.Role != authpb.Role_ROLE_OWNER {
gwRes := &gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
User: res.User,
@@ -116,7 +114,7 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest
// we need to pass the token to authenticate the CreateHome request.
// TODO(labkode): appending to existing context will not pass the token.
ctx = tokenpkg.ContextSetToken(ctx, token)
ctx = userpkg.ContextSetUser(ctx, user)
ctx = userpkg.ContextSetUser(ctx, res.User)
ctx = metadata.AppendToOutgoingContext(ctx, tokenpkg.TokenHeader, token) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials?

// create home directory
@@ -145,7 +143,7 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest
}

func (s *svc) WhoAmI(ctx context.Context, req *gateway.WhoAmIRequest) (*gateway.WhoAmIResponse, error) {
u, err := s.tokenmgr.DismantleToken(ctx, req.Token)
u, _, err := s.tokenmgr.DismantleToken(ctx, req.Token)
if err != nil {
err = errors.Wrap(err, "gateway: error getting user from token")
return &gateway.WhoAmIResponse{
16 changes: 10 additions & 6 deletions internal/http/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
@@ -23,13 +23,13 @@ import (
"net/http"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/internal/http/interceptors/auth/credential/registry"
tokenregistry "github.com/cs3org/reva/internal/http/interceptors/auth/token/registry"
tokenwriterregistry "github.com/cs3org/reva/internal/http/interceptors/auth/tokenwriter/registry"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/rgrpc/status"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/rhttp/global"
@@ -234,16 +234,20 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
}

// validate token
claims, err := tokenManager.DismantleToken(r.Context(), tkn)
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), tkn)
if err != nil {
log.Error().Err(err).Msg("error dismantling token")
w.WriteHeader(http.StatusUnauthorized)
return
}

u := &userpb.User{}
if err := mapstructure.Decode(claims, u); err != nil {
log.Error().Err(err).Msg("error decoding user claims")
// ensure access to the resource is allowed
ok, err := scope.VerifyScope(tokenScope, r.URL.Path)
if err != nil {
log.Error().Err(err).Msg("error verifying scope of access token")
w.WriteHeader(http.StatusInternalServerError)
}
if !ok {
log.Error().Err(err).Msg("access to resource not allowed")
w.WriteHeader(http.StatusUnauthorized)
return
}
3 changes: 2 additions & 1 deletion pkg/auth/auth.go
Original file line number Diff line number Diff line change
@@ -22,13 +22,14 @@ import (
"context"
"net/http"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)

// Manager is the interface to implement to authenticate users
type Manager interface {
Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, error)
Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error)
}

// Credentials contains the auth type, client id and secret.
13 changes: 10 additions & 3 deletions pkg/auth/manager/demo/demo.go
Original file line number Diff line number Diff line change
@@ -21,9 +21,11 @@ package demo
import (
"context"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/errtypes"
)

@@ -48,13 +50,18 @@ func New(m map[string]interface{}) (auth.Manager, error) {
return &manager{credentials: creds}, nil
}

func (m *manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, error) {
func (m *manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
scope, err := scope.GetOwnerScope()
if err != nil {
return nil, nil, err
}

if c, ok := m.credentials[clientID]; ok {
if c.Secret == clientSecret {
return c.User, nil
return c.User, scope, nil
}
}
return nil, errtypes.InvalidCredentials(clientID)
return nil, nil, errtypes.InvalidCredentials(clientID)
}

func getCredentials() map[string]Credentials {
4 changes: 2 additions & 2 deletions pkg/auth/manager/demo/demo_test.go
Original file line number Diff line number Diff line change
@@ -30,13 +30,13 @@ func TestUserManager(t *testing.T) {
manager, _ := New(nil)

// Authenticate - positive test
_, err := manager.Authenticate(ctx, "einstein", "relativity")
_, _, err := manager.Authenticate(ctx, "einstein", "relativity")
if err != nil {
t.Fatalf("error while authenticate with correct credentials")
}

// Authenticate - negative test
_, err = manager.Authenticate(ctx, "einstein", "NotARealPassword")
_, _, err = manager.Authenticate(ctx, "einstein", "NotARealPassword")
if err == nil {
t.Fatalf("no error (but we expected one) while authenticate with bad credentials")
}
12 changes: 10 additions & 2 deletions pkg/auth/manager/impersonator/impersonator.go
Original file line number Diff line number Diff line change
@@ -22,9 +22,11 @@ import (
"context"
"strings"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/auth/scope"
)

func init() {
@@ -38,7 +40,7 @@ func New(c map[string]interface{}) (auth.Manager, error) {
return &mgr{}, nil
}

func (m *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, error) {
func (m *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
// allow passing in uid as <opaqueid>@<idp>
at := strings.LastIndex(clientID, "@")
uid := &user.UserId{}
@@ -48,8 +50,14 @@ func (m *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (
uid.OpaqueId = clientID[:at]
uid.Idp = clientID[at+1:]
}

scope, err := scope.GetOwnerScope()
if err != nil {
return nil, nil, err
}

return &user.User{
Id: uid,
// not much else to provide
}, nil
}, scope, nil
}
4 changes: 2 additions & 2 deletions pkg/auth/manager/impersonator/impersonator_test.go
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import (
func TestImpersonator(t *testing.T) {
ctx := context.Background()
i, _ := New(nil)
u, err := i.Authenticate(ctx, "admin", "pwd")
u, _, err := i.Authenticate(ctx, "admin", "pwd")
if err != nil {
t.Fatal(err)
}
@@ -39,7 +39,7 @@ func TestImpersonator(t *testing.T) {
}

ctx = context.Background()
u, err = i.Authenticate(ctx, "opaqueid@idp", "pwd")
u, _, err = i.Authenticate(ctx, "opaqueid@idp", "pwd")
if err != nil {
t.Fatal(err)
}
13 changes: 10 additions & 3 deletions pkg/auth/manager/json/json.go
Original file line number Diff line number Diff line change
@@ -23,10 +23,12 @@ import (
"encoding/json"
"io/ioutil"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
@@ -101,7 +103,12 @@ func New(m map[string]interface{}) (auth.Manager, error) {
return manager, nil
}

func (m *manager) Authenticate(ctx context.Context, username string, secret string) (*user.User, error) {
func (m *manager) Authenticate(ctx context.Context, username string, secret string) (*user.User, map[string]*authpb.Scope, error) {
scope, err := scope.GetOwnerScope()
if err != nil {
return nil, nil, err
}

if c, ok := m.credentials[username]; ok {
if c.Secret == secret {
return &user.User{
@@ -113,8 +120,8 @@ func (m *manager) Authenticate(ctx context.Context, username string, secret stri
Groups: c.Groups,
Opaque: c.Opaque,
// TODO add arbitrary keys as opaque data
}, nil
}, scope, nil
}
}
return nil, errtypes.InvalidCredentials(username)
return nil, nil, errtypes.InvalidCredentials(username)
}
2 changes: 1 addition & 1 deletion pkg/auth/manager/json/json_test.go
Original file line number Diff line number Diff line change
@@ -190,7 +190,7 @@ func TestGetAuthenticatedManager(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authenticated, err := manager.Authenticate(ctx, tt.username, tt.secret)
authenticated, _, err := manager.Authenticate(ctx, tt.username, tt.secret)
if !tt.expectAuthenticated {
assert.Empty(t, authenticated)
assert.EqualError(t, err, tt.expectedError.message)
28 changes: 18 additions & 10 deletions pkg/auth/manager/ldap/ldap.go
Original file line number Diff line number Diff line change
@@ -24,12 +24,14 @@ import (
"fmt"
"strings"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/logger"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
@@ -122,20 +124,20 @@ func New(m map[string]interface{}) (auth.Manager, error) {
}, nil
}

func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, error) {
func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
log := appctx.GetLogger(ctx)

l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", am.c.Hostname, am.c.Port), &tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
return nil, nil, err
}
defer l.Close()

// First bind with a read only user
err = l.Bind(am.c.BindUsername, am.c.BindPassword)
if err != nil {
log.Error().Err(err).Msg("bind with system user failed")
return nil, err
return nil, nil, err
}

// Search for the given clientID
@@ -149,11 +151,11 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)

sr, err := l.Search(searchRequest)
if err != nil {
return nil, err
return nil, nil, err
}

if len(sr.Entries) != 1 {
return nil, errtypes.NotFound(clientID)
return nil, nil, errtypes.NotFound(clientID)
}

userdn := sr.Entries[0].DN
@@ -162,7 +164,7 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)
err = l.Bind(userdn, clientSecret)
if err != nil {
log.Debug().Err(err).Interface("userdn", userdn).Msg("bind with user credentials failed")
return nil, err
return nil, nil, err
}

userID := &user.UserId{
@@ -171,16 +173,16 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)
}
gwc, err := pool.GetGatewayServiceClient(am.c.GatewaySvc)
if err != nil {
return nil, errors.Wrap(err, "ldap: error getting gateway grpc client")
return nil, nil, errors.Wrap(err, "ldap: error getting gateway grpc client")
}
getGroupsResp, err := gwc.GetUserGroups(ctx, &user.GetUserGroupsRequest{
UserId: userID,
})
if err != nil {
return nil, errors.Wrap(err, "ldap: error getting user groups")
return nil, nil, errors.Wrap(err, "ldap: error getting user groups")
}
if getGroupsResp.Status.Code != rpc.Code_CODE_OK {
return nil, errors.Wrap(err, "ldap: grpc getting user groups failed")
return nil, nil, errors.Wrap(err, "ldap: grpc getting user groups failed")
}

u := &user.User{
@@ -204,9 +206,15 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)
},
},
}

scope, err := scope.GetOwnerScope()
if err != nil {
return nil, nil, err
}

log.Debug().Interface("entry", sr.Entries[0]).Interface("user", u).Msg("authenticated user")

return u, nil
return u, scope, nil

}

31 changes: 19 additions & 12 deletions pkg/auth/manager/oidc/oidc.go
Original file line number Diff line number Diff line change
@@ -26,11 +26,13 @@ import (
"time"

oidc "github.com/coreos/go-oidc"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/rhttp"
"github.com/cs3org/reva/pkg/sharedconf"
@@ -90,27 +92,27 @@ func New(m map[string]interface{}) (auth.Manager, error) {
// the clientID it would be empty as we only need to validate the clientSecret variable
// which contains the access token that we can use to contact the UserInfo endpoint
// and get the user claims.
func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, error) {
func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
ctx = am.getOAuthCtx(ctx)

provider, err := am.getOIDCProvider(ctx)
oidcProvider, err := am.getOIDCProvider(ctx)
if err != nil {
return nil, fmt.Errorf("error creating oidc provider: +%v", err)
return nil, nil, fmt.Errorf("error creating oidc provider: +%v", err)
}

oauth2Token := &oauth2.Token{
AccessToken: clientSecret,
}
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
userInfo, err := oidcProvider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
return nil, fmt.Errorf("oidc: error getting userinfo: +%v", err)
return nil, nil, fmt.Errorf("oidc: error getting userinfo: +%v", err)
}

// claims contains the standard OIDC claims like issuer, iat, aud, ... and any other non-standard one.
// TODO(labkode): make claims configuration dynamic from the config file so we can add arbitrary mappings from claims to user struct.
var claims map[string]interface{}
if err := userInfo.Claims(&claims); err != nil {
return nil, fmt.Errorf("oidc: error unmarshaling userinfo claims: %v", err)
return nil, nil, fmt.Errorf("oidc: error unmarshaling userinfo claims: %v", err)
}
log.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Msg("unmarshalled userinfo")

@@ -122,11 +124,11 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)
}

if claims["email"] == nil {
return nil, fmt.Errorf("no \"email\" attribute found in userinfo: maybe the client did not request the oidc \"email\"-scope")
return nil, nil, fmt.Errorf("no \"email\" attribute found in userinfo: maybe the client did not request the oidc \"email\"-scope")
}

if claims["preferred_username"] == nil || claims["name"] == nil {
return nil, fmt.Errorf("no \"preferred_username\" or \"name\" attribute found in userinfo: maybe the client did not request the oidc \"profile\"-scope")
return nil, nil, fmt.Errorf("no \"preferred_username\" or \"name\" attribute found in userinfo: maybe the client did not request the oidc \"profile\"-scope")
}

opaqueObj := &types.Opaque{
@@ -157,16 +159,16 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)
}
gwc, err := pool.GetGatewayServiceClient(am.c.GatewaySvc)
if err != nil {
return nil, errors.Wrap(err, "oidc: error getting gateway grpc client")
return nil, nil, errors.Wrap(err, "oidc: error getting gateway grpc client")
}
getGroupsResp, err := gwc.GetUserGroups(ctx, &user.GetUserGroupsRequest{
UserId: userID,
})
if err != nil {
return nil, errors.Wrap(err, "oidc: error getting user groups")
return nil, nil, errors.Wrap(err, "oidc: error getting user groups")
}
if getGroupsResp.Status.Code != rpc.Code_CODE_OK {
return nil, errors.Wrap(err, "oidc: grpc getting user groups failed")
return nil, nil, errors.Wrap(err, "oidc: grpc getting user groups failed")
}

u := &user.User{
@@ -183,7 +185,12 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string)
Opaque: opaqueObj,
}

return u, nil
scope, err := scope.GetOwnerScope()
if err != nil {
return nil, nil, err
}

return u, scope, nil
}

func (am *mgr) getOAuthCtx(ctx context.Context) context.Context {
48 changes: 38 additions & 10 deletions pkg/auth/manager/publicshares/publicshares.go
Original file line number Diff line number Diff line change
@@ -23,15 +23,17 @@ import (
"strings"
"time"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
userprovider "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/utils"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
@@ -69,10 +71,10 @@ func New(m map[string]interface{}) (auth.Manager, error) {
}, nil
}

func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user.User, error) {
func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user.User, map[string]*authpb.Scope, error) {
gwConn, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
if err != nil {
return nil, err
return nil, nil, err
}

var auth *link.PublicShareAuthentication
@@ -93,7 +95,7 @@ func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user
Spec: &link.PublicShareAuthentication_Signature{
Signature: &link.ShareSignature{
Signature: sig,
SignatureExpiration: &typesv1beta1.Timestamp{
SignatureExpiration: &types.Timestamp{
Seconds: uint64(exp.UnixNano() / 1000000000),
Nanos: uint32(exp.UnixNano() % 1000000000),
},
@@ -109,23 +111,49 @@ func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user
})
switch {
case err != nil:
return nil, err
return nil, nil, err
case publicShareResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound(publicShareResponse.Status.Message)
return nil, nil, errtypes.NotFound(publicShareResponse.Status.Message)
case publicShareResponse.Status.Code == rpcv1beta1.Code_CODE_PERMISSION_DENIED:
return nil, errtypes.InvalidCredentials(publicShareResponse.Status.Message)
return nil, nil, errtypes.InvalidCredentials(publicShareResponse.Status.Message)
case publicShareResponse.Status.Code != rpcv1beta1.Code_CODE_OK:
return nil, errtypes.InternalError(publicShareResponse.Status.Message)
return nil, nil, errtypes.InternalError(publicShareResponse.Status.Message)
}

getUserResponse, err := gwConn.GetUser(ctx, &userprovider.GetUserRequest{
UserId: publicShareResponse.GetShare().GetCreator(),
})
if err != nil {
return nil, err
return nil, nil, err
}

scope, err := m.getScope(ctx, publicShareResponse.GetShare())
if err != nil {
return nil, nil, err
}

return getUserResponse.GetUser(), nil
return getUserResponse.GetUser(), scope, nil
}

func (m *manager) getScope(ctx context.Context, share *link.PublicShare) (map[string]*authpb.Scope, error) {
role := authpb.Role_ROLE_VIEWER
if share.Permissions.Permissions.InitiateFileUpload {
role = authpb.Role_ROLE_EDITOR
}

val, err := utils.MarshalProtoV1ToJSON(share)
if err != nil {
return nil, err
}
return map[string]*authpb.Scope{
"publicshare": &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: role,
},
}, nil
}

// ErrPasswordNotProvided is returned when the public share is password protected, but there was no password on the request
100 changes: 100 additions & 0 deletions pkg/auth/scope/publicshare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package scope

import (
"fmt"
"strings"

authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/utils"
)

func publicshareScope(scope *authpb.Scope, resource interface{}) (bool, error) {
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
if err != nil {
return false, err
}

switch v := resource.(type) {
// Viewer role
case *registry.GetStorageProvidersRequest:
return checkStorageRef(&share, v.GetRef()), nil
case *provider.StatRequest:
return checkStorageRef(&share, v.GetRef()), nil
case *provider.ListContainerRequest:
return checkStorageRef(&share, v.GetRef()), nil
case *provider.InitiateFileDownloadRequest:
return checkStorageRef(&share, v.GetRef()), nil

// Editor role
// TODO(ishank011): Add role checks,
// need to return appropriate status codes in the ocs/ocdav layers.
case *provider.CreateContainerRequest:
return checkStorageRef(&share, v.GetRef()), nil
case *provider.DeleteRequest:
return checkStorageRef(&share, v.GetRef()), nil
case *provider.MoveRequest:
return checkStorageRef(&share, v.GetSource()) && checkStorageRef(&share, v.GetDestination()), nil
case *provider.InitiateFileUploadRequest:
return checkStorageRef(&share, v.GetRef()), nil

case *link.GetPublicShareRequest:
return checkPublicShareRef(&share, v.GetRef()), nil
case string:
return checkPath(&share, v), nil
}

return false, errtypes.InternalError(fmt.Sprintf("resource type assertion failed: %+v", resource))
}

func checkStorageRef(s *link.PublicShare, r *provider.Reference) bool {
// ref: <id:<storage_id:$storageID opaque_id:$opaqueID > >
if r.GetId() != nil {
return s.ResourceId.StorageId == r.GetId().StorageId && s.ResourceId.OpaqueId == r.GetId().OpaqueId
}
// ref: <path:"/public/$token" >
if strings.HasPrefix(r.GetPath(), "/public/"+s.Token) {
return true
}
return false
}

func checkPublicShareRef(s *link.PublicShare, ref *link.PublicShareReference) bool {
// ref: <token:$token >
return ref.GetToken() == s.Token
}

func checkPath(s *link.PublicShare, path string) bool {
paths := []string{
"/dataprovider",
"/data",
}
for _, p := range paths {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
72 changes: 72 additions & 0 deletions pkg/auth/scope/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package scope

import (
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/utils"
)

// Verifier is the function signature which every scope verifier should implement.
type Verifier func(*authpb.Scope, interface{}) (bool, error)

var supportedScopes = map[string]Verifier{
"user": userScope,
"publicshare": publicshareScope,
}

// VerifyScope is the function to be called when dismantling tokens to check if
// the token has access to a particular resource.
func VerifyScope(scopeMap map[string]*authpb.Scope, resource interface{}) (bool, error) {
for k, scope := range scopeMap {
verifierFunc := supportedScopes[k]
valid, err := verifierFunc(scope, resource)
if err != nil {
continue
}
if valid {
return true, nil
}
}
return false, nil
}

// GetOwnerScope returns the default owner scope with access to all resources.
func GetOwnerScope() (map[string]*authpb.Scope, error) {
ref := &provider.Reference{
Spec: &provider.Reference_Path{
Path: "/",
},
}
val, err := utils.MarshalProtoV1ToJSON(ref)
if err != nil {
return nil, err
}
return map[string]*authpb.Scope{
"user": &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: authpb.Role_ROLE_OWNER,
},
}, nil
}
27 changes: 27 additions & 0 deletions pkg/auth/scope/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package scope

import authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"

func userScope(scope *authpb.Scope, resource interface{}) (bool, error) {
// Always return true. Registered users can access all paths.
// TODO(ishank011): Add checks for read/write permissions.
return true, nil
}
30 changes: 18 additions & 12 deletions pkg/token/manager/demo/demo.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import (
"encoding/base64"
"encoding/gob"

auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/pkg/token"
"github.com/cs3org/reva/pkg/token/manager/registry"
@@ -42,28 +43,33 @@ func New(m map[string]interface{}) (token.Manager, error) {

type manager struct{}

func (m *manager) MintToken(ctx context.Context, u *user.User) (string, error) {
token, err := encode(u)
type claims struct {
User *user.User `json:"user"`
Scope map[string]*auth.Scope `json:"scope"`
}

func (m *manager) MintToken(ctx context.Context, u *user.User, scope map[string]*auth.Scope) (string, error) {
token, err := encode(&claims{u, scope})
if err != nil {
return "", errors.Wrap(err, "error encoding user")
}
return token, nil
}

func (m *manager) DismantleToken(ctx context.Context, token string) (*user.User, error) {
u, err := decode(token)
func (m *manager) DismantleToken(ctx context.Context, token string) (*user.User, map[string]*auth.Scope, error) {
c, err := decode(token)
if err != nil {
return nil, errors.Wrap(err, "error decoding claims")
return nil, nil, errors.Wrap(err, "error decoding claims")
}
return u, nil
return c.User, c.Scope, nil
}

// from https://stackoverflow.com/questions/28020070/golang-serialize-and-deserialize-back
// go binary encoder
func encode(u *user.User) (string, error) {
func encode(c *claims) (string, error) {
b := bytes.Buffer{}
e := gob.NewEncoder(&b)
err := e.Encode(u)
err := e.Encode(c)
if err != nil {
return "", err
}
@@ -72,18 +78,18 @@ func encode(u *user.User) (string, error) {

// from https://stackoverflow.com/questions/28020070/golang-serialize-and-deserialize-back
// go binary decoder
func decode(token string) (*user.User, error) {
u := &user.User{}
func decode(token string) (*claims, error) {
c := &claims{}
by, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, err
}
b := bytes.Buffer{}
b.Write(by)
d := gob.NewDecoder(&b)
err = d.Decode(&u)
err = d.Decode(&c)
if err != nil {
return nil, err
}
return u, nil
return c, nil
}
31 changes: 29 additions & 2 deletions pkg/token/manager/demo/demo_test.go
Original file line number Diff line number Diff line change
@@ -20,9 +20,13 @@ package demo

import (
"context"
"encoding/json"
"testing"

auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)

var ctx = context.Background()
@@ -33,17 +37,40 @@ func TestEncodeDecode(t *testing.T) {
Username: "marie",
}

encoded, err := m.MintToken(ctx, u)
ref := &provider.Reference{
Spec: &provider.Reference_Path{
Path: "/",
},
}
val, err := json.Marshal(ref)
if err != nil {
t.Fatal(err)
}
scope := map[string]*auth.Scope{
"user": &auth.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: val,
},
Role: auth.Role_ROLE_OWNER,
},
}

encoded, err := m.MintToken(ctx, u, scope)
if err != nil {
t.Fatal(err)
}

decodedUser, err := m.DismantleToken(ctx, encoded)
decodedUser, decodedScope, err := m.DismantleToken(ctx, encoded)
if err != nil {
t.Fatal(err)
}

if u.Username != decodedUser.Username {
t.Fatalf("mail claims differ: expected=%s got=%s", u.Username, decodedUser.Username)
}

if s, ok := decodedScope["user"]; !ok || s.Role != auth.Role_ROLE_OWNER {
t.Fatalf("scope claims differ: expected=%s got=%s", scope, decodedScope)
}
}
42 changes: 19 additions & 23 deletions pkg/token/manager/jwt/jwt.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import (
"context"
"time"

auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/sharedconf"
@@ -43,6 +44,17 @@ type config struct {
Expires int64 `mapstructure:"expires"`
}

type manager struct {
conf *config
}

// claims are custom claims for the JWT token.
type claims struct {
jwt.StandardClaims
User *user.User `json:"user"`
Scope map[string]*auth.Scope `json:"scope"`
}

func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
@@ -73,23 +85,7 @@ func New(value map[string]interface{}) (token.Manager, error) {
return m, nil
}

type manager struct {
conf *config
}

// claims are custom claims for the JWT token.
type claims struct {
jwt.StandardClaims
User *user.User `json:"user"`
}

// TODO(labkode): resulting JSON contains internal protobuf fields:
// "Username": "einstein",
// "XXX_NoUnkeyedLiteral": {},
// "XXX_sizecache": 0,
// "XXX_unrecognized": null
//}
func (m *manager) MintToken(ctx context.Context, u *user.User) (string, error) {
func (m *manager) MintToken(ctx context.Context, u *user.User, scope map[string]*auth.Scope) (string, error) {
ttl := time.Duration(m.conf.Expires) * time.Second
claims := claims{
StandardClaims: jwt.StandardClaims{
@@ -98,7 +94,8 @@ func (m *manager) MintToken(ctx context.Context, u *user.User) (string, error) {
Audience: "reva",
IssuedAt: time.Now().Unix(),
},
User: u,
User: u,
Scope: scope,
}

t := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
@@ -111,19 +108,18 @@ func (m *manager) MintToken(ctx context.Context, u *user.User) (string, error) {
return tkn, nil
}

func (m *manager) DismantleToken(ctx context.Context, tkn string) (*user.User, error) {
func (m *manager) DismantleToken(ctx context.Context, tkn string) (*user.User, map[string]*auth.Scope, error) {
token, err := jwt.ParseWithClaims(tkn, &claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(m.conf.Secret), nil
})

if err != nil {
return nil, errors.Wrap(err, "error parsing token")
return nil, nil, errors.Wrap(err, "error parsing token")
}

if claims, ok := token.Claims.(*claims); ok && token.Valid {
return claims.User, nil
return claims.User, claims.Scope, nil
}

err = errtypes.InvalidCredentials("token invalid")
return nil, err
return nil, nil, errtypes.InvalidCredentials("invalid token")
}
8 changes: 3 additions & 5 deletions pkg/token/token.go
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ package token
import (
"context"

auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)

@@ -32,13 +33,10 @@ type key int

const tokenKey key = iota

// Claims is the map of attributes to encode into a token
type Claims map[string]interface{}

// Manager is the interface to implement to sign and verify tokens
type Manager interface {
MintToken(ctx context.Context, u *user.User) (string, error)
DismantleToken(ctx context.Context, token string) (*user.User, error)
MintToken(ctx context.Context, u *user.User, scope map[string]*auth.Scope) (string, error)
DismantleToken(ctx context.Context, token string) (*user.User, map[string]*auth.Scope, error)
}

// ContextGetToken returns the token if set in the given context.
1 change: 1 addition & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
@@ -110,6 +110,7 @@ func ResolvePath(path string) (string, error) {

// RandString is a helper to create tokens.
func RandString(n int) string {
rand.Seed(time.Now().UTC().UnixNano())
var l = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
5 changes: 4 additions & 1 deletion tests/integration/grpc/storageprovider_test.go
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ import (
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storagep "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/storage/fs/ocis"
"github.com/cs3org/reva/pkg/storage/fs/owncloud"
@@ -90,7 +91,9 @@ var _ = Describe("storage providers", func() {
// Add auth token
tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"})
Expect(err).ToNot(HaveOccurred())
t, err := tokenManager.MintToken(ctx, user)
scope, err := scope.GetOwnerScope()
Expect(err).ToNot(HaveOccurred())
t, err := tokenManager.MintToken(ctx, user, scope)
Expect(err).ToNot(HaveOccurred())
ctx = token.ContextSetToken(ctx, t)
ctx = metadata.AppendToOutgoingContext(ctx, token.TokenHeader, t)
8 changes: 5 additions & 3 deletions tests/integration/grpc/userprovider_test.go
Original file line number Diff line number Diff line change
@@ -23,12 +23,12 @@ import (

userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"google.golang.org/grpc/metadata"

"github.com/cs3org/reva/pkg/auth/scope"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/token"
jwt "github.com/cs3org/reva/pkg/token/manager/jwt"
ruser "github.com/cs3org/reva/pkg/user"
"google.golang.org/grpc/metadata"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -58,7 +58,9 @@ var _ = Describe("user providers", func() {
}
tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"})
Expect(err).ToNot(HaveOccurred())
t, err := tokenManager.MintToken(ctx, user)
scope, err := scope.GetOwnerScope()
Expect(err).ToNot(HaveOccurred())
t, err := tokenManager.MintToken(ctx, user, scope)
Expect(err).ToNot(HaveOccurred())
ctx = token.ContextSetToken(ctx, t)
ctx = metadata.AppendToOutgoingContext(ctx, token.TokenHeader, t)