diff --git a/changelog/unreleased/oidc-lw-users.md b/changelog/unreleased/oidc-lw-users.md new file mode 100644 index 0000000000..2053f69551 --- /dev/null +++ b/changelog/unreleased/oidc-lw-users.md @@ -0,0 +1,3 @@ +Enhancement: OIDC driver changes for lightweight users + +https://github.com/cs3org/reva/pull/2278 \ No newline at end of file diff --git a/internal/grpc/interceptors/auth/scope.go b/internal/grpc/interceptors/auth/scope.go index 8bad148478..d2161b6eb5 100644 --- a/internal/grpc/interceptors/auth/scope.go +++ b/internal/grpc/interceptors/auth/scope.go @@ -120,6 +120,9 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s if utils.ResourceIDEqual(share.Share.ResourceId, ref.GetResourceId()) { return nil } + if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok { + return nil + } } } else if strings.HasPrefix(k, "publicshare") { var share link.PublicShare diff --git a/internal/grpc/services/gateway/authprovider.go b/internal/grpc/services/gateway/authprovider.go index 81156facd8..e8092f0b5f 100644 --- a/internal/grpc/services/gateway/authprovider.go +++ b/internal/grpc/services/gateway/authprovider.go @@ -21,7 +21,6 @@ package gateway import ( "context" "fmt" - "strings" authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" @@ -29,17 +28,13 @@ import ( 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" - collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/auth/scope" ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/sharedconf" - "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" "google.golang.org/grpc/metadata" ) @@ -121,13 +116,19 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest ctx = ctxpkg.ContextSetToken(ctx, token) ctx = ctxpkg.ContextSetUser(ctx, res.User) ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) - scope, err := s.expandScopes(ctx, res.TokenScope) + + // Commenting out as the token size can get too big + // For now, we'll try to resolve all resources on every request + // TODO(ishank011): Add a cache for these + /* scope, err := s.expandScopes(ctx, res.TokenScope) if err != nil { err = errors.Wrap(err, "authsvc: error expanding token scope") return &gateway.AuthenticateResponse{ Status: status.NewUnauthenticated(ctx, err, "error expanding access token scope"), }, nil } + */ + scope := res.TokenScope token, err = s.tokenmgr.MintToken(ctx, &u, scope) if err != nil { @@ -241,6 +242,7 @@ func (s *svc) findAuthProvider(ctx context.Context, authType string) (provider.P return nil, errtypes.InternalError("gateway: error finding an auth provider for type: " + authType) } +/* func (s *svc) expandScopes(ctx context.Context, scopeMap map[string]*authpb.Scope) (map[string]*authpb.Scope, error) { log := appctx.GetLogger(ctx) newMap := make(map[string]*authpb.Scope) @@ -311,3 +313,4 @@ func (s *svc) statAndAddResource(ctx context.Context, r *storageprovider.Resourc return scope.AddResourceInfoScope(statResponse.Info, role, scopeMap) } +*/ diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index 7511e0a581..8a5e1c284d 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -23,11 +23,13 @@ import ( "fmt" "path" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" @@ -321,7 +323,7 @@ func (s *svc) UpdateReceivedShare(ctx context.Context, req *collaboration.Update } // if we don't need to create/delete references then we return early. - if !s.c.CommitShareToStorageRef { + if !s.c.CommitShareToStorageRef || ctxpkg.ContextMustGetUser(ctx).Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT { return res, nil } diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go index 5f2a38add1..0a9b2fd187 100644 --- a/pkg/app/provider/wopi/wopi.go +++ b/pkg/app/provider/wopi/wopi.go @@ -36,6 +36,7 @@ import ( "github.com/beevik/etree" appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/app" "github.com/cs3org/reva/pkg/app/provider/registry" @@ -139,6 +140,11 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc u, ok := ctxpkg.ContextGetUser(ctx) if ok { // else defaults to "Guest xyz" + if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT { + q.Add("userid", resource.Owner.OpaqueId+"@"+resource.Owner.Idp) + } else { + q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp) + } var isPublicShare bool if u.Opaque != nil { if _, ok := u.Opaque.Map["public-share-role"]; ok { @@ -148,7 +154,6 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc if !isPublicShare { q.Add("username", u.Username) - q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp) } } diff --git a/pkg/auth/manager/oidc/oidc.go b/pkg/auth/manager/oidc/oidc.go index 2ed67a2698..e882b399ba 100644 --- a/pkg/auth/manager/oidc/oidc.go +++ b/pkg/auth/manager/oidc/oidc.go @@ -23,6 +23,7 @@ package oidc import ( "context" "fmt" + "strings" "time" oidc "github.com/coreos/go-oidc" @@ -130,6 +131,12 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) if claims["email_verified"] == nil { // This is not set in simplesamlphp claims["email_verified"] = false } + if claims["preferred_username"] == nil { + claims["preferred_username"] = claims[am.c.IDClaim] + } + if claims["name"] == nil { + claims["name"] = claims[am.c.IDClaim] + } if claims["email"] == nil { return nil, nil, fmt.Errorf("no \"email\" attribute found in userinfo: maybe the client did not request the oidc \"email\"-scope") @@ -158,7 +165,7 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) userID := &user.UserId{ OpaqueId: claims[am.c.IDClaim].(string), // a stable non reassignable id Idp: claims["issuer"].(string), // in the scope of this issuer - Type: user.UserType_USER_TYPE_PRIMARY, + Type: getUserType(claims[am.c.IDClaim].(string)), } gwc, err := pool.GetGatewayServiceClient(am.c.GatewaySvc) if err != nil { @@ -236,3 +243,17 @@ func (am *mgr) getOIDCProvider(ctx context.Context) (*oidc.Provider, error) { am.provider = provider return am.provider, nil } + +func getUserType(upn string) user.UserType { + var t user.UserType + switch { + case strings.HasPrefix(upn, "guest"): + t = user.UserType_USER_TYPE_LIGHTWEIGHT + case strings.Contains(upn, "@"): + t = user.UserType_USER_TYPE_FEDERATED + default: + t = user.UserType_USER_TYPE_PRIMARY + } + return t + +} diff --git a/pkg/auth/scope/lightweight.go b/pkg/auth/scope/lightweight.go index 8256f3f09b..5ebc8d2225 100644 --- a/pkg/auth/scope/lightweight.go +++ b/pkg/auth/scope/lightweight.go @@ -20,6 +20,7 @@ package scope import ( "context" + "strings" authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -34,12 +35,41 @@ func lightweightAccountScope(_ context.Context, scope *authpb.Scope, resource in // These cannot be resolved from here, but need to be added to the scope from // where the call to mint tokens is made. // From here, we only allow ListReceivedShares calls - if _, ok := resource.(*collaboration.ListReceivedSharesRequest); ok { + switch v := resource.(type) { + case *collaboration.ListReceivedSharesRequest: return true, nil + case string: + return checkLightweightPath(v), nil } return false, nil } +func checkLightweightPath(path string) bool { + paths := []string{ + "/ocs/v2.php/apps/files_sharing/api/v1/shares", + "/ocs/v1.php/apps/files_sharing/api/v1/shares", + "/ocs/v2.php/apps/files_sharing//api/v1/shares", + "/ocs/v1.php/apps/files_sharing//api/v1/shares", + "/ocs/v2.php/cloud/capabilities", + "/ocs/v1.php/cloud/capabilities", + "/ocs/v2.php/cloud/user", + "/ocs/v1.php/cloud/user", + "/remote.php/webdav", + "/remote.php/dav/files", + "/app/open", + "/app/new", + "/archiver", + "/dataprovider", + "/data", + } + for _, p := range paths { + if strings.HasPrefix(path, p) { + return true + } + } + return false +} + // AddLightweightAccountScope adds the scope to allow access to lightweight user. func AddLightweightAccountScope(role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) { ref := &provider.Reference{Path: "/"} diff --git a/pkg/auth/scope/receivedshare.go b/pkg/auth/scope/receivedshare.go index 2974b0ca2a..81236a3c35 100644 --- a/pkg/auth/scope/receivedshare.go +++ b/pkg/auth/scope/receivedshare.go @@ -54,7 +54,10 @@ func receivedShareScope(_ context.Context, scope *authpb.Scope, resource interfa // AddReceivedShareScope adds the scope to allow access to a received user/group share and // the shared resource. func AddReceivedShareScope(share *collaboration.ReceivedShare, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) { - val, err := utils.MarshalProtoV1ToJSON(share) + // Create a new "scope share" to only expose the required fields to the scope. + scopeShare := &collaboration.Share{Id: share.Share.Id, Owner: share.Share.Owner, Creator: share.Share.Creator, ResourceId: share.Share.ResourceId} + + val, err := utils.MarshalProtoV1ToJSON(&collaboration.ReceivedShare{Share: scopeShare}) if err != nil { return nil, err } diff --git a/pkg/auth/scope/share.go b/pkg/auth/scope/share.go index 48683dea17..6284efd007 100644 --- a/pkg/auth/scope/share.go +++ b/pkg/auth/scope/share.go @@ -121,7 +121,10 @@ func checkSharePath(path string) bool { // AddShareScope adds the scope to allow access to a user/group share and // the shared resource. func AddShareScope(share *collaboration.Share, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) { - val, err := utils.MarshalProtoV1ToJSON(share) + // Create a new "scope share" to only expose the required fields to the scope. + scopeShare := &collaboration.Share{Id: share.Id, Owner: share.Owner, Creator: share.Creator, ResourceId: share.ResourceId} + + val, err := utils.MarshalProtoV1ToJSON(scopeShare) if err != nil { return nil, err } diff --git a/pkg/cbox/user/rest/rest.go b/pkg/cbox/user/rest/rest.go index eeedaba59a..fbd1b125b2 100644 --- a/pkg/cbox/user/rest/rest.go +++ b/pkg/cbox/user/rest/rest.go @@ -128,9 +128,7 @@ func (m *manager) Configure(ml map[string]interface{}) error { return nil } -func (m *manager) getUserByParam(ctx context.Context, param, val string) (map[string]interface{}, error) { - url := fmt.Sprintf("%s/Identity?filter=%s:%s&field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type", - m.conf.APIBaseURL, param, url.QueryEscape(val)) +func (m *manager) getUser(ctx context.Context, url string) (map[string]interface{}, error) { responseData, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false) if err != nil { return nil, err @@ -151,17 +149,38 @@ func (m *manager) getUserByParam(ctx context.Context, param, val string) (map[st } if len(users) != 1 { - return nil, errors.New("rest: user not found: " + param + ": " + val) + return nil, errors.New("rest: user not found for URL: " + url) } return users[0], nil } +func (m *manager) getUserByParam(ctx context.Context, param, val string) (map[string]interface{}, error) { + url := fmt.Sprintf("%s/Identity?filter=%s:%s&field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type", + m.conf.APIBaseURL, param, url.QueryEscape(val)) + return m.getUser(ctx, url) +} + +func (m *manager) getLightweightUser(ctx context.Context, mail string) (map[string]interface{}, error) { + url := fmt.Sprintf("%s/Identity?filter=primaryAccountEmail:%s&filter=upn:contains:guest&field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type", + m.conf.APIBaseURL, url.QueryEscape(mail)) + return m.getUser(ctx, url) +} + func (m *manager) getInternalUserID(ctx context.Context, uid *userpb.UserId) (string, error) { internalID, err := m.fetchCachedInternalID(uid) if err != nil { - userData, err := m.getUserByParam(ctx, "upn", uid.OpaqueId) + var ( + userData map[string]interface{} + err error + ) + if uid.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT { + // Lightweight accounts need to be fetched by email + userData, err = m.getLightweightUser(ctx, strings.TrimPrefix(uid.OpaqueId, "guest:")) + } else { + userData, err = m.getUserByParam(ctx, "upn", uid.OpaqueId) + } if err != nil { return "", err } @@ -217,7 +236,16 @@ func (m *manager) parseAndCacheUser(ctx context.Context, userData map[string]int func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User, error) { u, err := m.fetchCachedUserDetails(uid) if err != nil { - userData, err := m.getUserByParam(ctx, "upn", uid.OpaqueId) + var ( + userData map[string]interface{} + err error + ) + if uid.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT { + // Lightweight accounts need to be fetched by email + userData, err = m.getLightweightUser(ctx, strings.TrimPrefix(uid.OpaqueId, "guest:")) + } else { + userData, err = m.getUserByParam(ctx, "upn", uid.OpaqueId) + } if err != nil { return nil, err } @@ -252,7 +280,12 @@ func (m *manager) GetUserByClaim(ctx context.Context, claim, value string) (*use userData, err := m.getUserByParam(ctx, claim, value) if err != nil { - return nil, err + // Lightweight accounts need to be fetched by email + if strings.HasPrefix(value, "guest:") { + if userData, err = m.getLightweightUser(ctx, strings.TrimPrefix(value, "guest:")); err != nil { + return nil, err + } + } } u := m.parseAndCacheUser(ctx, userData) diff --git a/pkg/eosclient/eosbinary/eosbinary.go b/pkg/eosclient/eosbinary/eosbinary.go index 91b45dd037..0da98de2d6 100644 --- a/pkg/eosclient/eosbinary/eosbinary.go +++ b/pkg/eosclient/eosbinary/eosbinary.go @@ -1001,7 +1001,7 @@ func (c *Client) parseFileInfo(raw string) (*eosclient.FileInfo, error) { }) var previousXAttr = "" for _, p := range partsBySpace { - partsByEqual := strings.Split(p, "=") // we have kv pairs like [size 14] + partsByEqual := strings.SplitN(p, "=", 2) // we have kv pairs like [size 14] if len(partsByEqual) == 2 { // handle xattrn and xattrv special cases switch { diff --git a/pkg/storage/utils/acl/acl.go b/pkg/storage/utils/acl/acl.go index 1c493c7224..311ee1a454 100644 --- a/pkg/storage/utils/acl/acl.go +++ b/pkg/storage/utils/acl/acl.go @@ -57,7 +57,13 @@ func Parse(acls string, delimiter string) (*ACLs, error) { if t == "" || isComment(t) { continue } - entry, err := ParseEntry(t) + var err error + var entry *Entry + if strings.HasPrefix(t, TypeLightweight) { + entry, err = ParseLWEntry(t) + } else { + entry, err = ParseEntry(t) + } if err != nil { return nil, err } @@ -133,6 +139,24 @@ func ParseEntry(singleSysACL string) (*Entry, error) { }, nil } +// ParseLWEntry parses a single lightweight ACL +func ParseLWEntry(singleSysACL string) (*Entry, error) { + if !strings.HasPrefix(singleSysACL, TypeLightweight+":") { + return nil, errInvalidACL + } + singleSysACL = strings.TrimPrefix(singleSysACL, TypeLightweight+":") + + tokens := strings.Split(singleSysACL, "=") + if len(tokens) != 2 { + return nil, errInvalidACL + } + return &Entry{ + Type: TypeLightweight, + Qualifier: tokens[0], + Permissions: tokens[1], + }, nil +} + // CitrineSerialize serializes an ACL entry for citrine EOS ACLs func (a *Entry) CitrineSerialize() string { return fmt.Sprintf("%s:%s=%s", a.Type, a.Qualifier, a.Permissions) diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 028e7d90d7..50cdc8dfd9 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -811,7 +811,18 @@ func (fs *eosfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []st if err != nil { return nil, err } - auth, err := fs.getUserAuth(ctx, u, "") + + fn := "" + if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT { + p, err := fs.resolve(ctx, ref) + if err != nil { + return nil, errors.Wrap(err, "eosfs: error resolving reference") + } + + fn = fs.wrap(ctx, p) + } + + auth, err := fs.getUserAuth(ctx, u, fn) if err != nil { return nil, err } @@ -838,7 +849,7 @@ func (fs *eosfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []st } } - fn := fs.wrap(ctx, p) + fn = fs.wrap(ctx, p) eosFileInfo, err := fs.c.GetFileInfoByPath(ctx, auth, fn) if err != nil { return nil, err