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

Make capabilities endpoint public, authenticate users is present #2698

Merged
merged 5 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/unreleased/capabilities-public.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Enhancement: Make capabilities endpoint public, authenticate users is present

https://github.com/cs3org/reva/pull/2698
2 changes: 1 addition & 1 deletion internal/grpc/services/authprovider/authprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
232 changes: 128 additions & 104 deletions internal/http/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package auth

import (
"context"
"fmt"
"net/http"
"strings"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions internal/http/services/owncloud/ocs/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion internal/http/services/owncloud/ocs/ocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion pkg/auth/manager/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion pkg/cbox/utils/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
32 changes: 18 additions & 14 deletions pkg/storage/utils/acl/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down