diff --git a/changelog/unreleased/capabilities-public.md b/changelog/unreleased/capabilities-public.md new file mode 100644 index 0000000000..0cb5029340 --- /dev/null +++ b/changelog/unreleased/capabilities-public.md @@ -0,0 +1,3 @@ +Enhancement: Make capabilities endpoint public, authenticate users is present + +https://github.com/cs3org/reva/pull/2698 \ No newline at end of file diff --git a/internal/grpc/services/authprovider/authprovider.go b/internal/grpc/services/authprovider/authprovider.go index f3a27dbd82..ae4c7c1050 100644 --- a/internal/grpc/services/authprovider/authprovider.go +++ b/internal/grpc/services/authprovider/authprovider.go @@ -138,7 +138,7 @@ func (s *service) Authenticate(ctx context.Context, req *provider.AuthenticateRe u, scope, err := s.authmgr.Authenticate(ctx, username, password) switch v := err.(type) { case nil: - log.Info().Msgf("user %s authenticated", u.String()) + log.Info().Msgf("user %s authenticated", u.Id) return &provider.AuthenticateResponse{ Status: status.NewOK(ctx), User: u, diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 4e5a07c80e..c73b8afacb 100644 --- a/internal/http/interceptors/auth/auth.go +++ b/internal/http/interceptors/auth/auth.go @@ -19,6 +19,7 @@ package auth import ( + "context" "fmt" "net/http" "strings" @@ -35,14 +36,17 @@ import ( "github.com/cs3org/reva/pkg/auth" "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/rhttp/global" "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/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" + "github.com/rs/zerolog" "google.golang.org/grpc/metadata" ) @@ -151,7 +155,6 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err chain := func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() // OPTION requests need to pass for preflight requests // TODO(labkode): this will break options for auth protected routes. // Maybe running the CORS middleware before auth kicks in is enough. @@ -160,135 +163,156 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err return } - log := appctx.GetLogger(ctx) + log := appctx.GetLogger(r.Context()) + isUnprotectedEndpoint := false - client, err := pool.GetGatewayServiceClient(conf.GatewaySvc) - if err != nil { - log.Error().Err(err).Msg("error getting the authsvc client") - w.WriteHeader(http.StatusUnauthorized) - return - } - - // skip auth for urls set in the config. - // TODO(labkode): maybe use method:url to bypass auth. + // For unprotected URLs, we try to authenticate the request in case some service needs it, + // but don't return any errors if it fails. if utils.Skip(r.URL.Path, unprotected) { log.Info().Msg("skipping auth check for: " + r.URL.Path) - h.ServeHTTP(w, r) - return + isUnprotectedEndpoint = true } - tkn := tokenStrategy.GetToken(r) - if tkn == "" { - log.Warn().Msg("core access token not set") - - userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain) - - // obtain credentials (basic auth, bearer token, ...) based on user agent - var creds *auth.Credentials - for _, k := range userAgentCredKeys { - creds, err = credChain[k].GetCredentials(w, r) - if err != nil { - log.Debug().Err(err).Msg("error retrieving credentials") - } - - if creds != nil { - log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID) - break - } - } - - // if no credentials are found, reply with authentication challenge depending on user agent - if creds == nil { - for _, key := range userAgentCredKeys { - if cred, ok := credChain[key]; ok { - cred.AddWWWAuthenticate(w, r, conf.Realm) - } else { - panic("auth credential strategy: " + key + "must have been loaded in init method") - } - } - w.WriteHeader(http.StatusUnauthorized) + ctx, err := authenticateUser(w, r, conf, unprotected, tokenStrategy, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint) + if err != nil { + if !isUnprotectedEndpoint { return } + } else { + r = r.WithContext(ctx) + } + h.ServeHTTP(w, r) + }) + } + return chain, nil +} - req := &gateway.AuthenticateRequest{ - Type: creds.Type, - ClientId: creds.ClientID, - ClientSecret: creds.ClientSecret, - } +func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, unprotected []string, tokenStrategy auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) { + ctx := r.Context() + log := appctx.GetLogger(ctx) - log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc) + // Add the request user-agent to the ctx + ctx = metadata.NewIncomingContext(ctx, metadata.New(map[string]string{ctxpkg.UserAgentHeader: r.UserAgent()})) - res, err := client.Authenticate(ctx, req) - if err != nil { - log.Error().Err(err).Msg("error calling Authenticate") - w.WriteHeader(http.StatusUnauthorized) - return - } + client, err := pool.GetGatewayServiceClient(conf.GatewaySvc) + if err != nil { + logError(isUnprotectedEndpoint, log, err, "error getting the authsvc client", http.StatusUnauthorized, w) + return nil, err + } - if res.Status.Code != rpc.Code_CODE_OK { - err := status.NewErrorFromCode(res.Status.Code, "auth") - log.Err(err).Msg("error generating access token from credentials") - w.WriteHeader(http.StatusUnauthorized) - return - } + tkn := tokenStrategy.GetToken(r) + if tkn == "" { + log.Warn().Msg("core access token not set") - log.Info().Msg("core access token generated") - // write token to response - tkn = res.Token - tokenWriter.WriteToken(tkn, w) - } else { - log.Debug().Msg("access token is already provided") - } + userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain) - // validate token - u, tokenScope, err := tokenManager.DismantleToken(r.Context(), tkn) + // obtain credentials (basic auth, bearer token, ...) based on user agent + var creds *auth.Credentials + for _, k := range userAgentCredKeys { + creds, err = credChain[k].GetCredentials(w, r) if err != nil { - log.Error().Err(err).Msg("error dismantling token") - w.WriteHeader(http.StatusUnauthorized) - return + log.Debug().Err(err).Msg("error retrieving credentials") } - if sharedconf.SkipUserGroupsInToken() { - var groups []string - if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil { - groups = groupsIf.([]string) - } else { - groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id}) - if err != nil { - log.Error().Err(err).Msg("error retrieving user groups") - w.WriteHeader(http.StatusInternalServerError) - return + if creds != nil { + log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID) + break + } + } + + // if no credentials are found, reply with authentication challenge depending on user agent + if creds == nil { + if !isUnprotectedEndpoint { + for _, key := range userAgentCredKeys { + if cred, ok := credChain[key]; ok { + cred.AddWWWAuthenticate(w, r, conf.Realm) + } else { + panic("auth credential strategy: " + key + "must have been loaded in init method") } - groups = groupsRes.Groups - _ = userGroupsCache.SetWithExpire(u.Id.OpaqueId, groupsRes.Groups, 3600*time.Second) } - u.Groups = groups + w.WriteHeader(http.StatusUnauthorized) } + return nil, errtypes.PermissionDenied("no credentials found") + } + + req := &gateway.AuthenticateRequest{ + Type: creds.Type, + ClientId: creds.ClientID, + ClientSecret: creds.ClientSecret, + } + + log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc) + + res, err := client.Authenticate(ctx, req) + if err != nil { + logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w) + return nil, err + } + + if res.Status.Code != rpc.Code_CODE_OK { + err := status.NewErrorFromCode(res.Status.Code, "auth") + logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w) + return nil, err + } + + log.Info().Msg("core access token generated") + // write token to response + tkn = res.Token + tokenWriter.WriteToken(tkn, w) + } else { + log.Debug().Msg("access token is already provided") + } + + // validate token + u, tokenScope, err := tokenManager.DismantleToken(r.Context(), tkn) + if err != nil { + logError(isUnprotectedEndpoint, log, err, "error dismantling token", http.StatusUnauthorized, w) + return nil, err + } - // ensure access to the resource is allowed - ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path) + if sharedconf.SkipUserGroupsInToken() { + var groups []string + if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil { + groups = groupsIf.([]string) + } else { + groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id}) 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 + logError(isUnprotectedEndpoint, log, err, "error retrieving user groups", http.StatusInternalServerError, w) + return nil, err } + groups = groupsRes.Groups + _ = userGroupsCache.SetWithExpire(u.Id.OpaqueId, groupsRes.Groups, 3600*time.Second) + } + u.Groups = groups + } + + // ensure access to the resource is allowed + ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path) + if err != nil { + logError(isUnprotectedEndpoint, log, err, "error verifying scope of access token", http.StatusInternalServerError, w) + return nil, err + } + if !ok { + err := errtypes.PermissionDenied("access to resource not allowed") + logError(isUnprotectedEndpoint, log, err, "access to resource not allowed", http.StatusUnauthorized, w) + return nil, err + } - // store user and core access token in context. - ctx = ctxpkg.ContextSetUser(ctx, u) - ctx = ctxpkg.ContextSetToken(ctx, tkn) - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials? + // store user and core access token in context. + ctx = ctxpkg.ContextSetUser(ctx, u) + ctx = ctxpkg.ContextSetToken(ctx, tkn) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials? - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) - r = r.WithContext(ctx) - h.ServeHTTP(w, r) - }) + return ctx, nil +} + +func logError(isUnprotectedEndpoint bool, log *zerolog.Logger, err error, msg string, status int, w http.ResponseWriter) { + if !isUnprotectedEndpoint { + log.Error().Err(err).Msg(msg) + w.WriteHeader(status) } - return chain, nil } // getCredsForUserAgent returns the WWW Authenticate challenges keys to use given an http request diff --git a/internal/http/services/owncloud/ocs/cache.go b/internal/http/services/owncloud/ocs/cache.go index 8381f6150e..8ab887d4c6 100644 --- a/internal/http/services/owncloud/ocs/cache.go +++ b/internal/http/services/owncloud/ocs/cache.go @@ -30,8 +30,12 @@ import ( func (s *svc) cacheWarmup(w http.ResponseWriter, r *http.Request) { if s.warmupCacheTracker != nil { - u := ctxpkg.ContextMustGetUser(r.Context()) - tkn := ctxpkg.ContextMustGetToken(r.Context()) + u, ok1 := ctxpkg.ContextGetUser(r.Context()) + tkn, ok2 := ctxpkg.ContextGetToken(r.Context()) + if !ok1 || !ok2 { + return + } + log := appctx.GetLogger(r.Context()) // We make a copy of the context because the original one comes with its cancel channel, diff --git a/internal/http/services/owncloud/ocs/ocs.go b/internal/http/services/owncloud/ocs/ocs.go index d411f4dace..8a166ee0ba 100644 --- a/internal/http/services/owncloud/ocs/ocs.go +++ b/internal/http/services/owncloud/ocs/ocs.go @@ -83,7 +83,7 @@ func (s *svc) Close() error { } func (s *svc) Unprotected() []string { - return []string{} + return []string{"/v1.php/cloud/capabilities", "/v2.php/cloud/capabilities"} } func (s *svc) routerInit() error { diff --git a/pkg/auth/manager/oidc/oidc.go b/pkg/auth/manager/oidc/oidc.go index e882b399ba..ac5e86d638 100644 --- a/pkg/auth/manager/oidc/oidc.go +++ b/pkg/auth/manager/oidc/oidc.go @@ -33,6 +33,7 @@ import ( "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/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/sharedconf" @@ -178,7 +179,7 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) return nil, nil, errors.Wrap(err, "oidc: error getting user groups") } if getGroupsResp.Status.Code != rpc.Code_CODE_OK { - return nil, nil, errors.Wrap(err, "oidc: grpc getting user groups failed") + return nil, nil, status.NewErrorFromCode(getGroupsResp.Status.Code, "oidc") } u := &user.User{ diff --git a/pkg/cbox/utils/conversions.go b/pkg/cbox/utils/conversions.go index 1391df675a..3148e24377 100644 --- a/pkg/cbox/utils/conversions.go +++ b/pkg/cbox/utils/conversions.go @@ -255,7 +255,7 @@ func ConvertToCS3PublicShare(s DBShare) *link.PublicShare { } var expires *typespb.Timestamp if s.Expiration != "" { - t, err := time.Parse("2006-01-02 03:04:05", s.Expiration) + t, err := time.Parse("2006-01-02 15:04:05", s.Expiration) if err == nil { expires = &typespb.Timestamp{ Seconds: uint64(t.Unix()), diff --git a/pkg/storage/utils/acl/acl.go b/pkg/storage/utils/acl/acl.go index 802bcf3a1a..e678aeb8de 100644 --- a/pkg/storage/utils/acl/acl.go +++ b/pkg/storage/utils/acl/acl.go @@ -124,22 +124,26 @@ type Entry struct { // ParseEntry parses a single ACL func ParseEntry(singleSysACL string) (*Entry, error) { tokens := strings.Split(singleSysACL, ":") - if len(tokens) != 3 { - if len(tokens) == 2 { - // The ACL entries might be stored as type:qualifier=permissions - // Handle that case separately - parts := (strings.Split(tokens[1], "=")) - tokens = []string{tokens[0], parts[0], parts[1]} - } else { - return nil, errInvalidACL + switch len(tokens) { + case 2: + // The ACL entries might be stored as type:qualifier=permissions + // Handle that case separately + parts := strings.SplitN(tokens[1], "=", 2) + if len(parts) == 2 { + return &Entry{ + Type: tokens[0], + Qualifier: parts[0], + Permissions: parts[1], + }, nil } + case 3: + return &Entry{ + Type: tokens[0], + Qualifier: tokens[1], + Permissions: tokens[2], + }, nil } - - return &Entry{ - Type: tokens[0], - Qualifier: tokens[1], - Permissions: tokens[2], - }, nil + return nil, errInvalidACL } // ParseLWEntry parses a single lightweight ACL