Skip to content

Commit

Permalink
Make capabilities endpoint public, authenticate users is present (#2698)
Browse files Browse the repository at this point in the history
  • Loading branch information
ishank011 authored Mar 30, 2022
1 parent 7f7b2c5 commit 6fb72f1
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 124 deletions.
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

0 comments on commit 6fb72f1

Please sign in to comment.