From 65e451a6361241ce710644245e9793e55b4f9557 Mon Sep 17 00:00:00 2001 From: Ishank Arora Date: Wed, 28 Apr 2021 09:07:22 +0200 Subject: [PATCH] Mint scope-based access tokens for RBAC --- changelog/unreleased/scope-based-tokens.md | 3 + examples/storage-references/gateway.toml | 6 ++ .../storage-references/storage-public.toml | 15 ++++ go.mod | 1 + internal/grpc/interceptors/auth/auth.go | 11 ++- .../grpc/services/appprovider/appprovider.go | 5 ++ .../services/authprovider/authprovider.go | 7 +- .../grpc/services/gateway/authprovider.go | 8 +- internal/http/interceptors/auth/auth.go | 3 +- pkg/auth/auth.go | 3 +- pkg/auth/manager/demo/demo.go | 29 ++++++- pkg/auth/manager/demo/demo_test.go | 4 +- pkg/auth/manager/impersonator/impersonator.go | 28 ++++++- .../manager/impersonator/impersonator_test.go | 4 +- pkg/auth/manager/json/json.go | 27 ++++++- pkg/auth/manager/json/json_test.go | 2 +- pkg/auth/manager/ldap/ldap.go | 43 +++++++--- pkg/auth/manager/oidc/oidc.go | 46 ++++++++--- pkg/auth/manager/publicshares/publicshares.go | 48 ++++++++--- pkg/auth/scope/publicshare.go | 79 +++++++++++++++++++ pkg/auth/scope/scope.go | 43 ++++++++++ pkg/auth/scope/user.go | 27 +++++++ pkg/publicshare/manager/json/json.go | 3 + pkg/token/manager/demo/demo.go | 5 +- pkg/token/manager/demo/demo_test.go | 26 +++++- pkg/token/manager/jwt/jwt.go | 43 +++++----- pkg/token/token.go | 8 +- 27 files changed, 440 insertions(+), 87 deletions(-) create mode 100644 changelog/unreleased/scope-based-tokens.md create mode 100644 examples/storage-references/storage-public.toml create mode 100644 pkg/auth/scope/publicshare.go create mode 100644 pkg/auth/scope/scope.go create mode 100644 pkg/auth/scope/user.go diff --git a/changelog/unreleased/scope-based-tokens.md b/changelog/unreleased/scope-based-tokens.md new file mode 100644 index 00000000000..79f5ceaca29 --- /dev/null +++ b/changelog/unreleased/scope-based-tokens.md @@ -0,0 +1,3 @@ +Enhancement: Mint scope-based access tokens for RBAC + +https://github.com/cs3org/reva/pull/1669 diff --git a/examples/storage-references/gateway.toml b/examples/storage-references/gateway.toml index bb486363fec..402935c9ee6 100644 --- a/examples/storage-references/gateway.toml +++ b/examples/storage-references/gateway.toml @@ -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] diff --git a/examples/storage-references/storage-public.toml b/examples/storage-references/storage-public.toml new file mode 100644 index 00000000000..8d409908c54 --- /dev/null +++ b/examples/storage-references/storage-public.toml @@ -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" diff --git a/go.mod b/go.mod index 70ed8d2b466..ee1e55d4812 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( go 1.16 replace ( + github.com/cs3org/go-cs3apis => ../cs3apis/build/go-cs3apis github.com/eventials/go-tus => github.com/andrewmostello/go-tus v0.0.0-20200314041820-904a9904af9a github.com/oleiade/reflections => github.com/oleiade/reflections v1.0.1 google.golang.org/grpc => google.golang.org/grpc v1.26.0 // temporary downgrade diff --git a/internal/grpc/interceptors/auth/auth.go b/internal/grpc/interceptors/auth/auth.go index 9b3599871a1..8252954779e 100644 --- a/internal/grpc/interceptors/auth/auth.go +++ b/internal/grpc/interceptors/auth/auth.go @@ -88,7 +88,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 := tokenManager.DismantleToken(ctx, tkn, req) if err == nil { ctx = user.ContextSetUser(ctx, u) } @@ -96,6 +96,8 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI return handler(ctx, req) } + log.Info().Msgf("GRPC unary interceptor %s, %+v", info.FullMethod, req) + span.AddAttributes(trace.BoolAttribute("auth_enabled", true)) tkn, ok := token.ContextGetToken(ctx) @@ -106,7 +108,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI } // validate the token - u, err := tokenManager.DismantleToken(ctx, tkn) + u, err := tokenManager.DismantleToken(ctx, tkn, req) if err != nil { log.Warn().Msg("access token is invalid") return nil, status.Errorf(codes.Unauthenticated, "auth: core access token is invalid") @@ -151,6 +153,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := ss.Context() log := appctx.GetLogger(ctx) + log.Info().Msgf("GRPC stream interceptor %s, %+v", info.FullMethod, unprotected) if utils.Skip(info.FullMethod, unprotected) { log.Debug().Str("method", info.FullMethod).Msg("skipping auth") @@ -159,7 +162,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 := tokenManager.DismantleToken(ctx, tkn, ss) if err == nil { ctx = user.ContextSetUser(ctx, u) ss = newWrappedServerStream(ctx, ss) @@ -177,7 +180,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe } // validate the token - claims, err := tokenManager.DismantleToken(ctx, tkn) + claims, err := tokenManager.DismantleToken(ctx, tkn, ss) if err != nil { log.Warn().Msg("access token invalid") return status.Errorf(codes.Unauthenticated, "auth: core access token is invalid") diff --git a/internal/grpc/services/appprovider/appprovider.go b/internal/grpc/services/appprovider/appprovider.go index 90dd2c4a91d..2186c935024 100644 --- a/internal/grpc/services/appprovider/appprovider.go +++ b/internal/grpc/services/appprovider/appprovider.go @@ -35,6 +35,7 @@ import ( "github.com/cs3org/reva/pkg/app" "github.com/cs3org/reva/pkg/app/provider/demo" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rhttp" @@ -274,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") +} diff --git a/internal/grpc/services/authprovider/authprovider.go b/internal/grpc/services/authprovider/authprovider.go index abf88f43bef..bb4d179b8e9 100644 --- a/internal/grpc/services/authprovider/authprovider.go +++ b/internal/grpc/services/authprovider/authprovider.go @@ -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{ diff --git a/internal/grpc/services/gateway/authprovider.go b/internal/grpc/services/gateway/authprovider.go index 487094fa6ad..9dbf72ad2a7 100644 --- a/internal/grpc/services/gateway/authprovider.go +++ b/internal/grpc/services/gateway/authprovider.go @@ -93,9 +93,7 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest }, 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{ @@ -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, nil) if err != nil { err = errors.Wrap(err, "gateway: error getting user from token") return &gateway.WhoAmIResponse{ diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 37bd8ae0d9b..c98496ecf0b 100644 --- a/internal/http/interceptors/auth/auth.go +++ b/internal/http/interceptors/auth/auth.go @@ -154,6 +154,7 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err } log := appctx.GetLogger(ctx) + log.Info().Msgf("HTTP interceptor %s, %+v", r.URL.Path, unprotected) // skip auth for urls set in the config. // TODO(labkode): maybe use method:url to bypass auth. @@ -234,7 +235,7 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err } // validate token - claims, err := tokenManager.DismantleToken(r.Context(), tkn) + claims, err := tokenManager.DismantleToken(r.Context(), tkn, r.URL.Path) if err != nil { log.Error().Err(err).Msg("error dismantling token") w.WriteHeader(http.StatusUnauthorized) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 01ecbd4b93e..87140996833 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -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. diff --git a/pkg/auth/manager/demo/demo.go b/pkg/auth/manager/demo/demo.go index 987ac2437ec..e1a60671446 100644 --- a/pkg/auth/manager/demo/demo.go +++ b/pkg/auth/manager/demo/demo.go @@ -20,8 +20,12 @@ package demo import ( "context" + "encoding/json" + authpb "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" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" "github.com/cs3org/reva/pkg/errtypes" @@ -48,13 +52,32 @@ 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) { + ref := &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: "/", + }, + } + val, err := json.Marshal(ref) + if err != nil { + return nil, nil, err + } + scope := map[string]*authpb.Scope{ + "user": &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: authpb.Role_ROLE_OWNER, + }, + } + 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 { diff --git a/pkg/auth/manager/demo/demo_test.go b/pkg/auth/manager/demo/demo_test.go index 5b5c5e0f317..2b73d153c92 100644 --- a/pkg/auth/manager/demo/demo_test.go +++ b/pkg/auth/manager/demo/demo_test.go @@ -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") } diff --git a/pkg/auth/manager/impersonator/impersonator.go b/pkg/auth/manager/impersonator/impersonator.go index 7478851c5b1..ca08ddecd85 100644 --- a/pkg/auth/manager/impersonator/impersonator.go +++ b/pkg/auth/manager/impersonator/impersonator.go @@ -20,9 +20,13 @@ package impersonator import ( "context" + "encoding/json" "strings" + authpb "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" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" ) @@ -38,7 +42,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 @ at := strings.LastIndex(clientID, "@") uid := &user.UserId{} @@ -48,8 +52,28 @@ func (m *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) ( uid.OpaqueId = clientID[:at] uid.Idp = clientID[at+1:] } + + ref := &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: "/", + }, + } + val, err := json.Marshal(ref) + if err != nil { + return nil, nil, err + } + scope := map[string]*authpb.Scope{ + "user": &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: authpb.Role_ROLE_OWNER, + }, + } + return &user.User{ Id: uid, // not much else to provide - }, nil + }, scope, nil } diff --git a/pkg/auth/manager/impersonator/impersonator_test.go b/pkg/auth/manager/impersonator/impersonator_test.go index 41713cd4e42..31fabefab12 100644 --- a/pkg/auth/manager/impersonator/impersonator_test.go +++ b/pkg/auth/manager/impersonator/impersonator_test.go @@ -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) } diff --git a/pkg/auth/manager/json/json.go b/pkg/auth/manager/json/json.go index b88f39432ea..bf3a9c1c000 100644 --- a/pkg/auth/manager/json/json.go +++ b/pkg/auth/manager/json/json.go @@ -23,7 +23,9 @@ 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" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" @@ -101,7 +103,26 @@ 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) { + ref := &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: "/", + }, + } + val, err := json.Marshal(ref) + if err != nil { + return nil, nil, err + } + scope := map[string]*authpb.Scope{ + "user": &authpb.Scope{ + Resource: &typespb.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: authpb.Role_ROLE_OWNER, + }, + } + if c, ok := m.credentials[username]; ok { if c.Secret == secret { return &user.User{ @@ -113,8 +134,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) } diff --git a/pkg/auth/manager/json/json_test.go b/pkg/auth/manager/json/json_test.go index 4e9ee1e50f1..fa867dfaec2 100644 --- a/pkg/auth/manager/json/json_test.go +++ b/pkg/auth/manager/json/json_test.go @@ -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) diff --git a/pkg/auth/manager/ldap/ldap.go b/pkg/auth/manager/ldap/ldap.go index 4a490937c31..c796898dbed 100644 --- a/pkg/auth/manager/ldap/ldap.go +++ b/pkg/auth/manager/ldap/ldap.go @@ -21,11 +21,14 @@ package ldap import ( "context" "crypto/tls" + "encoding/json" "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" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/auth" @@ -122,12 +125,12 @@ 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() @@ -135,7 +138,7 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) 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 +152,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 +165,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 +174,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 +207,29 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) }, }, } + + ref := &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: "/", + }, + } + val, err := json.Marshal(ref) + if err != nil { + return nil, nil, err + } + scope := map[string]*authpb.Scope{ + "user": &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: authpb.Role_ROLE_OWNER, + }, + } + log.Debug().Interface("entry", sr.Entries[0]).Interface("user", u).Msg("authenticated user") - return u, nil + return u, scope, nil } diff --git a/pkg/auth/manager/oidc/oidc.go b/pkg/auth/manager/oidc/oidc.go index 9690c548e05..db9d5742a45 100644 --- a/pkg/auth/manager/oidc/oidc.go +++ b/pkg/auth/manager/oidc/oidc.go @@ -22,12 +22,15 @@ package oidc import ( "context" + "encoding/json" "fmt" "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" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" @@ -90,27 +93,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 +125,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 +160,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 +186,26 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) Opaque: opaqueObj, } - return u, nil + ref := &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: "/", + }, + } + val, err := json.Marshal(ref) + if err != nil { + return nil, nil, err + } + scope := map[string]*authpb.Scope{ + "user": &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: authpb.Role_ROLE_OWNER, + }, + } + + return u, scope, nil } func (am *mgr) getOAuthCtx(ctx context.Context) context.Context { diff --git a/pkg/auth/manager/publicshares/publicshares.go b/pkg/auth/manager/publicshares/publicshares.go index fdffdaf5a15..8521e47e001 100644 --- a/pkg/auth/manager/publicshares/publicshares.go +++ b/pkg/auth/manager/publicshares/publicshares.go @@ -20,14 +20,16 @@ package publicshares import ( "context" + "encoding/json" "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" @@ -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 := json.Marshal(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 diff --git a/pkg/auth/scope/publicshare.go b/pkg/auth/scope/publicshare.go new file mode 100644 index 00000000000..921ea737555 --- /dev/null +++ b/pkg/auth/scope/publicshare.go @@ -0,0 +1,79 @@ +// 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 ( + "encoding/json" + "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" + "github.com/cs3org/reva/pkg/errtypes" +) + +func publicshareScope(scope *authpb.Scope, resource interface{}) (bool, error) { + var share *link.PublicShare + err := json.Unmarshal(scope.Resource.Value, &share) + if err != nil { + return false, err + } + + switch v := resource.(type) { + case *provider.Reference: + return checkStorageRef(share, v), nil + case *link.PublicShareReference: + return checkPublicShareRef(share, v), 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: > + if r.GetId() != nil { + return s.ResourceId.StorageId == r.GetId().StorageId && s.ResourceId.OpaqueId == r.GetId().OpaqueId + } + // ref: + if strings.HasPrefix(r.GetPath(), "/public/"+s.Token) { + return true + } + return false +} + +func checkPublicShareRef(s *link.PublicShare, ref *link.PublicShareReference) bool { + // ref: + 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 +} diff --git a/pkg/auth/scope/scope.go b/pkg/auth/scope/scope.go new file mode 100644 index 00000000000..c8372fc1011 --- /dev/null +++ b/pkg/auth/scope/scope.go @@ -0,0 +1,43 @@ +// 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" +) + +type Verifier func(*authpb.Scope, interface{}) (bool, error) + +var supportedScopes = map[string]Verifier{ + "user": userScope, + "publicshare": publicshareScope, +} + +func VerifyScope(scopeMap map[string]*authpb.Scope, resource interface{}) (bool, error) { + valid := true + var err error + for k, scope := range scopeMap { + verifierFunc := supportedScopes[k] + valid, err = verifierFunc(scope, resource) + if err != nil { + return false, err + } + } + return valid, nil +} diff --git a/pkg/auth/scope/user.go b/pkg/auth/scope/user.go new file mode 100644 index 00000000000..f885a43ba8b --- /dev/null +++ b/pkg/auth/scope/user.go @@ -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 +} diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index eb2e17aa4b1..e09242754c1 100644 --- a/pkg/publicshare/manager/json/json.go +++ b/pkg/publicshare/manager/json/json.go @@ -508,6 +508,8 @@ func (m *manager) getByToken(ctx context.Context, token string) (*link.PublicSha // GetPublicShareByToken gets a public share by its opaque token. func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) { + log := appctx.GetLogger(ctx) + log.Info().Msgf("GetPublicShareByToken %s %+v", token, auth) db, err := m.readDb() if err != nil { return nil, err @@ -522,6 +524,7 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local); err != nil { return nil, err } + log.Info().Msgf("GetPublicShareByToken listing keys %s", local.Token) if local.Token == token { if !notExpired(&local) { diff --git a/pkg/token/manager/demo/demo.go b/pkg/token/manager/demo/demo.go index 08ae96312c9..52c1bc5ce71 100644 --- a/pkg/token/manager/demo/demo.go +++ b/pkg/token/manager/demo/demo.go @@ -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,7 +43,7 @@ func New(m map[string]interface{}) (token.Manager, error) { type manager struct{} -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) { token, err := encode(u) if err != nil { return "", errors.Wrap(err, "error encoding user") @@ -50,7 +51,7 @@ func (m *manager) MintToken(ctx context.Context, u *user.User) (string, error) { return token, nil } -func (m *manager) DismantleToken(ctx context.Context, token string) (*user.User, error) { +func (m *manager) DismantleToken(ctx context.Context, token string, resource interface{}) (*user.User, error) { u, err := decode(token) if err != nil { return nil, errors.Wrap(err, "error decoding claims") diff --git a/pkg/token/manager/demo/demo_test.go b/pkg/token/manager/demo/demo_test.go index 684be5887bb..5684b383500 100644 --- a/pkg/token/manager/demo/demo_test.go +++ b/pkg/token/manager/demo/demo_test.go @@ -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,12 +37,30 @@ 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) + } + + encoded, err := m.MintToken(ctx, u, map[string]*auth.Scope{ + "user": &auth.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: auth.Role_ROLE_OWNER, + }, + }) if err != nil { t.Fatal(err) } - decodedUser, err := m.DismantleToken(ctx, encoded) + decodedUser, err := m.DismantleToken(ctx, encoded, nil) if err != nil { t.Fatal(err) } diff --git a/pkg/token/manager/jwt/jwt.go b/pkg/token/manager/jwt/jwt.go index 7ff7a72abf2..2079e41df30 100644 --- a/pkg/token/manager/jwt/jwt.go +++ b/pkg/token/manager/jwt/jwt.go @@ -22,7 +22,9 @@ 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/auth/scope" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/sharedconf" "github.com/cs3org/reva/pkg/token" @@ -43,6 +45,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 +86,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 +95,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,7 +109,7 @@ 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, resource interface{}) (*user.User, error) { token, err := jwt.ParseWithClaims(tkn, &claims{}, func(token *jwt.Token) (interface{}, error) { return []byte(m.conf.Secret), nil }) @@ -121,6 +119,13 @@ func (m *manager) DismantleToken(ctx context.Context, tkn string) (*user.User, e } if claims, ok := token.Claims.(*claims); ok && token.Valid { + ok, err = scope.VerifyScope(claims.Scope, resource) + if err != nil { + return nil, errtypes.InternalError("error verifying scope of access token") + } + if !ok { + return nil, errtypes.PermissionDenied("token missing necessary scope access") + } return claims.User, nil } diff --git a/pkg/token/token.go b/pkg/token/token.go index 0d1c14f4550..3164b27b3dc 100644 --- a/pkg/token/token.go +++ b/pkg/token/token.go @@ -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, resource interface{}) (*user.User, error) } // ContextGetToken returns the token if set in the given context.