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
Expand Up @@ -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]
Expand Down
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
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
116 changes: 102 additions & 14 deletions internal/grpc/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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")
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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{
Expand Down
14 changes: 6 additions & 8 deletions internal/grpc/services/gateway/authprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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{
Expand Down
16 changes: 10 additions & 6 deletions internal/http/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading